From a205ee16b9a7b1eae59e788a2b6943d4d20e8bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 19 Jun 2025 10:05:33 +0800 Subject: [PATCH] feat: improve the orgnize node operation (#21183) --- .../utils/{layout.ts => dagre-layout.ts} | 72 ++++++++++++++++++- web/app/components/workflow/utils/index.ts | 2 +- 2 files changed, 71 insertions(+), 3 deletions(-) rename web/app/components/workflow/utils/{layout.ts => dagre-layout.ts} (62%) diff --git a/web/app/components/workflow/utils/layout.ts b/web/app/components/workflow/utils/dagre-layout.ts similarity index 62% rename from web/app/components/workflow/utils/layout.ts rename to web/app/components/workflow/utils/dagre-layout.ts index 3c4189b5bc..5eafe77586 100644 --- a/web/app/components/workflow/utils/layout.ts +++ b/web/app/components/workflow/utils/dagre-layout.ts @@ -19,19 +19,87 @@ import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => { - const dagreGraph = new dagre.graphlib.Graph() + const dagreGraph = new dagre.graphlib.Graph({ compound: true }) dagreGraph.setDefaultEdgeLabel(() => ({})) + const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop)) + +// The default dagre layout algorithm often fails to correctly order the branches +// of an If/Else node, leading to crossed edges. +// +// To solve this, we employ a "virtual container" strategy: +// 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches. +// 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container. +// 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE). +// +// This forces dagre to treat the ordered branches as an unbreakable, atomic group, +// ensuring their layout respects the intended logical sequence. + const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse) + let virtualLogicApplied = false + + ifElseNodes.forEach((ifElseNode) => { + const childEdges = edges.filter(e => e.source === ifElseNode.id) + if (childEdges.length <= 1) + return + + virtualLogicApplied = true + const sortedChildEdges = childEdges.sort((edgeA, edgeB) => { + const handleA = edgeA.sourceHandle + const handleB = edgeB.sourceHandle + + if (handleA && handleB) { + const cases = (ifElseNode.data as any).cases || [] + const isAElse = handleA === 'false' + const isBElse = handleB === 'false' + + if (isAElse) return 1 + if (isBElse) return -1 + + const indexA = cases.findIndex((c: any) => c.case_id === handleA) + const indexB = cases.findIndex((c: any) => c.case_id === handleB) + + if (indexA !== -1 && indexB !== -1) + return indexA - indexB + } + return 0 + }) + + const parentDummyId = `dummy-parent-${ifElseNode.id}` + dagreGraph.setNode(parentDummyId, { width: 1, height: 1 }) + + const dummyNodes: string[] = [] + sortedChildEdges.forEach((edge) => { + const dummyNodeId = `dummy-${edge.source}-${edge.target}` + dummyNodes.push(dummyNodeId) + dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 }) + dagreGraph.setParent(dummyNodeId, parentDummyId) + + const edgeIndex = edges.findIndex(e => e.id === edge.id) + if (edgeIndex > -1) + edges.splice(edgeIndex, 1) + + edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge) + edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge) + }) + + for (let i = 0; i < dummyNodes.length - 1; i++) { + const sourceDummy = dummyNodes[i] + const targetDummy = dummyNodes[i + 1] + edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge) + } + }) + dagreGraph.setGraph({ rankdir: 'LR', align: 'UL', nodesep: 40, - ranksep: 60, + ranksep: virtualLogicApplied ? 30 : 60, ranker: 'tight-tree', marginx: 30, marginy: 200, }) + nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: node.width!, diff --git a/web/app/components/workflow/utils/index.ts b/web/app/components/workflow/utils/index.ts index 4a1da760d4..0d10551dda 100644 --- a/web/app/components/workflow/utils/index.ts +++ b/web/app/components/workflow/utils/index.ts @@ -1,7 +1,7 @@ export * from './node' export * from './edge' export * from './workflow-init' -export * from './layout' +export * from './dagre-layout' export * from './common' export * from './tool' export * from './workflow'