fix(web): loop & iteration child node history
This commit is contained in:
@@ -1,134 +1,15 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { graphNodeLibrary, edgeAttrs } from '../../constant';
|
|
||||||
import NodeTools from './NodeTools'
|
import NodeTools from './NodeTools'
|
||||||
|
|
||||||
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
const LoopNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||||
const data = node.getData() || {};
|
const data = node.getData() || {};
|
||||||
const { t } = useTranslation()
|
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 (
|
return (
|
||||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||||
'rb:border-[#171719]!': data.isSelected && !data.executionStatus,
|
'rb:border-[#171719]!': data.isSelected && !data.executionStatus,
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-27 13:47:02
|
* @Last Modified time: 2026-04-27 16:30:30
|
||||||
*/
|
*/
|
||||||
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6';
|
import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6';
|
||||||
import type { HistoryCommand as Command } from '@antv/x6/lib/plugin/history/type';
|
|
||||||
import { register } from '@antv/x6-react-shape';
|
import { register } from '@antv/x6-react-shape';
|
||||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
@@ -493,6 +492,72 @@ export const useWorkflowGraph = ({
|
|||||||
graphRef.current.cleanHistory()
|
graphRef.current.cleanHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resizeGroupNodes = (graph: Graph) => {
|
||||||
|
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)
|
* Setup X6 graph plugins (MiniMap, Snapline, Clipboard, Keyboard)
|
||||||
*/
|
*/
|
||||||
@@ -538,6 +603,9 @@ export const useWorkflowGraph = ({
|
|||||||
setCanUndo(graphRef.current?.canUndo() ?? false)
|
setCanUndo(graphRef.current?.canUndo() ?? false)
|
||||||
setCanRedo(graphRef.current?.canRedo() ?? false)
|
setCanRedo(graphRef.current?.canRedo() ?? false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
graphRef.current.on('history:undo', syncChildRelationships)
|
||||||
|
graphRef.current.on('history:redo', syncChildRelationships)
|
||||||
};
|
};
|
||||||
// 显示/隐藏连接桩
|
// 显示/隐藏连接桩
|
||||||
// const showPorts = (show: boolean) => {
|
// const showPorts = (show: boolean) => {
|
||||||
@@ -781,48 +849,50 @@ export const useWorkflowGraph = ({
|
|||||||
|
|
||||||
// Delete all collected nodes and edges
|
// Delete all collected nodes and edges
|
||||||
if (cells.length > 0) {
|
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');
|
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);
|
graphRef.current?.removeCells(cells);
|
||||||
|
|
||||||
// If parent is iteration/loop and only cycle-start remains, add add-node connected to it
|
parentsNeedingAddNode.forEach(({ parentNode, cycleStartNode }) => {
|
||||||
parentNodesToUpdate.forEach(parentNode => {
|
|
||||||
const parentShape = parentNode.shape;
|
|
||||||
if (parentShape !== 'loop-node' && parentShape !== 'iteration-node') return;
|
|
||||||
const parentData = parentNode.getData();
|
const parentData = parentNode.getData();
|
||||||
const remainingChildren = graphRef.current!.getNodes().filter(
|
const bbox = cycleStartNode.getBBox();
|
||||||
n => n.getData()?.cycle === parentData.id
|
const addNode = graphRef.current!.addNode({
|
||||||
);
|
...graphNodeLibrary.addStart,
|
||||||
const cycleStartNodes = remainingChildren.filter(n => n.getData()?.type === 'cycle-start');
|
x: bbox.x + 84,
|
||||||
if (cycleStartNodes.length === 1 && remainingChildren.length === 1) {
|
y: bbox.y + 4,
|
||||||
const cycleStartNode = cycleStartNodes[0];
|
data: { type: 'add-node', parentId: parentNode.id, cycle: parentData.id, label: t('workflow.addNode'), icon: '+' },
|
||||||
const bbox = cycleStartNode.getBBox();
|
});
|
||||||
const addNode = graphRef.current!.addNode({
|
parentNode.addChild(addNode);
|
||||||
...graphNodeLibrary.addStart,
|
graphRef.current!.addEdge({
|
||||||
x: bbox.x + 84,
|
source: { cell: cycleStartNode.id, port: cycleStartNode.getPorts().find(p => p.group === 'right')?.id || 'right' },
|
||||||
y: bbox.y + 4,
|
target: { cell: addNode.id, port: addNode.getPorts().find(p => p.group === 'left')?.id || 'left' },
|
||||||
data: {
|
...edgeAttrs,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
graphRef.current?.stopBatch('delete');
|
graphRef.current?.stopBatch('delete');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1199,13 +1269,39 @@ export const useWorkflowGraph = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (dragData.type === 'loop' || dragData.type === 'iteration') {
|
if (dragData.type === 'loop' || dragData.type === 'iteration') {
|
||||||
graphRef.current.addNode({
|
graphRef.current.startBatch('add-group')
|
||||||
|
const parentNode = graphRef.current.addNode({
|
||||||
...graphNodeLibrary[dragData.type],
|
...graphNodeLibrary[dragData.type],
|
||||||
x: point.x - 150,
|
x: point.x - 150,
|
||||||
y: point.y - 100,
|
y: point.y - 100,
|
||||||
id: cleanNodeData.id,
|
id: cleanNodeData.id,
|
||||||
data: { ...cleanNodeData, isGroup: true },
|
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') {
|
} else if (dragData.type === 'if-else') {
|
||||||
// Create condition node
|
// Create condition node
|
||||||
graphRef.current.addNode({
|
graphRef.current.addNode({
|
||||||
|
|||||||
Reference in New Issue
Block a user