From fbda26a1886631c014774461e0d8f7ab32f689f1 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 10:34:51 +0100 Subject: [PATCH] feat(web): ad-hoc inline steps + import-to-inline in workflow editor --- web/app/workflows/[id]/page.tsx | 161 +++++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 21 deletions(-) diff --git a/web/app/workflows/[id]/page.tsx b/web/app/workflows/[id]/page.tsx index bd6e2b7..dc0992e 100644 --- a/web/app/workflows/[id]/page.tsx +++ b/web/app/workflows/[id]/page.tsx @@ -35,6 +35,14 @@ function DefaultBadge() { ); } +function AdhocBadge() { + return ( + + ad-hoc + + ); +} + export default function WorkflowBuilder() { const params = useParams<{ id: string }>(); const id = params.id; @@ -49,8 +57,10 @@ export default function WorkflowBuilder() { const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [importing, setImporting] = useState(false); + const [importingInline, setImportingInline] = useState(false); const [syncing, setSyncing] = useState(false); const fileInputRef = useRef(null); + const inlineFileInputRef = useRef(null); const [groupKeys, setGroupKeys] = useState>({}); const [editWorkflowOpen, setEditWorkflowOpen] = useState(false); const [editingStep, setEditingStep] = useState(null); @@ -140,6 +150,45 @@ export default function WorkflowBuilder() { setWf({ ...wf, steps: resequence(next) }); }; + const appendRef = (ref: WorkflowStepRef) => { + setWf({ ...wf, steps: resequence([...sortedSteps, ref]) }); + }; + + const addAdhocStep = () => { + appendRef({ + inline: { + step_id: "", + name: "New ad-hoc step", + description: "", + interpreter: "bash", + script: "", + declared_outputs: [], + declared_inputs: [], + secret_refs: [], + }, + order: wf.steps.length, + on_failure: "stop", + max_retries: 0, + }); + }; + + const onImportInline = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setImportingInline(true); + setError(null); + try { + const doc = JSON.parse(await file.text()); + const step = await api.parseStep(doc); + appendRef({ inline: step, order: wf.steps.length, on_failure: "stop", max_retries: 0 }); + } catch (err) { + setError((err as Error).message); + } finally { + setImportingInline(false); + e.target.value = ""; + } + }; + const moveStep = (from: number, pos: number) => { const next = [...sortedSteps]; const [item] = next.splice(from, 1); @@ -177,6 +226,12 @@ export default function WorkflowBuilder() { steps: wf.steps.map((r, i) => (i === idx ? { ...r, ...patch } : r)), }); + const updateInline = (idx: number, patch: Partial) => + setWf({ + ...wf, + steps: wf.steps.map((r, i) => (i === idx && r.inline ? { ...r, inline: { ...r.inline, ...patch } } : r)), + }); + const removeStep = (idx: number) => { const remaining = resequence(wf.steps.filter((_, i) => i !== idx)); setWf({ ...wf, steps: remaining }); @@ -320,6 +375,26 @@ export default function WorkflowBuilder() { Sync defaults +
+ + + +
{ const lib = libById(ref.step_id); const outs = upstreamOutputsFor(i); - const script = ref.overrides?.script ?? lib?.script ?? ""; + const script = ref.inline?.script ?? ref.overrides?.script ?? lib?.script ?? ""; const wfIdx = wf.steps.indexOf(ref); const isSelected = selected === i; return ( @@ -405,8 +480,10 @@ export default function WorkflowBuilder() { {i + 1} - {lib?.name ?? ref.step_id} - {lib && } + {ref.inline?.name ?? lib?.name ?? ref.step_id} + {ref.inline && } + {lib && !ref.inline && } + {ref.inline && }
                                             {script.slice(0, 200)}
@@ -439,27 +516,69 @@ export default function WorkflowBuilder() {
                                     Step {selected + 1} · Inspector
                                 
                                 
- {selectedLib && } -

{selectedLib?.name ?? selectedRef.step_id}

+ {selectedRef.inline && } + {selectedLib && !selectedRef.inline && } + {selectedRef.inline && } +

{selectedRef.inline?.name ?? selectedLib?.name ?? selectedRef.step_id}

-
- -