import { useCallback, useMemo, useReducer, useState, type ReactNode } from "react"; import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import type { ScheduleCadence } from "@getpaseo/protocol/schedule/types"; import type { FieldControlSize } from "@/components/ui/control-geometry"; import { Field, FormTextInput } from "@/components/ui/form-field"; import { SelectField, type SelectFieldOption } from "@/components/ui/select-field"; import { CADENCE_PRESET_OPTIONS, normalizeScheduleFormCadence, resolveCronPresetDisplay, resolveCronPresetId, } from "@/schedules/schedule-cadence-options"; import { getDeviceTimeZone } from "@/utils/device-timezone"; import { describeCron, validateCron } from "@/utils/schedule-format"; type CronCadence = Extract; export interface CadenceEditorProps { value: ScheduleCadence; onChange: (next: ScheduleCadence) => void; error?: string; size?: FieldControlSize; } const PRESET_OPTIONS: SelectFieldOption[] = CADENCE_PRESET_OPTIONS.map((option) => ({ id: option.id, value: option.id, label: option.label, testID: `schedule-cadence-preset-${option.id}`, })); function getCronPreview(expression: string, timezone: string, error: string | null): string | null { if (error || !expression) { return null; } return describeCron({ type: "cron", expression, timezone }) ?? expression; } function buildCronCadence(expression: string, timezone: string): CronCadence { return { type: "cron", expression, timezone }; } export function CadenceEditor({ value, onChange, error, size = "md" }: CadenceEditorProps) { const deviceTimeZone = useMemo(getDeviceTimeZone, []); const normalizedValue = normalizeScheduleFormCadence(value, deviceTimeZone); const [cronText, setCronText] = useState(() => normalizedValue.expression); const [fieldResetKey, bumpFieldResetKey] = useReducer((key: number) => key + 1, 0); const cronTimeZone = normalizedValue.timezone ?? deviceTimeZone; const trimmedCron = cronText.trim(); const localCronError = trimmedCron ? validateCron(trimmedCron) : null; const effectiveError = error ?? localCronError; const preview = getCronPreview(trimmedCron, cronTimeZone, effectiveError ?? null); const currentCadence = useMemo( () => buildCronCadence(trimmedCron, cronTimeZone), [cronTimeZone, trimmedCron], ); const selectedPresetId = resolveCronPresetId(currentCadence); const selectedPresetDisplay = resolveCronPresetDisplay(currentCadence); const handlePresetChange = useCallback( (presetId: string) => { const preset = CADENCE_PRESET_OPTIONS.find((option) => option.id === presetId); if (!preset) { return; } setCronText(preset.expression); bumpFieldResetKey(); onChange(buildCronCadence(preset.expression, cronTimeZone)); }, [cronTimeZone, onChange], ); const handleCronChange = useCallback( (text: string) => { setCronText(text); onChange(buildCronCadence(text.trim(), cronTimeZone)); }, [cronTimeZone, onChange], ); let feedback: ReactNode = null; if (effectiveError) { feedback = {effectiveError}; } else if (preview) { feedback = {preview}; } return ( {feedback} ); } const styles = StyleSheet.create((theme) => ({ stack: { gap: theme.spacing[3], }, cronInput: { fontFamily: theme.fontFamily.mono, }, preview: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, error: { fontSize: theme.fontSize.sm, color: theme.colors.palette.red[300], }, }));