diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
index cffb62dd..ac81a667 100644
--- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
@@ -1,134 +1,15 @@
-import { useEffect } from 'react';
-import { useTranslation } from 'react-i18next'
import clsx from 'clsx';
import type { ReactShapeConfig } from '@antv/x6-react-shape';
import { Flex } from 'antd';
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
+import { useTranslation } from 'react-i18next'
-import { graphNodeLibrary, edgeAttrs } from '../../constant';
import NodeTools from './NodeTools'
-const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
+const LoopNode: ReactShapeConfig['component'] = ({ node }) => {
const data = node.getData() || {};
const { t } = useTranslation()
- useEffect(() => {
- // 使用setTimeout确保在所有节点都添加完成后再创建连线
- const timer = setTimeout(() => {
- initNodes()
- checkAndAddAddNode()
- }, 50)
-
- return () => clearTimeout(timer)
- }, [graph])
-
- const checkAndAddAddNode = () => {
- if (!graph) return;
-
- const childNodes = graph.getNodes().filter((n: any) => n.getData()?.cycle === data.id);
- const cycleStartNodes = childNodes.filter((n: any) => n.getData()?.type === 'cycle-start');
-
- // 如果只有一个cycle-start节点且没有其他类型的子节点,则添加add-node
- if (cycleStartNodes.length === 1 && childNodes.length === 1) {
- const cycleStartNode = cycleStartNodes[0];
- const cycleStartBBox = cycleStartNode.getBBox();
-
- const addNode = graph.addNode({
- ...graphNodeLibrary.addStart,
- x: cycleStartBBox.x + 84,
- y: cycleStartBBox.y + 4,
- data: {
- type: 'add-node',
- label: t('workflow.addNode'),
- icon: '+',
- parentId: node.id,
- cycle: data.id,
- },
- });
-
- node.addChild(addNode);
-
- // 连接cycle-start和add-node
- const sourcePorts = cycleStartNode.getPorts();
- const targetPorts = addNode.getPorts();
- const sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
- const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
-
- // 然后创建连线
- graph.addEdge({
- source: { cell: cycleStartNode.id, port: sourcePort },
- target: { cell: addNode.id, port: targetPort },
- ...edgeAttrs,
- });
-
- cycleStartNode.toFront()
- addNode.toFront()
- }
- }
-
- const initNodes = () => {
- // 检查是否存在cycle为当前节点ID的子节点,若存在则不调用initNodes,避免重复创建
- const existingCycleNodes = graph.getNodes().filter((n: any) =>
- n.getData()?.cycle === data.id
- );
- if (existingCycleNodes.length > 0) return;
- // 添加默认子节点
- const parentBBox = node.getBBox();
- const centerX = parentBBox.x + 24;
- const centerY = parentBBox.y + 70;
-
- const cycleStartNodeId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
- const cycleStartNode = graph.addNode({
- ...graphNodeLibrary.cycleStart,
- x: centerX,
- y: centerY,
- id: cycleStartNodeId,
- data: {
- id: cycleStartNodeId,
- type: 'cycle-start',
- parentId: node.id,
- isDefault: true, // 标记为默认节点,不可删除
- cycle: data.id,
- },
- });
- const addNode = graph.addNode({
- ...graphNodeLibrary.addStart,
- x: centerX + 84,
- y: centerY + 4,
- data: {
- type: 'add-node',
- label: t('workflow.addNode'),
- icon: '+',
- parentId: node.id,
- cycle: data.id,
- },
- });
- node.addChild(cycleStartNode)
- node.addChild(addNode)
- const sourcePorts = cycleStartNode.getPorts()
- const targetPorts = addNode.getPorts()
- let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
-
- const edgeConfig = {
- source: {
- cell: cycleStartNode.id,
- port: sourcePort
- },
- target: {
- cell: addNode.id,
- port: targetPorts.find((port: any) => port.group === 'left')?.id || 'left'
- },
- ...edgeAttrs
- }
- graph.addEdge(edgeConfig)
-
- setTimeout(() => {
-
- cycleStartNode.toFront()
- addNode.toFront()
- }, 0)
- }
-
return (
{
+ graph.getNodes().forEach(parentNode => {
+ const parentType = parentNode.getData()?.type
+ if (parentType !== 'loop' && parentType !== 'iteration') return
+ const children = graph.getNodes().filter(
+ n => n.getData()?.cycle === parentNode.getData()?.id && n.getData()?.type !== 'add-node'
+ )
+ if (!children.length) return
+ const padding = 24
+ const headerHeight = 50
+ const childBounds = children.map(c => c.getBBox())
+ const minX = Math.min(...childBounds.map(b => b.x))
+ const minY = Math.min(...childBounds.map(b => b.y))
+ const maxX = Math.max(...childBounds.map(b => b.x + b.width))
+ const maxY = Math.max(...childBounds.map(b => b.y + b.height))
+ const parentBBox = parentNode.getBBox()
+ const newWidth = Math.max(parentBBox.width, maxX - minX + padding * 2)
+ const newHeight = Math.max(parentBBox.height, maxY - minY + padding * 2 + headerHeight)
+ parentNode.prop('size', { width: newWidth, height: newHeight })
+ parentNode.getPorts().forEach(port => {
+ if (port.group === 'right' && port.args) {
+ parentNode.portProp(port.id!, 'args/x', newWidth)
+ }
+ })
+ })
+ }
+
+ const syncChildRelationships = () => {
+ if (!graphRef.current) return
+ const graph = graphRef.current
+ // Re-establish parent-child relationships based on cycle data
+ graph.getNodes().forEach(node => {
+ const cycleId = node.getData()?.cycle
+ if (!cycleId) return
+ const parentNode = graph.getCellById(cycleId) as Node | null
+ if (!parentNode) return
+ if (!parentNode.getChildren()?.some(c => c.id === node.id)) {
+ parentNode.addChild(node)
+ }
+ })
+ // Remove stale parent-child links (parent exists but child's cycle no longer points to it)
+ graph.getNodes().forEach(node => {
+ const children = node.getChildren()
+ if (!children?.length) return
+ children.forEach(child => {
+ const childCycleId = (child as Node).getData?.()?.cycle
+ if (childCycleId !== node.id && childCycleId !== node.getData?.()?.id) {
+ node.removeChild(child)
+ }
+ })
+ })
+ // Recalculate group node size based on current children
+ resizeGroupNodes(graph)
+ // Bring child edges and nodes to front
+ graph.getEdges().forEach(edge => {
+ const src = graph.getCellById(edge.getSourceCellId())
+ const tgt = graph.getCellById(edge.getTargetCellId())
+ if (src?.getData()?.cycle || tgt?.getData()?.cycle) {
+ edge.toFront()
+ }
+ })
+ graph.getNodes().forEach(node => {
+ if (node.getData()?.cycle) node.toFront()
+ })
+ }
/**
* Setup X6 graph plugins (MiniMap, Snapline, Clipboard, Keyboard)
*/
@@ -538,6 +603,9 @@ export const useWorkflowGraph = ({
setCanUndo(graphRef.current?.canUndo() ?? false)
setCanRedo(graphRef.current?.canRedo() ?? false)
})
+
+ graphRef.current.on('history:undo', syncChildRelationships)
+ graphRef.current.on('history:redo', syncChildRelationships)
};
// 显示/隐藏连接桩
// const showPorts = (show: boolean) => {
@@ -781,48 +849,50 @@ export const useWorkflowGraph = ({
// Delete all collected nodes and edges
if (cells.length > 0) {
+ // Pre-calculate which parents need an add-node restored (before removal changes the graph)
+ const parentsNeedingAddNode = parentNodesToUpdate
+ .filter(parentNode => {
+ const parentShape = parentNode.shape;
+ if (parentShape !== 'loop-node' && parentShape !== 'iteration-node') return false;
+ const parentData = parentNode.getData();
+ const allChildren = graphRef.current!.getNodes().filter(n => n.getData()?.cycle === parentData.id);
+ const cycleStartNodes = allChildren.filter(n => n.getData()?.type === 'cycle-start');
+ // After deletion, only cycle-start will remain
+ const nonCycleStartToDelete = cells.filter(c =>
+ c.isNode() &&
+ (c as Node).getData()?.cycle === parentData.id &&
+ (c as Node).getData()?.type !== 'cycle-start'
+ );
+ return cycleStartNodes.length === 1 && (allChildren.length - nonCycleStartToDelete.length) === 1;
+ })
+ .map(parentNode => ({
+ parentNode,
+ cycleStartNode: graphRef.current!.getNodes().find(
+ n => n.getData()?.cycle === parentNode.getData().id && n.getData()?.type === 'cycle-start'
+ )!
+ }))
+ .filter(({ cycleStartNode }) => !!cycleStartNode);
+
graphRef.current?.startBatch('delete');
- // Remove parent-child relationships before removeCells
- parentNodesToUpdate.forEach(parentNode => {
- cells.filter(c => c.isNode() && (c as Node).getData()?.cycle === parentNode.getData()?.id)
- .forEach(child => parentNode.removeChild(child));
- });
graphRef.current?.removeCells(cells);
- // If parent is iteration/loop and only cycle-start remains, add add-node connected to it
- parentNodesToUpdate.forEach(parentNode => {
- const parentShape = parentNode.shape;
- if (parentShape !== 'loop-node' && parentShape !== 'iteration-node') return;
+ parentsNeedingAddNode.forEach(({ parentNode, cycleStartNode }) => {
const parentData = parentNode.getData();
- const remainingChildren = graphRef.current!.getNodes().filter(
- n => n.getData()?.cycle === parentData.id
- );
- const cycleStartNodes = remainingChildren.filter(n => n.getData()?.type === 'cycle-start');
- if (cycleStartNodes.length === 1 && remainingChildren.length === 1) {
- const cycleStartNode = cycleStartNodes[0];
- const bbox = cycleStartNode.getBBox();
- const addNode = graphRef.current!.addNode({
- ...graphNodeLibrary.addStart,
- x: bbox.x + 84,
- y: bbox.y + 4,
- data: {
- type: 'add-node',
- parentId: parentNode.id,
- cycle: parentData.id,
- label: t('workflow.addNode'),
- icon: '+',
- },
- });
- parentNode.addChild(addNode);
- const sourcePort = cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right';
- const targetPort = addNode.getPorts().find(p => p.group === 'left')?.id || 'left';
- graphRef.current!.addEdge({
- source: { cell: cycleStartNode.id, port: sourcePort },
- target: { cell: addNode.id, port: targetPort },
- ...edgeAttrs,
- });
- }
+ const bbox = cycleStartNode.getBBox();
+ const addNode = graphRef.current!.addNode({
+ ...graphNodeLibrary.addStart,
+ x: bbox.x + 84,
+ y: bbox.y + 4,
+ data: { type: 'add-node', parentId: parentNode.id, cycle: parentData.id, label: t('workflow.addNode'), icon: '+' },
+ });
+ parentNode.addChild(addNode);
+ graphRef.current!.addEdge({
+ source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' },
+ target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' },
+ ...edgeAttrs,
+ });
});
+
graphRef.current?.stopBatch('delete');
}
return false;
@@ -1199,13 +1269,39 @@ export const useWorkflowGraph = ({
};
if (dragData.type === 'loop' || dragData.type === 'iteration') {
- graphRef.current.addNode({
+ graphRef.current.startBatch('add-group')
+ const parentNode = graphRef.current.addNode({
...graphNodeLibrary[dragData.type],
x: point.x - 150,
y: point.y - 100,
id: cleanNodeData.id,
data: { ...cleanNodeData, isGroup: true },
});
+ const parentBBox = parentNode.getBBox()
+ const cycleStartId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+ const cycleStartNode = graphRef.current.addNode({
+ ...graphNodeLibrary.cycleStart,
+ x: parentBBox.x + 24,
+ y: parentBBox.y + 70,
+ id: cycleStartId,
+ data: { id: cycleStartId, type: 'cycle-start', parentId: cleanNodeData.id, isDefault: true, cycle: cleanNodeData.id },
+ })
+ const addNode = graphRef.current.addNode({
+ ...graphNodeLibrary.addStart,
+ x: parentBBox.x + 24 + 84,
+ y: parentBBox.y + 70 + 4,
+ data: { type: 'add-node', label: t('workflow.addNode'), icon: '+', parentId: cleanNodeData.id, cycle: cleanNodeData.id },
+ })
+ parentNode.addChild(cycleStartNode)
+ parentNode.addChild(addNode)
+ graphRef.current.addEdge({
+ source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' },
+ target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' },
+ ...edgeAttrs,
+ })
+ cycleStartNode.toFront()
+ addNode.toFront()
+ graphRef.current.stopBatch('add-group')
} else if (dragData.type === 'if-else') {
// Create condition node
graphRef.current.addNode({