import { useState, useCallback } from "react"; import { Calendar, Webhook, Code, Zap, Globe, Folder } from "lucide-react"; import type { Routine, RoutineCreateInput, RoutineUpdateInput, RoutineTrigger, RoutineTriggerType, RoutineCronTrigger, RoutineWebhookTrigger, RoutineApiTrigger, RoutineManualTrigger, RoutineCatchUpPolicy, RoutineExecutionPolicy, } from "@fusion/core"; /** * 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)); } /** * Build a trigger object from form state. */ function buildTrigger( triggerType: RoutineTriggerType, cronExpression: string, webhookPath: string, webhookSecret: string, endpoint: string, ): RoutineTrigger { switch (triggerType) { case "cron": return { type: "cron", cronExpression } as RoutineCronTrigger; case "webhook": return { type: "webhook", webhookPath: webhookPath || "/trigger/" + Math.random().toString(36).slice(2, 10), secret: webhookSecret || undefined, } as RoutineWebhookTrigger; case "api": return { type: "api", endpoint: endpoint || "/api/routine/" + Math.random().toString(36).slice(2, 10), } as RoutineApiTrigger; case "manual": return { type: "manual" } as RoutineManualTrigger; } } /** * Extract trigger fields from a Routine object. */ function extractTriggerFields(routine: Routine) { const trigger = routine.trigger; switch (trigger.type) { case "cron": return { triggerType: "cron" as RoutineTriggerType, cronExpression: (trigger as RoutineCronTrigger).cronExpression, webhookPath: "", webhookSecret: "", endpoint: "", }; case "webhook": return { triggerType: "webhook" as RoutineTriggerType, cronExpression: "", webhookPath: (trigger as RoutineWebhookTrigger).webhookPath, webhookSecret: (trigger as RoutineWebhookTrigger).secret || "", endpoint: "", }; case "api": return { triggerType: "api" as RoutineTriggerType, cronExpression: "", webhookPath: "", webhookSecret: "", endpoint: (trigger as RoutineApiTrigger).endpoint, }; case "manual": return { triggerType: "manual" as RoutineTriggerType, cronExpression: "", webhookPath: "", webhookSecret: "", endpoint: "", }; } } const TRIGGER_TYPE_LABELS: Record = { cron: "Cron Schedule", webhook: "Webhook", api: "API", manual: "Manual", }; const EXECUTION_POLICY_OPTIONS: { value: RoutineExecutionPolicy; label: string }[] = [ { value: "parallel", label: "Allow concurrent runs" }, { value: "queue", label: "Queue after current (one at a time)" }, { value: "reject", label: "Reject new runs while running" }, ]; const CATCH_UP_POLICY_OPTIONS: { value: RoutineCatchUpPolicy; label: string }[] = [ { value: "skip", label: "Skip missed runs" }, { value: "run_one", label: "Run the most recent missed run" }, { value: "run", label: "Run all missed runs" }, ]; interface RoutineEditorProps { /** Existing routine for editing. Omit for create mode. */ routine?: Routine; /** Called with form data on submit. */ onSubmit: (input: RoutineCreateInput) => Promise; /** Called when the user cancels. */ onCancel: () => void; /** Scope for the routine (global or project). Defaults to routine.scope or "project". */ scope?: "global" | "project"; /** Project ID for project-scoped routines. */ projectId?: string; } export function RoutineEditor({ routine, onSubmit, onCancel, scope: formScope, projectId }: RoutineEditorProps) { const isEditing = !!routine; // Extract trigger fields if editing const initialTriggerFields = routine ? extractTriggerFields(routine) : { triggerType: "cron" as RoutineTriggerType, cronExpression: "0 * * * *", webhookPath: "", webhookSecret: "", endpoint: "", }; const [name, setName] = useState(routine?.name ?? ""); const [description, setDescription] = useState(routine?.description ?? ""); const [triggerType, setTriggerType] = useState(initialTriggerFields.triggerType); const [cronExpression, setCronExpression] = useState(initialTriggerFields.cronExpression); const [webhookPath, setWebhookPath] = useState(initialTriggerFields.webhookPath); const [webhookSecret, setWebhookSecret] = useState(initialTriggerFields.webhookSecret); const [endpoint, setEndpoint] = useState(initialTriggerFields.endpoint); const [executionPolicy, setExecutionPolicy] = useState( routine?.executionPolicy ?? "queue" ); const [catchUpPolicy, setCatchUpPolicy] = useState( routine?.catchUpPolicy ?? "run_one" ); const [enabled, setEnabled] = useState(routine?.enabled ?? true); const [errors, setErrors] = useState>({}); const [submitting, setSubmitting] = useState(false); const validate = useCallback((): boolean => { const e: Record = {}; if (!name.trim()) e.name = "Name is required"; // Scope validation: project scope requires projectId if (formScope === "project" && !projectId) { e.scope = "Project-specific entries require an active project."; } if (triggerType === "cron") { if (!cronExpression.trim()) { e.cronExpression = "Cron expression is required"; } else if (!isLikelyCron(cronExpression)) { e.cronExpression = "Invalid cron format — expected 5 fields (e.g. '0 */6 * * *')"; } } if (triggerType === "webhook" && !webhookPath.trim()) { e.webhookPath = "Webhook path is required"; } if (triggerType === "api" && !endpoint.trim()) { e.endpoint = "API endpoint is required"; } setErrors(e); return Object.keys(e).length === 0; }, [name, triggerType, cronExpression, webhookPath, endpoint, formScope, projectId]); const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!validate()) return; setSubmitting(true); try { // Determine scope: use edit mode's existing scope, otherwise use formScope prop // When formScope is "project" but no projectId provided, fall back to "global" let effectiveScope = routine?.scope ?? formScope ?? (projectId ? "project" : "global"); if (effectiveScope === "project" && !projectId) { effectiveScope = "global"; } const trigger = buildTrigger(triggerType, cronExpression, webhookPath, webhookSecret, endpoint); const input: RoutineCreateInput = { name: name.trim(), agentId: routine?.agentId ?? "", description: description.trim() || undefined, trigger, executionPolicy, catchUpPolicy, enabled, scope: effectiveScope, }; await onSubmit(input); } finally { setSubmitting(false); } }, [validate, onSubmit, name, description, triggerType, cronExpression, webhookPath, webhookSecret, endpoint, executionPolicy, catchUpPolicy, enabled, formScope, projectId, routine?.scope], ); const nameErrorId = "routine-name-error"; const cronErrorId = "routine-cron-error"; const webhookErrorId = "routine-webhook-error"; const endpointErrorId = "routine-endpoint-error"; return (

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

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