diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.css b/packages/dashboard/app/components/WorkflowNodeEditor.css index 3909061b42..fbd3c130b1 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.css +++ b/packages/dashboard/app/components/WorkflowNodeEditor.css @@ -159,9 +159,26 @@ .wf-editor-actions { display: flex; + align-items: center; gap: var(--space-xs); } +.wf-editor-readonly-note { + font-size: 0.75rem; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* Neutralize the fieldset wrapper so it only gates interactivity, not layout. */ +.wf-inspector-fields { + display: contents; + border: none; + margin: 0; + padding: 0; + min-inline-size: 0; +} + .wf-editor-save { background: var(--todo); border-color: var(--todo); diff --git a/packages/dashboard/app/components/WorkflowNodeEditor.tsx b/packages/dashboard/app/components/WorkflowNodeEditor.tsx index 2f8129f556..4e4b13ba4c 100644 --- a/packages/dashboard/app/components/WorkflowNodeEditor.tsx +++ b/packages/dashboard/app/components/WorkflowNodeEditor.tsx @@ -16,7 +16,7 @@ import { } from "@xyflow/react"; import { X, Plus, Trash2, Save, MessageSquare, Terminal, Shield, GitMerge, Loader2, HelpCircle } from "lucide-react"; import type { WorkflowDefinition } from "@fusion/core"; -import { getErrorMessage } from "@fusion/core"; +import { getErrorMessage, isBuiltinWorkflowId } from "@fusion/core"; import { fetchWorkflows, createWorkflow, @@ -87,6 +87,7 @@ function InnerEditor({ const [selectedNodeId, setSelectedNodeId] = useState(null); const activeWorkflow = useMemo(() => workflows.find((w) => w.id === activeId), [workflows, activeId]); + const isBuiltin = !!activeWorkflow && isBuiltinWorkflowId(activeWorkflow.id); const loadWorkflows = useCallback(async () => { setLoading(true); @@ -187,6 +188,7 @@ function InnerEditor({ const handleDeleteWorkflow = useCallback(async () => { if (!activeWorkflow) return; + if (isBuiltinWorkflowId(activeWorkflow.id)) return; // built-ins are read-only if (!window.confirm(`Delete workflow "${activeWorkflow.name}"?`)) return; try { await deleteWorkflow(activeWorkflow.id, projectId); @@ -198,8 +200,29 @@ function InnerEditor({ } }, [activeWorkflow, projectId, addToast]); + const handleDuplicate = useCallback(async () => { + if (!activeWorkflow) return; + try { + const created = await createWorkflow( + { + name: `${activeWorkflow.name} (copy)`, + description: activeWorkflow.description, + ir: activeWorkflow.ir, + layout: activeWorkflow.layout, + }, + projectId, + ); + setWorkflows((ws) => [...ws, created]); + setActiveId(created.id); + addToast(`Duplicated to "${created.name}" — editable`, "success"); + } catch (err) { + addToast(getErrorMessage(err) || "Failed to duplicate workflow", "error"); + } + }, [activeWorkflow, projectId, addToast]); + const handleSave = useCallback(async () => { if (!activeWorkflow) return; + if (isBuiltinWorkflowId(activeWorkflow.id)) return; // built-ins are read-only setSaving(true); setValidationError(null); try { @@ -294,18 +317,37 @@ function InnerEditor({
{PALETTE.map(({ kind, label, icon: Icon, presetConfig }) => ( - ))}
- - + {isBuiltin ? ( + <> + + Read-only built-in + + + + ) : ( + <> + + + + )}
@@ -343,6 +385,12 @@ function InnerEditor({ {selectedNode && selectedNode.data.kind !== "start" && selectedNode.data.kind !== "end" && ( )}