// ScheduledTasksModal renders schedule/routine cards using .scheduling-*, .routine-*, // .schedule-form classes that live in ScriptsModal.css. Both modals share that file. import "./ScriptsModal.css"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Plus, Zap, Globe, Folder, X } from "lucide-react"; import type { Routine, RoutineCreateInput } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import { fetchRoutines, createRoutine, updateRoutine, deleteRoutine, runRoutine, } from "../api"; import { RoutineCard } from "./RoutineCard"; import { RoutineEditor } from "./RoutineEditor"; import type { ToastType } from "../hooks/useToast"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; /** Polling interval for auto-refreshing the schedule/routine list (30 seconds). */ const POLL_INTERVAL_MS = 30_000; /** Scheduling scope: global (user-level) or project-scoped. */ export type SchedulingScope = "global" | "project"; interface ScheduledTasksModalProps { onClose: () => void; addToast: (message: string, type?: ToastType) => void; /** Optional project ID for project-scoped scheduling. When provided, scope defaults to "project". */ projectId?: string; } export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledTasksModalProps) { // Scope state: defaults to "project" when projectId exists, else "global" const [activeScope, setActiveScope] = useState(() => projectId ? "project" : "global"); // Routine state const [routines, setRoutines] = useState([]); const [routineView, setRoutineView] = useState<"list" | "create" | "edit">("list"); const [editingRoutine, setEditingRoutine] = useState(); const [runningRoutineId, setRunningRoutineId] = useState(null); const [lastRunOutput, setLastRunOutput] = useState>({}); const modalRef = useRef(null); useModalResizePersist(modalRef, true, "fusion:automation-modal-size"); // Build scope options for API calls const scopeOptions = useMemo(() => ({ scope: activeScope, projectId: activeScope === "project" ? projectId : undefined, }), [activeScope, projectId]); // Load routines const loadRoutines = useCallback(async () => { try { const data = await fetchRoutines(scopeOptions); setRoutines(data); setLastRunOutput((previous) => { const next = { ...previous }; for (const routine of data) { const pendingOutput = next[routine.id]; if (!pendingOutput || !routine.lastRunResult) continue; const reflected = routine.lastRunResult; if ( reflected.success === pendingOutput.success && (reflected.output || "") === pendingOutput.output && (reflected.error || "") === (pendingOutput.error || "") ) { delete next[routine.id]; } } return next; }); } catch (err) { addToast(getErrorMessage(err) || "Failed to load routines", "error"); } }, [addToast, scopeOptions]); useEffect(() => { void loadRoutines(); }, [loadRoutines]); // Poll for updates while modal is open useEffect(() => { const interval = setInterval(() => { void loadRoutines(); }, POLL_INTERVAL_MS); return () => clearInterval(interval); }, [loadRoutines]); // Close on Escape (only when not in a sub-form) useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") { if (routineView !== "list") { setRoutineView("list"); setEditingRoutine(undefined); } else { onClose(); } } }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [onClose, routineView]); const overlayDismissProps = useOverlayDismiss(onClose); // ── Routine CRUD handlers ─────────────────────────────────────────────── const handleCreateRoutine = useCallback( async (input: RoutineCreateInput) => { try { await createRoutine(input, scopeOptions); addToast("Routine created", "success"); setRoutineView("list"); await loadRoutines(); } catch (err) { addToast(getErrorMessage(err) || "Failed to create routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleEditRoutine = useCallback((routine: Routine) => { setEditingRoutine(routine); setRoutineView("edit"); }, []); const handleUpdateRoutine = useCallback( async (input: RoutineCreateInput) => { if (!editingRoutine) return; try { await updateRoutine(editingRoutine.id, input, scopeOptions); addToast("Routine updated", "success"); setRoutineView("list"); setEditingRoutine(undefined); await loadRoutines(); } catch (err) { addToast(getErrorMessage(err) || "Failed to update routine", "error"); } }, [editingRoutine, addToast, loadRoutines, scopeOptions], ); const handleDeleteRoutine = useCallback( async (routine: Routine) => { try { await deleteRoutine(routine.id, scopeOptions); addToast(`Deleted "${routine.name}"`, "success"); await loadRoutines(); } catch (err) { addToast(getErrorMessage(err) || "Failed to delete routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleRunRoutine = useCallback( async (routine: Routine) => { setRunningRoutineId(routine.id); try { const { result } = await runRoutine(routine.id, scopeOptions); setLastRunOutput((previous) => ({ ...previous, [routine.id]: { output: result.output || "", error: result.error, success: result.success, }, })); if (result.success) { addToast(`"${routine.name}" completed successfully`, "success"); } else { addToast(`"${routine.name}" failed: ${result.error || "Unknown error"}`, "error"); } await loadRoutines(); } catch (err) { addToast(getErrorMessage(err) || "Failed to run routine", "error"); } finally { setRunningRoutineId(null); } }, [addToast, loadRoutines, scopeOptions], ); const handleToggleRoutine = useCallback( async (routine: Routine) => { try { await updateRoutine(routine.id, { enabled: !routine.enabled }, scopeOptions); addToast( `"${routine.name}" ${routine.enabled ? "disabled" : "enabled"}`, "success", ); await loadRoutines(); } catch (err) { addToast(getErrorMessage(err) || "Failed to toggle routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleRoutineCancel = useCallback(() => { setRoutineView("list"); setEditingRoutine(undefined); }, []); useEffect(() => { if (routineView !== "list") { setLastRunOutput({}); } }, [routineView]); // ── Scope switch handler ─────────────────────────────────────────────── const handleScopeSwitch = useCallback((scope: SchedulingScope) => { setActiveScope(scope); // Reset to list view when switching scope setRoutineView("list"); setEditingRoutine(undefined); setLastRunOutput({}); }, []); // ── Render content ───────────────────────────────────────────────────── const renderRoutinesContent = () => { if (routineView === "create") { return ; } if (routineView === "edit" && editingRoutine) { return ( ); } // List view if (routines.length === 0) { return (

No automations yet

Create an automation with a schedule, webhook, API, or manual trigger.

); } return (
{routines.map((r) => ( ))}
); }; const renderContent = () => { return renderRoutinesContent(); }; // Determine if we're in "list" view for showing the "New" button const isShowingList = routineView === "list" && routines.length > 0; return (

Automations

{routines.length} automation{routines.length === 1 ? "" : "s"}
{isShowingList && ( )}
{renderContent()}
); }