Skip to content

Commit 02b888e

Browse files
authored
Merge pull request #248 from ShipSecAI/@krishna9358/undo-redo
fix: undo/redo not restoring connections properly
2 parents c4d75bb + d5fb6a9 commit 02b888e

File tree

3 files changed

+36
-14
lines changed

3 files changed

+36
-14
lines changed

frontend/src/components/workflow/Canvas.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -904,27 +904,41 @@ export function Canvas({
904904
dedupedEdges.set(edge.id, { ...edge, selected: false });
905905
});
906906

907+
// Calculate next state for snapshot before applying changes
908+
let nextNodes = nodes;
909+
let nextEdges = edges;
910+
907911
if (selectedNodes.length > 0) {
908-
setNodes((nds) => nds.filter((node) => !nodeIds.has(node.id)));
909-
setEdges((eds) =>
910-
eds.filter((edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target)),
912+
nextNodes = nodes.filter((node) => !nodeIds.has(node.id));
913+
nextEdges = edges.filter(
914+
(edge) => !nodeIds.has(edge.source) && !nodeIds.has(edge.target),
911915
);
912-
setSelectedNode(null);
913916
}
914917

915918
if (selectedEdges.length > 0) {
916-
setEdges((eds) => eds.filter((edge) => !selectedEdgeIds.has(edge.id)));
919+
nextEdges = nextEdges.filter((edge) => !selectedEdgeIds.has(edge.id));
920+
}
921+
922+
// Apply the changes
923+
if (selectedNodes.length > 0) {
924+
setNodes(nextNodes);
925+
setEdges(nextEdges);
926+
setSelectedNode(null);
927+
} else if (selectedEdges.length > 0) {
928+
setEdges(nextEdges);
917929
}
918930

931+
// Capture snapshot for undo/redo
919932
if (selectedNodes.length > 0 || selectedEdges.length > 0) {
933+
onSnapshot?.(nextNodes, nextEdges);
920934
markDirty();
921935
}
922936
}
923937
};
924938

925939
document.addEventListener('keydown', handleKeyPress);
926940
return () => document.removeEventListener('keydown', handleKeyPress);
927-
}, [nodes, edges, setNodes, setEdges, markDirty, mode]);
941+
}, [nodes, edges, setNodes, setEdges, markDirty, mode, onSnapshot, toast]);
928942

929943
// Panel width changes are handled by CSS transitions, no manual viewport translation needed
930944

frontend/src/features/workflow-builder/WorkflowBuilder.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,16 +291,19 @@ function WorkflowBuilderContent() {
291291

292292
const onEdgesChange = useCallback(
293293
(changes: any[]) => {
294-
// Capture snapshot for edge changes
294+
// Capture snapshot for edge changes (add/remove)
295+
// Note: Edge removals due to node deletion are handled by onNodesChange,
296+
// so we only need to capture explicit edge changes here
295297
if (mode === 'design' && changes.length > 0) {
296298
const hasStructuralChange = changes.some(
297299
(c: any) => c.type === 'add' || c.type === 'remove',
298300
);
299301
if (hasStructuralChange) {
302+
const currentNodes = designNodesRef.current;
300303
const currentEdges = designEdgesRef.current;
301304
const nextEdges = applyEdgeChanges(changes, currentEdges);
302-
// Pass undefined for nodes to allow merging with pending node changes (e.g. from node deletion)
303-
captureSnapshot(undefined, nextEdges);
305+
// Pass both nodes and edges to ensure consistent snapshot
306+
captureSnapshot(currentNodes, nextEdges);
304307
}
305308
}
306309

frontend/src/features/workflow-builder/hooks/useWorkflowHistory.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ export const useWorkflowHistory = ({
114114
);
115115

116116
/**
117-
* Capture the current graph state as an undoable snapshot
117+
* Capture the current graph state as an undoable snapshot.
118+
*
119+
* IMPORTANT: Always pass BOTH nodes and edges for a consistent snapshot.
120+
* The debouncing ensures that rapid changes (like node deletion which triggers
121+
* both onNodesChange and onEdgesChange) are captured as a single history entry.
118122
*/
119123
const captureSnapshot = useCallback(
120124
(nodesOverride?: ReactFlowNode<FrontendNodeData>[], edgesOverride?: ReactFlowEdge[]) => {
@@ -123,16 +127,17 @@ export const useWorkflowHistory = ({
123127
}
124128

125129
// Update pending state with new overrides if provided
126-
if (nodesOverride) pendingStateRef.current.nodes = nodesOverride;
127-
if (edgesOverride) pendingStateRef.current.edges = edgesOverride;
130+
// Always prefer the explicitly passed state over refs for consistency
131+
if (nodesOverride !== undefined) pendingStateRef.current.nodes = nodesOverride;
132+
if (edgesOverride !== undefined) pendingStateRef.current.edges = edgesOverride;
128133

129134
if (debounceRef.current) {
130135
clearTimeout(debounceRef.current);
131136
}
132137

133138
debounceRef.current = setTimeout(() => {
134-
// Resolve final state: Override (via pending) -> Ref
135-
// Note: We use the accumulated pending state as the source of truth for "next" state
139+
// Resolve final state from pending or fallback to refs
140+
// The pending state should contain the NEXT state after the change
136141
const nodes = pendingStateRef.current.nodes ?? designGraph.nodesRef.current;
137142
const edges = pendingStateRef.current.edges ?? designGraph.edgesRef.current;
138143

0 commit comments

Comments
 (0)