fix(workflows): disable start/end palette buttons when node already exists

Prevents users from adding duplicate start or end nodes, which made the
workflow invalid with no way to recover since those node kinds are
protected from deletion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-05 21:37:01 +02:00
co-authored by Copilot
parent 55df0c3b61
commit e47835c1e8
2 changed files with 18 additions and 3 deletions
+8 -1
View File
@@ -182,6 +182,13 @@ export function WorkflowEditor({
const issues = validateWorkflowDefinition(activeWorkflow);
const disabledPaletteKinds = useMemo(() => {
const disabled = new Set<WorkflowNodeKind>();
if (activeWorkflow.graph.nodes.some((n) => n.kind === 'start')) disabled.add('start');
if (activeWorkflow.graph.nodes.some((n) => n.kind === 'end')) disabled.add('end');
return disabled;
}, [activeWorkflow.graph.nodes]);
/* ── Change propagation ─────────────────────────────────── */
function propagateActiveChange(updatedActive: WorkflowDefinition) {
@@ -482,7 +489,7 @@ export function WorkflowEditor({
<div className="flex min-h-0 flex-1">
{/* Left palette */}
<div className="w-40 shrink-0 overflow-y-auto border-r border-[var(--color-border)] bg-[var(--color-surface-1)]">
<WorkflowNodePalette onAddNode={handleAddNode} />
<WorkflowNodePalette disabledKinds={disabledPaletteKinds} onAddNode={handleAddNode} />
</div>
{/* Center column: validation + canvas + settings */}
@@ -4,6 +4,7 @@ import type { WorkflowNodeKind } from '@shared/domain/workflow';
interface WorkflowNodePaletteProps {
onAddNode: (kind: WorkflowNodeKind) => void;
disabledKinds?: ReadonlySet<WorkflowNodeKind>;
}
interface PaletteItem {
@@ -48,7 +49,7 @@ const paletteGroups: PaletteGroup[] = [
},
];
export function WorkflowNodePalette({ onAddNode }: WorkflowNodePaletteProps) {
export function WorkflowNodePalette({ onAddNode, disabledKinds }: WorkflowNodePaletteProps) {
return (
<div className="space-y-4 p-3">
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
@@ -62,11 +63,18 @@ export function WorkflowNodePalette({ onAddNode }: WorkflowNodePaletteProps) {
<div className="space-y-0.5">
{group.items.map((item) => {
const Icon = item.icon;
const disabled = disabledKinds?.has(item.kind) ?? false;
return (
<button
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] text-[var(--color-text-secondary)] transition-all duration-200 hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]"
className={`flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-[12px] transition-all duration-200 ${
disabled
? 'cursor-not-allowed text-[var(--color-text-muted)] opacity-40'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-3)] hover:text-[var(--color-text-primary)]'
}`}
disabled={disabled}
key={item.kind}
onClick={() => onAddNode(item.kind)}
title={disabled ? `${item.label} node already exists` : undefined}
type="button"
>
<Icon className={`size-3.5 ${item.color}`} />