From 9c164282a17a83778b406b7a8067baeffaf69abd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 08:49:09 +0000 Subject: [PATCH] fix(editor): auto-save before Run so in-memory edits actually execute The Run button in NodeEditorDialog and the Test-run button in the editor toolbar both POST /api/workflows/{id}/executions, which reads the graph from the DB. Any un-saved edit (Python script tweak, config change, a freshly-dragged node) was silently ignored. Both entry points now await the workflow save before starting the execution. New Vitest pins the save-then-post ordering in the dialog. --- frontend/src/editor/NodeEditorDialog.test.tsx | 24 +++++++++++++++++++ frontend/src/editor/NodeEditorDialog.tsx | 10 ++++++-- frontend/src/pages/EditorPage.tsx | 5 +++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frontend/src/editor/NodeEditorDialog.test.tsx b/frontend/src/editor/NodeEditorDialog.test.tsx index 04bfcb6..34a378f 100644 --- a/frontend/src/editor/NodeEditorDialog.test.tsx +++ b/frontend/src/editor/NodeEditorDialog.test.tsx @@ -49,6 +49,7 @@ function renderDialog(overrides: Partial onPatch: vi.fn(), onDelete: vi.fn(), onClose: vi.fn(), + onSave: vi.fn().mockResolvedValue(undefined), ...overrides, }; const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); @@ -76,6 +77,7 @@ function renderControlledDialog(overrides: Partial setN((prev) => ({ ...prev, data: { ...prev.data, ...patch } }))} onDelete={overrides.onDelete ?? (() => {})} onClose={overrides.onClose ?? (() => {})} + onSave={overrides.onSave ?? (() => Promise.resolve())} /> ); } @@ -134,6 +136,28 @@ describe("NodeEditorDialog", () => { expect(screen.getByRole("button", { name: /^run/i })).toBeDisabled(); }); + it("Run button awaits onSave before POSTing the execution", async () => { + const events: string[] = []; + const onSave = vi.fn().mockImplementation(async () => { + events.push("save"); + }); + server.use( + http.post("/api/workflows/:id/executions", () => { + events.push("post"); + return HttpResponse.json({ executionId: "e-new" }, { status: 202 }); + }), + http.get("/api/executions/e-new", () => + HttpResponse.json({ + id: "e-new", workflowId: "wf1", trigger: "manual", status: "completed", + startedAt: "", endedAt: "", error: null, nodes: [], + } satisfies ExecutionDetail)), + ); + renderDialog({ onSave }); + await userEvent.click(screen.getByRole("button", { name: /^run/i })); + await waitFor(() => expect(events).toEqual(["save", "post"])); + expect(onSave).toHaveBeenCalledTimes(1); + }); + it("Run button POSTs an execution and shows the fresh row on success", async () => { server.use( http.post("/api/workflows/:id/executions", () => diff --git a/frontend/src/editor/NodeEditorDialog.tsx b/frontend/src/editor/NodeEditorDialog.tsx index feaf092..62852db 100644 --- a/frontend/src/editor/NodeEditorDialog.tsx +++ b/frontend/src/editor/NodeEditorDialog.tsx @@ -22,11 +22,14 @@ interface Props { onPatch: (patch: Partial) => void; onDelete: () => void; onClose: () => void; + // Persist the current in-memory graph. The dialog awaits this before starting a Run + // so unsaved edits (Python script tweaks, config changes) actually take effect. + onSave: () => Promise; } export function NodeEditorDialog({ node, nodeTypes, connections, workflowId, workflowActive, - triggerNodeIds, onPatch, onDelete, onClose, + triggerNodeIds, onPatch, onDelete, onClose, onSave, }: Props) { const nt = nodeTypes.find((t) => t.id === node.data.typeId); const focusedFieldRef = useRef(null); @@ -91,9 +94,12 @@ export function NodeEditorDialog({ const invalidate = useInvalidate(); const qc = useQueryClient(); const runMutation = useApiMutation( - () => { + async () => { const startFrom = triggerNodeIds[0]; if (!startFrom) throw new Error("no trigger"); + // Persist in-memory graph edits first — the executor reads from the DB, + // so an un-saved script tweak would otherwise be ignored on Run. + await onSave(); return apiFetch<{ executionId: string }>( `/api/workflows/${workflowId}/executions`, { method: "POST", body: { startFromNodeId: startFrom, triggerData: {} } }, diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 62f1c0c..4337aa2 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -103,9 +103,11 @@ export default function EditorPage() { ); const testRun = useApiMutation( - () => { + async () => { const startFrom = selectedId || triggerNodeIds[0]; if (!startFrom) throw new Error("Add a trigger node or select a node to start from."); + // Persist in-memory graph edits first — the executor reads from the DB. + await save.mutateAsync(); return apiFetch<{ executionId: string }>( `/api/workflows/${id}/executions`, { method: "POST", body: { startFromNodeId: startFrom, triggerData: {} } }, @@ -223,6 +225,7 @@ export default function EditorPage() { onPatch={(patch) => patchNode(editingNode.id, patch)} onDelete={() => deleteNode(editingNode.id)} onClose={() => setEditingId(null)} + onSave={() => save.mutateAsync()} /> )}