diff --git a/internal/ai/workflow/validator/validator.go b/internal/ai/workflow/validator/validator.go index 7259bfc..f84b169 100644 --- a/internal/ai/workflow/validator/validator.go +++ b/internal/ai/workflow/validator/validator.go @@ -310,6 +310,9 @@ func (v *definitionValidator) validateConditions() { if branch.Condition != nil { v.addError(branchField+".condition", "default condition branch must not define a condition") } + if branchIndex != len(config.Branches)-1 { + v.addError(branchField, "default condition branch must be last") + } continue } v.validateCondition(branchField+".condition", strings.TrimSpace(node.ID), branch.Condition) diff --git a/internal/ai/workflow/validator/validator_test.go b/internal/ai/workflow/validator/validator_test.go index 3017e65..6abb0d8 100644 --- a/internal/ai/workflow/validator/validator_test.go +++ b/internal/ai/workflow/validator/validator_test.go @@ -317,6 +317,29 @@ func TestValidateDefinitionRejectsInvalidConditionEnumValue(t *testing.T) { } } +func TestValidateDefinitionRejectsConditionDefaultBranchBeforeLast(t *testing.T) { + def := conditionDefinition() + var config dsl.ConditionConfig + if err := json.Unmarshal(def.Nodes[1].Config, &config); err != nil { + t.Fatalf("unmarshal condition config: %v", err) + } + config.Branches[0], config.Branches[1] = config.Branches[1], config.Branches[0] + raw, err := json.Marshal(config) + if err != nil { + t.Fatalf("marshal condition config: %v", err) + } + def.Nodes[1].Config = raw + + result := validator.ValidateDefinition(def, registry.DefaultRegistry()) + + if result.Valid { + t.Fatalf("expected default branch before last to be invalid") + } + if !hasValidationMessage(result, "default condition branch must be last") { + t.Fatalf("expected default branch order error, got %#v", result.Errors) + } +} + func minimalDefinition() dsl.Definition { return dsl.Definition{ SchemaVersion: 1, diff --git a/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx b/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx index 2b22301..96c6f55 100644 --- a/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx +++ b/web/app/dashboard/ai-workflows/_components/node-config-panel.tsx @@ -34,24 +34,17 @@ export type WorkflowBranchSummary = { isDefault: boolean } -export type WorkflowBranchTargetOption = { - value: string - label: string -} - export function NodeConfigPanel({ node, nodeSpec, availableVariables, branchSummaries = [], - branchTargetOptions = [], onChange, }: { node: Node | null nodeSpec?: WorkflowNodeSpec availableVariables: WorkflowVariableRef[] branchSummaries?: WorkflowBranchSummary[] - branchTargetOptions?: WorkflowBranchTargetOption[] onChange: (nodeId: string, data: WorkflowNodeData) => void }) { if (!node) { @@ -69,7 +62,6 @@ export function NodeConfigPanel({ nodeSpec={nodeSpec} availableVariables={availableVariables} branchSummaries={branchSummaries} - branchTargetOptions={branchTargetOptions} onChange={onChange} /> ) @@ -80,14 +72,12 @@ function NodeConfigForm({ nodeSpec, availableVariables, branchSummaries, - branchTargetOptions, onChange, }: { node: Node nodeSpec?: WorkflowNodeSpec availableVariables: WorkflowVariableRef[] branchSummaries: WorkflowBranchSummary[] - branchTargetOptions: WorkflowBranchTargetOption[] onChange: (nodeId: string, data: WorkflowNodeData) => void }) { const [name, setName] = useState(node.data.name ?? "") @@ -145,7 +135,6 @@ function NodeConfigForm({ commitChange({ config: { ...(node.data.config ?? {}), branches } })} @@ -237,14 +226,12 @@ function NodeConfigForm({ function ConditionNodePanel({ branches, branchSummaries, - branchTargetOptions, availableVariables, outputSchema, onChange, }: { branches: WorkflowConditionBranch[] branchSummaries: WorkflowBranchSummary[] - branchTargetOptions: WorkflowBranchTargetOption[] availableVariables: WorkflowVariableRef[] outputSchema: WorkflowVariableSpec[] onChange: (branches: WorkflowConditionBranch[]) => void @@ -257,25 +244,49 @@ function ConditionNodePanel({ } const addBranch = () => { const index = branches.length + 1 + const nextBranch = { + id: `branch_${index}`, + name: `分支 ${index}`, + targetNodeId: "", + condition: { operator: "eq" }, + } + const defaultIndex = branches.findIndex((branch) => branch.default) + if (defaultIndex >= 0) { + onChange([ + ...branches.slice(0, defaultIndex), + nextBranch, + ...branches.slice(defaultIndex), + ]) + return + } onChange([ ...branches, + nextBranch, { - id: `branch_${index}`, - name: `分支 ${index}`, - targetNodeId: branchTargetOptions[0]?.value ?? "", - condition: { operator: "eq" }, + id: "default", + name: "其他情况", + targetNodeId: "", + default: true, }, ]) } const deleteBranch = (branchId: string) => { - onChange(branches.filter((branch) => branch.id !== branchId)) + onChange(branches.filter((branch) => branch.id !== branchId || branch.default)) } - const markDefault = (branchId: string) => { - onChange(branches.map((branch) => normalizeBranch({ - ...branch, - default: branch.id === branchId, - condition: branch.id === branchId ? undefined : branch.condition ?? { operator: "eq" }, - }))) + const moveBranch = (branchId: string, direction: -1 | 1) => { + const index = branches.findIndex((branch) => branch.id === branchId) + if (index < 0 || branches[index]?.default) { + return + } + const nextIndex = index + direction + if (nextIndex < 0 || nextIndex >= branches.length || branches[nextIndex]?.default) { + return + } + const next = [...branches] + const current = next[index] + next[index] = next[nextIndex] + next[nextIndex] = current + onChange(next) } return ( @@ -317,14 +328,11 @@ function ConditionNodePanel({
- commitBranch(branch.id, { targetNodeId: value })} - /> +
+ {summary?.targetNodeId + ? `已连接到:${summary.targetName}` + : "请从画布中该分支右侧连接点拖线到目标节点"} +
{branch.default ? (
@@ -367,14 +375,21 @@ function ConditionNodePanel({
)}
- {!branch.default ? ( - + ) : null} + {!branch.default && index < branches.findIndex((item) => item.default) - 1 ? ( + + ) : null} + {!branch.default ? ( + ) : null} -
{summary?.conditionLabel ?? "尚未完成分支配置"} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx index 3eef9d9..3f37c83 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx @@ -52,12 +52,15 @@ import { import { cn } from "@/lib/utils" import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" import { + applyConditionBranchConnection, applyAutoInputMappings, calculateWorkflowHelperLines, + clearConditionBranchConnection, createWorkflowHistory, createWorkflowNodeFromSpec, fromApiDefinition, getAvailableVariables, + getConditionBranchHandleId, getNodeSpec, getRequiredInputs, pushWorkflowHistory, @@ -66,6 +69,7 @@ import { undoWorkflowHistory, validateWorkflowDraft, type WorkflowCondition, + type WorkflowDraft, type WorkflowEditorNode, type WorkflowHistory, type WorkflowHelperLine, @@ -73,7 +77,6 @@ import { type WorkflowVariableSpec, } from "./workflow-utils" import { NodeConfigPanel, type WorkflowBranchSummary } from "./node-config-panel" -import type { WorkflowBranchTargetOption } from "./node-config-panel" type WorkflowNodeData = Record & { nodeType?: string @@ -88,6 +91,7 @@ type WorkflowNodeData = Record & { inputCount?: number outputCount?: number missingInputs?: string[] + branchSummaries?: WorkflowBranchSummary[] } type WorkflowFlowNode = Node @@ -157,10 +161,25 @@ function toFlowEdges(definition: AIWorkflowDefinition): WorkflowFlowEdge[] { type: "workflowEdge", source: edge.source, target: edge.target, + sourceHandle: getConditionBranchHandleForEdge(definition, edge.source, edge.target), })) } -function toDraft(nodes: WorkflowFlowNode[], edges: WorkflowFlowEdge[]) { +function getConditionBranchHandleForEdge( + definition: AIWorkflowDefinition, + sourceNodeId: string, + targetNodeId: string +) { + const sourceNode = definition.nodes?.find((node) => node.id === sourceNodeId) + if (!sourceNode || sourceNode.type !== "condition") { + return undefined + } + const config = sourceNode.config as WorkflowNodeConfig | undefined + const branch = config?.branches?.find((item) => item.targetNodeId === targetNodeId) + return branch ? getConditionBranchHandleId(branch.id) : undefined +} + +function toDraft(nodes: WorkflowFlowNode[], edges: WorkflowFlowEdge[]): WorkflowDraft { return { nodes: nodes.map((node) => ({ id: node.id, @@ -177,6 +196,8 @@ function toDraft(nodes: WorkflowFlowNode[], edges: WorkflowFlowEdge[]) { id: edge.id, source: edge.source, target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, })), } } @@ -254,10 +275,6 @@ export function WorkflowEditor({ () => (propertyPanelNode ? getBranchSummaries(nodes, propertyPanelNode.id, nodeSpecs) : []), [nodeSpecs, nodes, propertyPanelNode] ) - const propertyPanelBranchTargetOptions = useMemo( - () => (propertyPanelNode ? getBranchTargetOptions(nodes, edges, propertyPanelNode.id) : []), - [edges, nodes, propertyPanelNode] - ) useEffect(() => { onDefinitionChange(toApiDefinition(draft) as AIWorkflowDefinition) }, [draft, onDefinitionChange]) @@ -415,18 +432,28 @@ export function WorkflowEditor({ pushCurrentSnapshotToHistory() const edge = { ...connection, - id: uniqueEdgeId(edges, connection.source, connection.target), + id: uniqueEdgeId(edges, connection.source, connection.target, connection.sourceHandle), type: "workflowEdge", } as WorkflowFlowEdge - setEdges((current) => addEdge(edge, current)) + const nextEdges = [ + ...edges.filter((item) => !( + connection.sourceHandle + && item.source === connection.source + && item.sourceHandle === connection.sourceHandle + )), + edge, + ] + setEdges((current) => addEdge(edge, current.filter((item) => !( + connection.sourceHandle + && item.source === connection.source + && item.sourceHandle === connection.sourceHandle + )))) setNodes((currentNodes) => { - const currentDraft = toDraft(currentNodes, [...edges, edge]) - const nextDraft = applyAutoInputMappings( - currentDraft, - connection.source!, - connection.target!, - nodeSpecs + const connectedDraft = applyConditionBranchConnection( + toDraft(currentNodes, nextEdges), + edge ) + const nextDraft = applyAutoInputMappings(connectedDraft, connection.source!, connection.target!, nodeSpecs) return currentNodes.map((node) => { const nextNode = nextDraft.nodes.find((item) => item.id === node.id) if (!nextNode) { @@ -436,6 +463,7 @@ export function WorkflowEditor({ ...node, data: { ...node.data, + config: nextNode.data?.config ?? node.data.config, inputs: nextNode.data?.inputs ?? node.data.inputs, }, } @@ -498,12 +526,41 @@ export function WorkflowEditor({ const onWorkflowEdgesChange = useCallback( (changes: EdgeChange[]) => { - if (changes.some((change) => change.type === "remove")) { + const removedEdges = changes + .filter((change) => change.type === "remove") + .map((change) => edges.find((edge) => edge.id === change.id)) + .filter((edge): edge is WorkflowFlowEdge => Boolean(edge)) + if (removedEdges.length > 0) { pushCurrentSnapshotToHistory() + setNodes((currentNodes) => { + let draft = toDraft(currentNodes, edges.filter((edge) => !removedEdges.some((removed) => removed.id === edge.id))) + for (const removedEdge of removedEdges) { + draft = clearConditionBranchConnection(draft, { + id: removedEdge.id, + source: removedEdge.source, + target: removedEdge.target, + sourceHandle: removedEdge.sourceHandle, + targetHandle: removedEdge.targetHandle, + }) + } + return currentNodes.map((node) => { + const nextNode = draft.nodes.find((item) => item.id === node.id) + if (!nextNode) { + return node + } + return { + ...node, + data: { + ...node.data, + config: nextNode.data?.config ?? node.data.config, + }, + } + }) + }) } onEdgesChange(changes) }, - [onEdgesChange, pushCurrentSnapshotToHistory] + [edges, onEdgesChange, pushCurrentSnapshotToHistory, setNodes] ) const onNodeDragStart = useCallback>(() => { @@ -938,7 +995,6 @@ export function WorkflowEditor({ nodeSpec={propertyPanelNodeSpec} availableVariables={propertyPanelAvailableVariables} branchSummaries={propertyPanelBranchSummaries} - branchTargetOptions={propertyPanelBranchTargetOptions} onChange={updateNodeData} /> ) : null} @@ -972,12 +1028,13 @@ export function WorkflowEditor({ ) } -function uniqueEdgeId(edges: WorkflowFlowEdge[], source: string, target: string) { +function uniqueEdgeId(edges: WorkflowFlowEdge[], source: string, target: string, sourceHandle?: string | null) { let nextIndex = edges.length + 1 - let id = `edge_${source}_${target}_${nextIndex}` + const handleSuffix = sourceHandle ? `_${sourceHandle.replace(/[^a-zA-Z0-9_-]/g, "_")}` : "" + let id = `edge_${source}${handleSuffix}_${target}_${nextIndex}` while (edges.some((edge) => edge.id === id)) { nextIndex += 1 - id = `edge_${source}_${target}_${nextIndex}` + id = `edge_${source}${handleSuffix}_${target}_${nextIndex}` } return id } @@ -1001,6 +1058,7 @@ function enrichNodesForRender( inputCount: spec?.inputSchema?.length ?? 0, outputCount: spec?.outputSchema?.length ?? 0, missingInputs: missingInputs.map((input) => input.name), + branchSummaries: node.data.nodeType === "condition" ? getBranchSummaries(nodes, node.id, nodeSpecs) : undefined, }, } }) @@ -1042,22 +1100,6 @@ function getBranchSummaries( }) } -function getBranchTargetOptions( - nodes: WorkflowFlowNode[], - edges: WorkflowFlowEdge[], - nodeId: string -): WorkflowBranchTargetOption[] { - return edges - .filter((edge) => edge.source === nodeId) - .map((edge) => { - const target = nodes.find((node) => node.id === edge.target) - return { - value: edge.target, - label: target?.data.name ?? target?.data.title ?? edge.target, - } - }) -} - function formatConditionLabel( condition: WorkflowCondition, nodes: WorkflowFlowNode[], @@ -1226,16 +1268,19 @@ function WorkflowCanvasEdge({ } function WorkflowNodeHandle({ + id, type, position, className, }: { + id?: string type: "source" | "target" position: Position className?: string }) { return ( ) showHandles ? "pointer-events-auto opacity-100" : "pointer-events-none" ) if (isConditionNode) { + const branches = data.branchSummaries ?? [] return (
setHovered(true)} onMouseLeave={() => setHovered(false)} > -
-
- {hasIssue ? ( - - ) : ( - - )} -
{data.name ?? data.title}
-
分支
+
+
+
+ {hasIssue ? ( + + ) : ( + + )} +
+
+
{data.name ?? data.title}
+
条件分支
+
+
+
+ {branches.length > 0 ? branches.map((branch, index) => ( +
+ + {branch.isDefault ? "ELSE" : index === 0 ? "IF" : "ELIF"} + +
+
{branch.conditionLabel}
+
+ {branch.targetNodeId ? `连接到:${branch.targetName}` : "未连接目标节点"} +
+
+ +
+ )) : ( +
当前还没有分支
+ )} +
- -
) } diff --git a/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs b/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs index 3a8d930..67c0487 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs +++ b/web/app/dashboard/ai-workflows/_components/workflow-utils.test.mjs @@ -80,6 +80,60 @@ describe("validateWorkflowDraft", () => { assert.equal(result.valid, false) assert.match(result.errors.join("\n"), /缺少必填输入「replyText」/) }) + + it("rejects condition branch target without matching branch handle edge", async () => { + const { getConditionBranchHandleId, validateWorkflowDraft } = await loadModule() + + const result = validateWorkflowDraft({ + nodes: [ + { id: "start_1", type: "workflowNode", position: { x: 0, y: 0 }, data: { nodeType: "start" } }, + { + id: "condition_1", + type: "workflowNode", + position: { x: 200, y: 0 }, + data: { + nodeType: "condition", + name: "Route", + config: { + branches: [ + { + id: "direct", + name: "Direct", + targetNodeId: "send_1", + condition: { + left: { nodeId: "start_1", field: "userMessage" }, + operator: "eq", + right: "hello", + }, + }, + { + id: "default", + name: "Else", + targetNodeId: "send_1", + default: true, + }, + ], + }, + }, + }, + { id: "send_1", type: "workflowNode", position: { x: 400, y: 0 }, data: { nodeType: "send_reply" } }, + { id: "end_1", type: "workflowNode", position: { x: 600, y: 0 }, data: { nodeType: "end" } }, + ], + edges: [ + { id: "e1", source: "start_1", target: "condition_1" }, + { + id: "e2", + source: "condition_1", + target: "send_1", + sourceHandle: getConditionBranchHandleId("default"), + }, + { id: "e3", source: "send_1", target: "end_1" }, + ], + }) + + assert.equal(result.valid, false) + assert.match(result.errors.join("\n"), /对应分支连接点/) + }) }) describe("applyAutoInputMappings", () => { @@ -363,6 +417,76 @@ describe("getAvailableVariables", () => { }) describe("toApiDefinition", () => { + it("updates condition branch target from branch handle connection", async () => { + const { applyConditionBranchConnection, getConditionBranchHandleId } = await loadModule() + + const draft = applyConditionBranchConnection( + { + nodes: [ + { + id: "condition_1", + type: "workflowNode", + position: { x: 0, y: 0 }, + data: { + nodeType: "condition", + config: { + branches: [ + { id: "direct", name: "Direct", targetNodeId: "", condition: { operator: "eq" } }, + { id: "default", name: "Else", targetNodeId: "", default: true }, + ], + }, + }, + }, + { id: "send_1", type: "workflowNode", position: { x: 200, y: 0 }, data: { nodeType: "send_reply" } }, + ], + edges: [], + }, + { + source: "condition_1", + target: "send_1", + sourceHandle: getConditionBranchHandleId("direct"), + } + ) + + assert.equal(draft.nodes[0].data.config.branches[0].targetNodeId, "send_1") + assert.equal(draft.nodes[0].data.config.branches[1].targetNodeId, "") + }) + + it("clears condition branch target when the branch edge is removed", async () => { + const { clearConditionBranchConnection, getConditionBranchHandleId } = await loadModule() + + const draft = clearConditionBranchConnection( + { + nodes: [ + { + id: "condition_1", + type: "workflowNode", + position: { x: 0, y: 0 }, + data: { + nodeType: "condition", + config: { + branches: [ + { id: "direct", name: "Direct", targetNodeId: "send_1", condition: { operator: "eq" } }, + { id: "default", name: "Else", targetNodeId: "fallback_1", default: true }, + ], + }, + }, + }, + ], + edges: [], + }, + { + id: "edge_condition_send", + source: "condition_1", + target: "send_1", + sourceHandle: getConditionBranchHandleId("direct"), + } + ) + + assert.equal(draft.nodes[0].data.config.branches[0].targetNodeId, "") + assert.equal(draft.nodes[0].data.config.branches[1].targetNodeId, "fallback_1") + }) + it("keeps condition branches on the condition node config and exports plain edges", async () => { const { toApiDefinition } = await loadModule() diff --git a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts index 92763b2..0e39799 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-utils.ts +++ b/web/app/dashboard/ai-workflows/_components/workflow-utils.ts @@ -48,6 +48,8 @@ export type WorkflowEditorEdge = { id: string source: string target: string + sourceHandle?: string | null + targetHandle?: string | null } export type WorkflowCondition = { @@ -162,6 +164,19 @@ export type WorkflowHistoryChange = { const helperLineAlignmentThreshold = 6 const defaultWorkflowHistoryLimit = 50 +const conditionBranchHandlePrefix = "condition-branch:" + +export function getConditionBranchHandleId(branchId: string): string { + return `${conditionBranchHandlePrefix}${branchId}` +} + +export function parseConditionBranchHandleId(handleId?: string | null): string | null { + if (!handleId?.startsWith(conditionBranchHandlePrefix)) { + return null + } + const branchId = handleId.slice(conditionBranchHandlePrefix.length) + return branchId || null +} function cloneHistorySnapshot(snapshot: T): T { return JSON.parse(JSON.stringify(snapshot)) as T @@ -373,6 +388,7 @@ export function validateWorkflowDraft( const edgeIds = new Set() const outgoingTargets = new Map>() + const branchEdges = new Set() for (const edge of draft.edges) { const id = edge.id.trim() if (!id) { @@ -391,19 +407,22 @@ export function validateWorkflowDraft( outgoingTargets.set(edge.source, new Set()) } outgoingTargets.get(edge.source)?.add(edge.target) + const branchId = parseConditionBranchHandleId(edge.sourceHandle) + if (branchId) { + branchEdges.add(`${edge.source}:${branchId}:${edge.target}`) + } } for (const node of draft.nodes) { const nodeType = node.data?.nodeType ?? node.type ?? "" const spec = getNodeSpec(nodeSpecs, nodeType) - if (!spec) { - continue - } - for (const input of getRequiredInputs(spec)) { - const selector = node.data?.inputs?.[input.name] - if (!selector?.nodeId || !selector.field) { - const nodeName = node.data?.name ?? spec.title ?? node.id - errors.push(`${nodeName} 缺少必填输入「${input.name}」,请选择上游节点的输出变量。`) + if (spec) { + for (const input of getRequiredInputs(spec)) { + const selector = node.data?.inputs?.[input.name] + if (!selector?.nodeId || !selector.field) { + const nodeName = node.data?.name ?? spec.title ?? node.id + errors.push(`${nodeName} 缺少必填输入「${input.name}」,请选择上游节点的输出变量。`) + } } } if (nodeType === "condition") { @@ -429,12 +448,17 @@ export function validateWorkflowDraft( errors.push(`${node.data?.name ?? node.id} 的分支「${branchName}」目标节点不存在。`) } else if (!targets.has(branch.targetNodeId)) { errors.push(`${node.data?.name ?? node.id} 的分支「${branchName}」需要连接到目标节点。`) + } else if (branchEdges.size > 0 && !branchEdges.has(`${node.id}:${branch.id}:${branch.targetNodeId}`)) { + errors.push(`${node.data?.name ?? node.id} 的分支「${branchName}」需要从对应分支连接点连到目标节点。`) } if (branch.default) { defaultCount += 1 if (branch.condition) { errors.push(`${node.data?.name ?? node.id} 的默认分支不能配置条件。`) } + if (branches.indexOf(branch) !== branches.length - 1) { + errors.push(`${node.data?.name ?? node.id} 的默认分支必须放在最后。`) + } continue } if (!branch.condition?.left?.nodeId || !branch.condition.left.field) { @@ -480,6 +504,69 @@ export function toApiDefinition(draft: WorkflowDraft): WorkflowDefinition { } } +export function applyConditionBranchConnection( + draft: WorkflowDraft, + connection: Pick +): WorkflowDraft { + const branchId = parseConditionBranchHandleId(connection.sourceHandle) + if (!branchId || !connection.source || !connection.target) { + return draft + } + return updateConditionBranchTarget(draft, connection.source, branchId, connection.target) +} + +export function clearConditionBranchConnection( + draft: WorkflowDraft, + edge: WorkflowEditorEdge +): WorkflowDraft { + const branchId = parseConditionBranchHandleId(edge.sourceHandle) + if (branchId) { + return updateConditionBranchTarget(draft, edge.source, branchId, "") + } + const sourceNode = draft.nodes.find((node) => node.id === edge.source) + if (!sourceNode || (sourceNode.data?.nodeType ?? sourceNode.type) !== "condition") { + return draft + } + const branch = sourceNode.data?.config?.branches?.find((item) => item.targetNodeId === edge.target) + return branch ? updateConditionBranchTarget(draft, edge.source, branch.id, "") : draft +} + +function updateConditionBranchTarget( + draft: WorkflowDraft, + conditionNodeId: string, + branchId: string, + targetNodeId: string +): WorkflowDraft { + let changed = false + const nodes = draft.nodes.map((node) => { + if (node.id !== conditionNodeId || (node.data?.nodeType ?? node.type) !== "condition") { + return node + } + const branches = node.data?.config?.branches ?? [] + const nextBranches = branches.map((branch) => { + if (branch.id !== branchId || branch.targetNodeId === targetNodeId) { + return branch + } + changed = true + return { ...branch, targetNodeId } + }) + if (!changed) { + return node + } + return { + ...node, + data: { + ...node.data, + config: { + ...(node.data?.config ?? {}), + branches: nextBranches, + }, + }, + } + }) + return changed ? { ...draft, nodes } : draft +} + export function fromApiDefinition(definition: WorkflowDefinition): WorkflowDraft { return { nodes: (definition.nodes ?? []).map((node) => ({