feat(frontend): editor toolbar — Save / Test run / Activate / Deactivate
This commit is contained in:
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user