feat(frontend): editor toolbar — Save / Test run / Activate / Deactivate

This commit is contained in:
Claude
2026-08-28 16:27:15 +00:00
parent 12c5fb1990
commit 655bd17f55

View File

@@ -6,6 +6,10 @@ import { Canvas } from "@/editor/Canvas";
import { NodePalette } from "@/editor/NodePalette";
import { NodeConfigPanel } from "@/editor/NodeConfigPanel";
import { fromGraph, type VcNodeData } from "@/editor/graphAdapter";
import { apiFetch } from "@/api/client";
import { qk, useApiMutation, useInvalidate } from "@/api/hooks";
import { toGraph } from "@/editor/graphAdapter";
import { Play, Pause, Save, PlayCircle } from "lucide-react";
function newNodeId() {
return `n_${Math.random().toString(36).slice(2, 10)}`;
@@ -60,6 +64,37 @@ export default function EditorPage() {
}));
}
const invalidate = useInvalidate();
const save = useApiMutation(
() =>
apiFetch<void>(`/api/workflows/${id}`, {
method: "PUT",
body: { name: wf.data!.name, graph: toGraph(nodes, edges) },
}),
{ onSuccess: () => id && invalidate(qk.workflow(id)) },
);
const activate = useApiMutation(
() => apiFetch<void>(`/api/workflows/${id}/activate`, { method: "POST" }),
{ onSuccess: () => id && invalidate(qk.workflow(id)) },
);
const deactivate = useApiMutation(
() => apiFetch<void>(`/api/workflows/${id}/deactivate`, { method: "POST" }),
{ onSuccess: () => id && invalidate(qk.workflow(id)) },
);
const testRun = useApiMutation(
() => {
const triggers = nodes.filter((n) =>
nts.data?.find((t) => t.id === n.data.typeId)?.category === "Trigger");
const startFrom = (selected?.id) || triggers[0]?.id;
if (!startFrom) throw new Error("Add a trigger node or select a node to start from.");
return apiFetch<{ executionId: string }>(
`/api/workflows/${id}/executions`,
{ method: "POST", body: { startFromNodeId: startFrom, triggerData: {} } },
);
},
);
if (wf.isLoading || !wf.data) {
return <div className="p-6 text-slate-400 text-sm">Loading…</div>;
}
@@ -69,7 +104,39 @@ export default function EditorPage() {
<div className="border-b border-slate-800 px-4 py-2 flex items-center gap-3">
<span className="font-semibold">{wf.data.name}</span>
<span className="text-xs text-slate-500">Editor</span>
{/* Save / Test-run / Activate toolbar lands Task 14. */}
<div className="ml-auto flex items-center gap-2">
<button
onClick={() => save.mutate()}
disabled={save.isPending}
className="px-3 py-1.5 rounded bg-brand-600 hover:bg-brand-700 text-sm flex items-center gap-1.5"
>
<Save size={14} /> Save
</button>
<button
onClick={() => testRun.mutate()}
disabled={testRun.isPending}
className="px-3 py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-sm flex items-center gap-1.5"
>
<PlayCircle size={14} /> Test run
</button>
{wf.data.active ? (
<button
onClick={() => deactivate.mutate()}
disabled={deactivate.isPending}
className="px-3 py-1.5 rounded bg-amber-700/70 hover:bg-amber-700 text-sm flex items-center gap-1.5"
>
<Pause size={14} /> Deactivate
</button>
) : (
<button
onClick={() => activate.mutate()}
disabled={activate.isPending}
className="px-3 py-1.5 rounded bg-emerald-700 hover:bg-emerald-600 text-sm flex items-center gap-1.5"
>
<Play size={14} /> Activate
</button>
)}
</div>
</div>
<div className="flex-1 flex">
<NodePalette />