diff --git a/web/components/workflows/EditStepModal.tsx b/web/components/workflows/EditStepModal.tsx new file mode 100644 index 0000000..5bdb047 --- /dev/null +++ b/web/components/workflows/EditStepModal.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { api, WorkflowStep, InputParam } from "@/lib/api"; +import { Button, Modal } from "@/components/ui"; + +const inputClass = + "w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder-text-secondary/50 focus:border-signal focus:outline-none focus:ring-1 focus:ring-signal"; + +export function EditStepModal({ open, step, onClose }: { open: boolean; step: WorkflowStep | null; onClose: () => void }) { + const qc = useQueryClient(); + const [name, setName] = useState(step?.name ?? ""); + const [interpreter, setInterpreter] = useState<"bash" | "powershell">(step?.interpreter ?? "bash"); + const [script, setScript] = useState(step?.script ?? ""); + const [outputs, setOutputs] = useState(step?.declared_outputs ?? []); + const [inputs, setInputs] = useState(step?.declared_inputs ?? []); + const [newOut, setNewOut] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + // NOTE: because state is seeded from props, render the modal conditionally + // (parent mounts it only when opening) OR key it by step_id so it re-seeds. + + const save = async () => { + setBusy(true); setError(null); + try { + const payload: Partial = { + name: name.trim(), description: step?.description ?? "", interpreter, script, + declared_outputs: outputs, declared_inputs: inputs.filter((i) => i.name.trim() !== ""), + secret_refs: step?.secret_refs ?? [], + }; + if (step) await api.updateStep(step.step_id, payload); + else await api.createStep(payload); + qc.invalidateQueries({ queryKey: ["steps"] }); + onClose(); + } catch (e) { setError((e as Error).message); } finally { setBusy(false); } + }; + + const del = async () => { + if (!step || !window.confirm("Delete this step? It will be removed from every workflow that uses it.")) return; + setBusy(true); setError(null); + try { + await api.deleteStep(step.step_id); + qc.invalidateQueries({ queryKey: ["steps"] }); + qc.invalidateQueries({ queryKey: ["workflow"] }); + onClose(); + } catch (e) { setError((e as Error).message); } finally { setBusy(false); } + }; + + return ( + +
+ {error &&
{error}
} +

Reusable steps are shared across all workflows. Editing here changes it everywhere.

+
+ + setName(e.target.value)} /> +
+
+ + +
+
+ +