Merge pull request #1038 from SuanmoSuanyangTechnology/fix/history_zy
fix(web): history undo/redo
This commit is contained in:
@@ -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;
|
||||
|
||||
// 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 graph = graphRef.current;
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user