import { useState, useCallback, useEffect } from "react"; import type { ScheduledTask, ScheduledTaskCreateInput, ScheduleType, AutomationStep } from "@fusion/core"; import { ScheduleStepsEditor } from "./ScheduleStepsEditor"; /** Mapping from preset schedule types to their cron expressions. Mirrored from @fusion/core. */ const PRESET_CRON: Record, string> = { hourly: "0 * * * *", daily: "0 0 * * *", weekly: "0 0 * * 1", monthly: "0 0 1 * *", every15Minutes: "*/15 * * * *", every30Minutes: "*/30 * * * *", every2Hours: "0 */2 * * *", every6Hours: "0 */6 * * *", every12Hours: "0 */12 * * *", weekdays: "0 9 * * 1-5", }; const SCHEDULE_TYPE_LABELS: Record = { hourly: "Every hour", daily: "Every day (midnight)", weekly: "Every week (Monday)", monthly: "Every month (1st)", custom: "Custom cron expression", every15Minutes: "Every 15 minutes", every30Minutes: "Every 30 minutes", every2Hours: "Every 2 hours", every6Hours: "Every 6 hours", every12Hours: "Every 12 hours", weekdays: "Weekdays at 9 AM (Mon-Fri)", }; /** * 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)); } type ScheduleMode = "simple" | "advanced"; 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; // Determine initial mode based on whether the schedule has steps const initialMode: ScheduleMode = schedule?.steps && schedule.steps.length > 0 ? "advanced" : "simple"; const [mode, setMode] = useState(initialMode); 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 [steps, setSteps] = useState(schedule?.steps ?? []); const [hasEditingSteps, setHasEditingSteps] = useState(false); 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 (mode === "simple" && !command.trim()) e.command = "Command is required"; if (mode === "advanced" && steps.length === 0) e.steps = "At least one step is required"; // Validate step content in multi-step mode if (mode === "advanced" && steps.length > 0) { const incompleteSteps: string[] = []; for (let i = 0; i < steps.length; i++) { const step = steps[i]; if (!step.name?.trim()) { incompleteSteps.push(`Step ${i + 1}: Name is required`); } if (step.type === "command" && !step.command?.trim()) { incompleteSteps.push(`Step ${i + 1}: Command is required`); } if (step.type === "ai-prompt" && !step.prompt?.trim()) { incompleteSteps.push(`Step ${i + 1}: Prompt is required`); } } if (incompleteSteps.length > 0) { e.steps = incompleteSteps.join("; "); } // Check if any steps are currently being edited if (hasEditingSteps) { e.stepsEditing = "Please save or cancel all step edits before saving the schedule"; } } 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, mode, steps, scheduleType, cronExpression, timeoutMs, hasEditingSteps]); 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: mode === "simple" ? command.trim() : "", enabled, timeoutMs, steps: mode === "advanced" ? steps : undefined, }); } finally { setSubmitting(false); } }, [validate, onSubmit, name, description, scheduleType, cronExpression, command, enabled, timeoutMs, mode, steps], ); 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} )}