Merge pull request #1038 from SuanmoSuanyangTechnology/fix/history_zy

fix(web): history undo/redo
This commit is contained in:
yingzhao
2026-05-06 10:41:42 +08:00
committed by GitHub
3 changed files with 192 additions and 216 deletions

View File

@@ -355,14 +355,13 @@ const CaseList: FC<CaseListProps> = ({
// Update node ports based on case count changes (add/remove cases)
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return;
const graph = graphRef.current;
// Get current port count to determine if it's an add or remove operation
const currentPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right');
const currentCaseCount = currentPorts.length - 1; // Exclude ELSE port
const currentRightPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right');
const currentCaseCount = currentRightPorts.length - 1;
const isAddingCase = removedCaseIndex === undefined && caseCount > currentCaseCount;
// Save existing edge connections (including left-side port connections)
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
const existingEdges = graph.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
);
const edgeConnections = existingEdges.map((edge: any) => ({
@@ -371,113 +370,70 @@ const CaseList: FC<CaseListProps> = ({
targetCellId: edge.getTargetCellId(),
targetPortId: edge.getTargetPortId(),
sourceCellId: edge.getSourceCellId(),
isIncoming: edge.getTargetCellId() === selectedNode.id
isIncoming: edge.getTargetCellId() === selectedNode.id,
}));
// Remove all existing right-side ports
const existingPorts = selectedNode.getPorts();
existingPorts.forEach((port: any) => {
if (port.group === 'right') {
selectedNode.removePort(port.id);
const cases = form.getFieldValue(name) || [];
const leftPorts = selectedNode.getPorts().filter((p: any) => p.group !== 'right');
const newRightPorts = Array.from({ length: caseCount + 1 }, (_, i) => ({
id: `CASE${i + 1}`,
group: 'right',
args: { x: nodeWidth, y: getConditionNodeCasePortY(cases, i) },
}));
graph.startBatch('update-ports');
existingEdges.forEach((edge: any) => graph.removeCell(edge));
// Replace all ports in one prop call — produces a single cell:change:ports command
selectedNode.prop('ports/items', [...leftPorts, ...newRightPorts], { rewrite: true });
selectedNode.prop('size', { width: nodeWidth, height: calcConditionNodeTotalHeight(cases) });
edgeConnections.forEach(({sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
if (isIncoming) {
const sourceCell = graph.getCellById(sourceCellId);
if (sourceCell) {
graph.addEdge({
source: { cell: sourceCellId, port: sourcePortId },
target: { cell: selectedNode.id, port: targetPortId },
...edgeAttrs
});
sourceCell.toFront();
bringLoopChildrenToFront(sourceCell);
selectedNode.toFront();
bringLoopChildrenToFront(selectedNode);
}
return;
}
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) return;
let newPortId = sourcePortId;
if (removedCaseIndex !== undefined) {
if (originalCaseNumber > removedCaseIndex + 1) {
newPortId = `CASE${originalCaseNumber - 1}`;
} else if (originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`;
}
} else if (isAddingCase && originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`;
}
if (newRightPorts.find((p) => p.id === newPortId)) {
const targetCell = graph.getCellById(targetCellId);
if (targetCell) {
graph.addEdge({
source: { cell: selectedNode.id, port: newPortId },
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront();
bringLoopChildrenToFront(selectedNode);
targetCell.toFront();
bringLoopChildrenToFront(targetCell);
}
}
});
const cases = form.getFieldValue(name) || [];
selectedNode.prop('size', { width: nodeWidth, height: calcConditionNodeTotalHeight(cases) });
// Add ELIF ports
for (let i = 0; i < caseCount; i++) {
selectedNode.addPort({
id: `CASE${i + 1}`,
group: 'right',
args: {
x: nodeWidth,
y: getConditionNodeCasePortY(cases, i),
},
});
}
// Add ELSE port
selectedNode.addPort({
id: `CASE${caseCount + 1}`,
group: 'right',
args: {
x: nodeWidth,
y: getConditionNodeCasePortY(cases, caseCount),
},
});
// Restore edge connections
setTimeout(() => {
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
// If it's an incoming connection (left-side port), restore directly
if (isIncoming) {
const sourceCell = graphRef.current?.getCellById(sourceCellId);
if (sourceCell) {
graphRef.current?.addEdge({
source: { cell: sourceCellId, port: sourcePortId },
target: { cell: selectedNode.id, port: targetPortId },
...edgeAttrs,
});
}
sourceCell.toFront()
selectedNode.toFront()
bringLoopChildrenToFront(sourceCell)
bringLoopChildrenToFront(selectedNode)
graphRef.current?.removeCell(edge);
return;
}
// Handle right-side port connections
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
// If it's a remove operation and the port is being removed, delete the connection
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
graphRef.current?.removeCell(edge);
return;
}
let newPortId = sourcePortId;
// If it's a remove operation, remap port IDs
if (removedCaseIndex !== undefined) {
if (originalCaseNumber > removedCaseIndex + 1) {
// Ports after the removed port, shift numbering forward
newPortId = `CASE${originalCaseNumber - 1}`;
}
// ELSE port always maps to the new ELSE port position
else if (originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`;
}
} else if (isAddingCase) {
// If it's an add operation, ELSE port needs to be remapped
if (originalCaseNumber === currentCaseCount + 1) {
newPortId = `CASE${caseCount + 1}`; // New ELSE port
}
// Newly added ports don't restore any connections
}
const newPorts = selectedNode.getPorts();
const matchingPort = newPorts.find((port: any) => port.id === newPortId);
if (matchingPort) {
const targetCell = graphRef.current?.getCellById(targetCellId);
if (targetCell) {
graphRef.current?.addEdge({
source: { cell: selectedNode.id, port: newPortId },
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
targetCell.toFront()
bringLoopChildrenToFront(targetCell)
}
}
graphRef.current?.removeCell(edge);
});
}, 50);
graph.stopBatch('update-ports');
};
const handleChangeLogicalOperator = (index: number) => {

View File

@@ -42,109 +42,73 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
// Update node ports based on category count changes (add/remove categories)
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return;
const graph = graphRef.current;
// Save existing edge connections (including left-side port connections)
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
const existingEdges = graph.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
);
const edgeConnections = existingEdges.map((edge: any) => ({
edge,
sourcePortId: edge.getSourcePortId(),
targetCellId: edge.getTargetCellId(),
targetPortId: edge.getTargetPortId(),
sourceCellId: edge.getSourceCellId(),
isIncoming: edge.getTargetCellId() === selectedNode.id
isIncoming: edge.getTargetCellId() === selectedNode.id,
}));
// Remove all existing right-side ports
const existingPorts = selectedNode.getPorts();
existingPorts.forEach((port: any) => {
if (port.group === 'right') {
selectedNode.removePort(port.id);
}
});
graph.startBatch('update-ports');
existingEdges.forEach((edge: any) => graph.removeCell(edge));
// Replace all ports in one prop call — produces a single cell:change:ports command
const leftPorts = selectedNode.getPorts().filter((p: any) => p.group !== 'right');
const newRightPorts = Array.from({ length: caseCount }, (_, i) => ({
id: `CASE${i + 1}`,
group: 'right',
args: { x: nodeWidth, y: portItemArgsY * i + conditionNodePortItemArgsY },
}));
selectedNode.prop('ports/items', [...leftPorts, ...newRightPorts], { rewrite: true });
// Calculate new node height: base height 88px + 30px for each additional port
const newHeight = conditionNodeHeight + (caseCount - 2) * conditionNodeItemHeight;
selectedNode.prop('size', { width: nodeWidth, height: newHeight < conditionNodeHeight ? conditionNodeHeight : newHeight });
selectedNode.prop('size', { width: nodeWidth, height: newHeight < conditionNodeHeight ? conditionNodeHeight : newHeight })
// Update right port x position
const currentPorts = selectedNode.getPorts();
currentPorts.forEach(port => {
if (port.group === 'right' && port.args) {
selectedNode.portProp(port.id!, 'args/x', nodeWidth);
edgeConnections.forEach(({ sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
if (isIncoming) {
const sourceCell = graph.getCellById(sourceCellId);
if (sourceCell) {
graph.addEdge({
source: { cell: sourceCellId, port: sourcePortId },
target: { cell: selectedNode.id, port: targetPortId },
...edgeAttrs
});
sourceCell.toFront();
bringLoopChildrenToFront(sourceCell);
selectedNode.toFront();
bringLoopChildrenToFront(selectedNode);
}
return;
}
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) return;
let newPortId = sourcePortId;
if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
newPortId = `CASE${originalCaseNumber - 1}`;
}
if (newRightPorts.find((p) => p.id === newPortId)) {
const targetCell = graph.getCellById(targetCellId);
if (targetCell) {
graph.addEdge({
source: { cell: selectedNode.id, port: newPortId },
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront();
bringLoopChildrenToFront(selectedNode);
targetCell.toFront();
bringLoopChildrenToFront(targetCell);
}
}
});
// Add category ports
for (let i = 0; i < caseCount; i++) {
selectedNode.addPort({
id: `CASE${i + 1}`,
group: 'right',
args: {
x: nodeWidth,
y: portItemArgsY * i + conditionNodePortItemArgsY,
},
});
}
// Restore edge connections
setTimeout(() => {
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
graphRef.current?.removeCell(edge);
// If it's an incoming connection (left-side port), restore directly
if (isIncoming) {
const sourceCell = graphRef.current?.getCellById(sourceCellId);
if (sourceCell) {
graphRef.current?.addEdge({
source: { cell: sourceCellId, port: sourcePortId },
target: { cell: selectedNode.id, port: targetPortId },
...edgeAttrs
});
sourceCell.toFront()
bringLoopChildrenToFront(sourceCell)
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
}
return;
}
// Handle right-side port connections
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
// If it's a removed port, don't recreate the connection
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
return;
}
let newPortId = sourcePortId;
// If a port was removed, remap subsequent port IDs
if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
newPortId = `CASE${originalCaseNumber - 1}`;
}
// Check if the new port exists
const newPorts = selectedNode.getPorts();
const matchingPort = newPorts.find((port: any) => port.id === newPortId);
if (matchingPort) {
const targetCell = graphRef.current?.getCellById(targetCellId);
if (targetCell) {
graphRef.current?.addEdge({
source: { cell: selectedNode.id, port: newPortId },
target: { cell: targetCellId, port: targetPortId },
...edgeAttrs
});
selectedNode.toFront()
bringLoopChildrenToFront(selectedNode)
targetCell.toFront()
bringLoopChildrenToFront(targetCell)
}
}
});
}, 50);
graph.stopBatch('update-ports');
};
const handleAddCategory = (addFunc: Function) => {

View File

@@ -124,9 +124,7 @@ export const useWorkflowGraph = ({
const [canRedo, setCanRedo] = useState(false)
const [historyRecords, setHistoryRecords] = useState<HistoryRecord[]>([])
const lastHistoryRef = useRef<{ cellIds: string[]; timestamp: number; type: string } | null>(null)
const undoRef = useRef<() => void>(() => {})
const redoRef = useRef<() => void>(() => {})
const syncChildRelationshipsRef = useRef<() => void>(() => {})
const syncChildRelationshipsRef = useRef<() => void>(() => { })
const isSyncingRef = useRef(false)
useEffect(() => {
if (!graphRef.current) return
@@ -532,24 +530,82 @@ export const useWorkflowGraph = ({
const graph = graphRef.current
graph.disableHistory()
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, { silent: true })
}
})
graph.getNodes().forEach(node => {
const nodeData = node.getData()
const children = node.getChildren()
if (!children?.length) return
children.forEach(child => {
if (!child.isNode()) return
const childCycleId = (child as Node).getData?.()?.cycle
if (childCycleId !== node.id && childCycleId !== node.getData?.()?.id) {
node.removeChild(child, { silent: true })
const cycleId = nodeData?.cycle
if (cycleId) {
const parentNode = graph.getCellById(cycleId) as Node | null
if (!parentNode) return
if (!parentNode.getChildren()?.some(c => c.id === node.id)) {
parentNode.addChild(node, { silent: true })
}
})
}
if (nodeData.type === 'if-else') {
const rightPorts = node.getPorts().filter(p => p.group === 'right')
const caseCount = rightPorts.length - 1 // last port is ELSE
const currentCases: any[] = nodeData.config?.cases?.defaultValue ?? []
const newCases = caseCount !== currentCases.length
? Array.from({ length: caseCount }, (_, i) => currentCases[i] ?? { logical_operator: 'and', expressions: [] })
: currentCases
if (caseCount !== currentCases.length) {
node.setData({
...nodeData,
config: { ...nodeData.config, cases: { ...nodeData.config.cases, defaultValue: newCases } }
}, { deep: false, silent: true })
}
// Sync node height and port Y positions
node.prop('size', { width: nodeWidth, height: calcConditionNodeTotalHeight(newCases) })
newCases.forEach((_c: any, i: number) => {
node.portProp(`CASE${i + 1}`, 'args/y', getConditionNodeCasePortY(newCases, i))
})
node.portProp(`CASE${newCases.length + 1}`, 'args/y', getConditionNodeCasePortY(newCases, newCases.length))
node.toFront()
graph.getEdges().filter(e => e.getSourceCellId() === node.id).forEach(e => {
const tgt = graph.getCellById(e.getTargetCellId())
tgt?.toFront()
})
} else if (nodeData.type === 'question-classifier') {
const rightPorts = node.getPorts().filter(p => p.group === 'right')
const currentCategories: any[] = nodeData.config?.categories?.defaultValue ?? []
const categoryCount = rightPorts.length
const newCategories = categoryCount !== currentCategories.length
? rightPorts.map((port, i) => {
if (currentCategories[i]) return currentCategories[i]
const edge = graph.getEdges().find(e => e.getSourceCellId() === node.id && e.getSourcePortId() === port.id)
return edge ? { name: '' } : {}
})
: currentCategories
if (categoryCount !== currentCategories.length) {
node.setData({
...nodeData,
config: { ...nodeData.config, categories: { ...nodeData.config.categories, defaultValue: [...newCategories] } }
}, { deep: false, silent: true })
}
// Sync node height and port Y positions
const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight
node.prop('size', { width: nodeWidth, height: Math.max(newHeight, conditionNodeHeight) })
rightPorts.forEach((_p, i) => {
node.portProp(`CASE${i + 1}`, 'args/y', portItemArgsY * i + conditionNodePortItemArgsY)
})
node.toFront()
graph.getEdges().filter(e => e.getSourceCellId() === node.id).forEach(e => {
const tgt = graph.getCellById(e.getTargetCellId())
tgt?.toFront()
})
}
if (children?.length) {
children.forEach(child => {
if (!child.isNode()) return
const childCycleId = (child as Node).getData?.()?.cycle
if (childCycleId !== node.id && childCycleId !== node.getData?.()?.id) {
node.removeChild(child, { silent: true })
}
})
}
})
resizeGroupNodes(graph)
graph.getEdges().forEach(edge => {