import { useState, useCallback, useEffect } from "react"; import type { ScheduledTask, ScheduledTaskCreateInput, ScheduleType } from "@kb/core"; /** Mapping from preset schedule types to their cron expressions. Mirrored from @kb/core. */ const PRESET_CRON: Record, string> = { hourly: "0 * * * *", daily: "0 0 * * *", weekly: "0 0 * * 1", monthly: "0 0 1 * *", }; const SCHEDULE_TYPE_LABELS: Record = { hourly: "Every hour", daily: "Every day (midnight)", weekly: "Every week (Monday)", monthly: "Every month (1st)", custom: "Custom cron expression", }; /** * Simple cron expression validator (5-field format). * Checks basic structure — authoritative validation happens server-side. */ function isLikelyCron(expr: string): boolean { const parts = expr.trim().split(/\s+/); if (parts.length !== 5) return false; // Each field should contain digits, *, /, -, or , return parts.every((p) => /^[\d*,/\-]+$/.test(p)); } interface ScheduleFormProps { /** Existing schedule for editing. Omit for create mode. */ schedule?: ScheduledTask; /** Called with form data on submit. */ onSubmit: (input: ScheduledTaskCreateInput) => Promise; /** Called when the user cancels. */ onCancel: () => void; } export function ScheduleForm({ schedule, onSubmit, onCancel }: ScheduleFormProps) { const isEditing = !!schedule; const [name, setName] = useState(schedule?.name ?? ""); const [description, setDescription] = useState(schedule?.description ?? ""); const [scheduleType, setScheduleType] = useState(schedule?.scheduleType ?? "daily"); const [cronExpression, setCronExpression] = useState(schedule?.cronExpression ?? ""); const [command, setCommand] = useState(schedule?.command ?? ""); const [enabled, setEnabled] = useState(schedule?.enabled ?? true); const [timeoutMs, setTimeoutMs] = useState(schedule?.timeoutMs ?? 300000); const [errors, setErrors] = useState>({}); const [submitting, setSubmitting] = useState(false); // Auto-fill cron expression when preset is selected useEffect(() => { if (scheduleType !== "custom") { setCronExpression(PRESET_CRON[scheduleType]); } }, [scheduleType]); const validate = useCallback((): boolean => { const e: Record = {}; if (!name.trim()) e.name = "Name is required"; if (!command.trim()) e.command = "Command is required"; if (scheduleType === "custom") { if (!cronExpression.trim()) { e.cronExpression = "Cron expression is required for custom schedules"; } else if (!isLikelyCron(cronExpression)) { e.cronExpression = "Invalid cron format — expected 5 fields (e.g. '0 */6 * * *')"; } } if (timeoutMs < 1000) { e.timeoutMs = "Timeout must be at least 1 second (1000ms)"; } setErrors(e); return Object.keys(e).length === 0; }, [name, command, scheduleType, cronExpression, timeoutMs]); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!validate()) return; setSubmitting(true); try { await onSubmit({ name: name.trim(), description: description.trim() || undefined, scheduleType, cronExpression: scheduleType === "custom" ? cronExpression.trim() : undefined, command: command.trim(), enabled, timeoutMs, }); } finally { setSubmitting(false); } }, [validate, onSubmit, name, description, scheduleType, cronExpression, command, enabled, timeoutMs], ); const cronFieldId = "schedule-cron"; const cronErrorId = "schedule-cron-error"; const nameErrorId = "schedule-name-error"; const commandErrorId = "schedule-command-error"; const timeoutErrorId = "schedule-timeout-error"; return (

{isEditing ? "Edit Schedule" : "New Schedule"}

setName(e.target.value)} aria-invalid={!!errors.name} aria-describedby={errors.name ? nameErrorId : undefined} /> {errors.name && ( {errors.name} )}