diff --git a/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/trigger-card.tsx b/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/trigger-card.tsx index a1d84d2d7..681877523 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/trigger-card.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/automations/[automation_id]/components/trigger-card.tsx @@ -1,12 +1,13 @@ "use client"; import { useAtomValue } from "jotai"; -import { CalendarClock, Clock, Trash2 } from "lucide-react"; +import { AlertCircle, CalendarClock, Clock, Pencil, Save, Trash2 } from "lucide-react"; import { useState } from "react"; import { updateTriggerMutationAtom } from "@/atoms/automations/automations-mutation.atoms"; import { JsonView } from "@/components/json-view"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; -import type { Trigger } from "@/contracts/types/automation.types"; +import { type Trigger, triggerUpdateRequest } from "@/contracts/types/automation.types"; import { describeCron } from "@/lib/automations/describe-cron"; import { formatRelativeDate, formatRelativeFutureDate } from "@/lib/format-date"; import { DeleteTriggerDialog } from "./delete-trigger-dialog"; @@ -18,20 +19,36 @@ interface TriggerCardProps { canDelete: boolean; } +interface TriggerDraft { + params: Record; + static_inputs: Record; +} + +function draftFromTrigger(trigger: Trigger): TriggerDraft { + return { + params: trigger.params, + static_inputs: trigger.static_inputs ?? {}, + }; +} + /** * One trigger row in the Triggers section of the detail page. Renders: * - type icon + human-readable schedule + timezone * - last_fired_at / next_fire_at hints * - static_inputs as formatted JSON (when present) - * - enable toggle + remove button (each gated independently) + * - enable toggle + remove button + inline edit (each gated independently) * - * Editing params (cron, timezone, static_inputs) lives behind the future - * raw-JSON path; this card stays read-only-except-for-toggle for v1. + * Inline edit covers ``params`` and ``static_inputs`` — the two fields the + * backend ``PATCH /triggers/[id]`` endpoint accepts beyond ``enabled``. + * ``enabled`` stays on the Switch so the two surfaces don't fight. */ export function TriggerCard({ trigger, automationId, canUpdate, canDelete }: TriggerCardProps) { const { mutateAsync: updateTrigger, isPending: updating } = useAtomValue(updateTriggerMutationAtom); const [deleteOpen, setDeleteOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [draft, setDraft] = useState(() => draftFromTrigger(trigger)); + const [issues, setIssues] = useState([]); const cron = typeof trigger.params.cron === "string" ? trigger.params.cron : undefined; const tz = typeof trigger.params.timezone === "string" ? trigger.params.timezone : "UTC"; @@ -47,6 +64,38 @@ export function TriggerCard({ trigger, automationId, canUpdate, canDelete }: Tri }); } + function startEdit() { + setDraft(draftFromTrigger(trigger)); + setIssues([]); + setIsEditing(true); + } + + function cancelEdit() { + setIsEditing(false); + setIssues([]); + } + + async function saveEdit() { + setIssues([]); + const result = triggerUpdateRequest.safeParse(draft); + if (!result.success) { + setIssues( + result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`) + ); + return; + } + try { + await updateTrigger({ + automationId, + triggerId: trigger.id, + patch: result.data, + }); + setIsEditing(false); + } catch (err) { + setIssues([(err as Error).message ?? "Update failed"]); + } + } + return ( <>
@@ -71,17 +120,29 @@ export function TriggerCard({ trigger, automationId, canUpdate, canDelete }: Tri
)} + {canUpdate && !isEditing && ( + + )} {canDelete && ( + + + + ) : ( + <> + {(trigger.last_fired_at || trigger.next_fire_at) && ( +
+ {trigger.next_fire_at && ( + + )} + {trigger.last_fired_at && ( + + )} +
+ )} + + {hasStaticInputs && ( +
+
Static inputs
+
+ +
+
+ )} + )}