@principal-ai/principal-view-react 0.13.22 → 0.13.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/GraphRenderer.d.ts +8 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +246 -9
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/hooks/useUndoRedo.d.ts +75 -0
- package/dist/hooks/useUndoRedo.d.ts.map +1 -0
- package/dist/hooks/useUndoRedo.js +70 -0
- package/dist/hooks/useUndoRedo.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +7 -2
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +327 -7
- package/src/hooks/useUndoRedo.ts +138 -0
- package/src/nodes/CustomNode.tsx +16 -1
|
@@ -64,6 +64,14 @@ export interface GraphRendererHandle {
|
|
|
64
64
|
resetEditState: () => void;
|
|
65
65
|
/** Check if there are unsaved changes */
|
|
66
66
|
hasUnsavedChanges: () => boolean;
|
|
67
|
+
/** Whether there are changes that can be undone */
|
|
68
|
+
canUndo: () => boolean;
|
|
69
|
+
/** Whether there are changes that can be redone */
|
|
70
|
+
canRedo: () => boolean;
|
|
71
|
+
/** Undo the last change */
|
|
72
|
+
undo: () => void;
|
|
73
|
+
/** Redo the last undone change */
|
|
74
|
+
redo: () => void;
|
|
67
75
|
}
|
|
68
76
|
/** Base props shared by all render modes */
|
|
69
77
|
interface GraphRendererBaseProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAUN,MAAM,OAAO,CAAC;AAqBf,OAAO,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAIV,SAAS,EACT,UAAU,EAIV,cAAc,EACd,gBAAgB,EACjB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ import { CustomNode } from '../nodes/CustomNode';
|
|
|
8
8
|
import { CustomEdge } from '../edges/CustomEdge';
|
|
9
9
|
import { convertToXYFlowNodes, convertToXYFlowEdges, } from '../utils/graphConverter';
|
|
10
10
|
import { GraphEditProvider } from '../contexts/GraphEditContext';
|
|
11
|
+
import { useUndoRedo } from '../hooks/useUndoRedo';
|
|
11
12
|
/**
|
|
12
13
|
* Context for providing a portal target for tooltips.
|
|
13
14
|
* This allows tooltips to be rendered within the graph container
|
|
@@ -85,7 +86,7 @@ const CenterIndicator = ({ color }) => {
|
|
|
85
86
|
/**
|
|
86
87
|
* Inner component that uses ReactFlow hooks
|
|
87
88
|
*/
|
|
88
|
-
const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'lines', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, resetVisualStateRef, onNodeClick: onNodeClickProp, fitViewToNodeIds, fitViewPadding = 0.2, draggableNodeIds, onNodeDragStop: onNodeDragStopProp, onCopy, }) => {
|
|
89
|
+
const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges, violations = [], configName: _configName, showMinimap = false, showControls = true, showBackground = true, backgroundVariant = 'lines', backgroundGap, showCenterIndicator = false, showTooltips = true, fitViewDuration = 200, highlightedNodeId, activeNodeIds, events = [], onEventProcessed, editable = false, onPendingChangesChange, onEditStateChange, editStateRef, resetVisualStateRef, undoRedoFunctionsRef, pushHistory, clearHistory, undoFromStack, redoFromStack, onNodeClick: onNodeClickProp, fitViewToNodeIds, fitViewPadding = 0.2, draggableNodeIds, onNodeDragStop: onNodeDragStopProp, onCopy, }) => {
|
|
89
90
|
const { fitView, fitBounds, getNodes } = useReactFlow();
|
|
90
91
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
91
92
|
const { theme } = useTheme();
|
|
@@ -201,6 +202,8 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
201
202
|
onPendingChangesChange?.(false);
|
|
202
203
|
// Clear animation state to prevent stale animations from affecting new edges
|
|
203
204
|
setAnimationState({ nodeAnimations: {}, edgeAnimations: {} });
|
|
205
|
+
// Clear undo/redo history when props change
|
|
206
|
+
clearHistory();
|
|
204
207
|
}
|
|
205
208
|
}, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
|
|
206
209
|
// When draggableNodeIds is provided, positions are managed externally
|
|
@@ -219,6 +222,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
219
222
|
// and receives state_changed event updates. localEdges only used in edit mode.
|
|
220
223
|
const nodes = localNodes;
|
|
221
224
|
const edges = editable ? localEdges : propEdges;
|
|
225
|
+
// Ref to track current xyflow nodes for undo/redo (set later via useEffect)
|
|
226
|
+
const xyflowNodesRef = useRef([]);
|
|
227
|
+
// Ref to capture node positions at drag start (for accurate undo "before" values)
|
|
228
|
+
const dragStartPositionsRef = useRef(new Map());
|
|
222
229
|
// Helper to check if there are pending changes
|
|
223
230
|
const checkHasChanges = useCallback((state) => {
|
|
224
231
|
return (state.positionChanges.size > 0 ||
|
|
@@ -249,12 +256,26 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
249
256
|
const handleNodeResizeEnd = useCallback((nodeId, dimensions) => {
|
|
250
257
|
if (!editable)
|
|
251
258
|
return;
|
|
259
|
+
// Capture before dimensions for undo
|
|
260
|
+
const currentNode = xyflowNodesRef.current.find((n) => n.id === nodeId);
|
|
261
|
+
const beforeDimensions = currentNode
|
|
262
|
+
? { width: currentNode.width ?? 0, height: currentNode.height ?? 0 }
|
|
263
|
+
: { width: 0, height: 0 };
|
|
264
|
+
// Push to history
|
|
265
|
+
pushHistory([
|
|
266
|
+
{
|
|
267
|
+
type: 'dimension',
|
|
268
|
+
nodeId,
|
|
269
|
+
before: beforeDimensions,
|
|
270
|
+
after: dimensions,
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
252
273
|
updateEditState((prev) => {
|
|
253
274
|
const newDimensions = new Map(prev.dimensionChanges);
|
|
254
275
|
newDimensions.set(nodeId, dimensions);
|
|
255
276
|
return { ...prev, dimensionChanges: newDimensions };
|
|
256
277
|
});
|
|
257
|
-
}, [editable, updateEditState]);
|
|
278
|
+
}, [editable, updateEditState, pushHistory]);
|
|
258
279
|
// Memoize the context value to prevent unnecessary re-renders
|
|
259
280
|
const graphEditContextValue = useMemo(() => ({
|
|
260
281
|
onNodeResizeEnd: handleNodeResizeEnd,
|
|
@@ -506,6 +527,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
506
527
|
// Create edge helper
|
|
507
528
|
const createEdge = useCallback((from, to, type, sourceHandle, targetHandle) => {
|
|
508
529
|
const edgeId = `${from}-${to}-${type}-${Date.now()}`;
|
|
530
|
+
const now = Date.now();
|
|
509
531
|
// Add to local state with handle information
|
|
510
532
|
const newEdge = {
|
|
511
533
|
id: edgeId,
|
|
@@ -513,12 +535,19 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
513
535
|
from,
|
|
514
536
|
to,
|
|
515
537
|
data: {},
|
|
516
|
-
createdAt:
|
|
517
|
-
updatedAt:
|
|
538
|
+
createdAt: now,
|
|
539
|
+
updatedAt: now,
|
|
518
540
|
sourceHandle,
|
|
519
541
|
targetHandle,
|
|
520
542
|
};
|
|
521
543
|
setLocalEdges((prev) => [...prev, newEdge]);
|
|
544
|
+
// Push to history for undo
|
|
545
|
+
pushHistory([
|
|
546
|
+
{
|
|
547
|
+
type: 'edgeCreate',
|
|
548
|
+
edge: newEdge,
|
|
549
|
+
},
|
|
550
|
+
]);
|
|
522
551
|
// Track the change
|
|
523
552
|
updateEditState((prev) => ({
|
|
524
553
|
...prev,
|
|
@@ -527,7 +556,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
527
556
|
{ id: edgeId, from, to, type, sourceHandle, targetHandle },
|
|
528
557
|
],
|
|
529
558
|
}));
|
|
530
|
-
}, [updateEditState]);
|
|
559
|
+
}, [updateEditState, pushHistory]);
|
|
531
560
|
// Handle new connection from drag
|
|
532
561
|
const handleConnect = useCallback((connection) => {
|
|
533
562
|
if (!editable || !connection.source || !connection.target)
|
|
@@ -852,6 +881,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
852
881
|
}, [edges]);
|
|
853
882
|
// Local xyflow nodes state for dragging
|
|
854
883
|
const [xyflowLocalNodes, setXyflowLocalNodes] = useState(xyflowNodesBase);
|
|
884
|
+
// Keep xyflowNodesRef in sync for undo/redo access to current node state
|
|
885
|
+
useEffect(() => {
|
|
886
|
+
xyflowNodesRef.current = xyflowLocalNodes;
|
|
887
|
+
}, [xyflowLocalNodes]);
|
|
855
888
|
// Sync when base node IDs change
|
|
856
889
|
const prevBaseNodesKeyRef = useRef(baseNodesKey);
|
|
857
890
|
useEffect(() => {
|
|
@@ -930,6 +963,165 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
930
963
|
onNodeDragStopProp(node.id, node.position);
|
|
931
964
|
}
|
|
932
965
|
}, [onNodeDragStopProp]);
|
|
966
|
+
// ============================================
|
|
967
|
+
// UNDO/REDO APPLY FUNCTIONS
|
|
968
|
+
// ============================================
|
|
969
|
+
// Apply undo - reverse the effects of history entries
|
|
970
|
+
const applyUndo = useCallback(() => {
|
|
971
|
+
const batch = undoFromStack();
|
|
972
|
+
if (!batch)
|
|
973
|
+
return;
|
|
974
|
+
// Process entries in reverse order for undo
|
|
975
|
+
for (const entry of batch.slice().reverse()) {
|
|
976
|
+
switch (entry.type) {
|
|
977
|
+
case 'position':
|
|
978
|
+
// Restore previous position
|
|
979
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
980
|
+
? { ...n, position: entry.before }
|
|
981
|
+
: n));
|
|
982
|
+
// Update edit state
|
|
983
|
+
updateEditState((prev) => {
|
|
984
|
+
const newPositions = new Map(prev.positionChanges);
|
|
985
|
+
newPositions.set(entry.nodeId, entry.before);
|
|
986
|
+
return { ...prev, positionChanges: newPositions };
|
|
987
|
+
});
|
|
988
|
+
break;
|
|
989
|
+
case 'dimension':
|
|
990
|
+
// Restore previous dimensions
|
|
991
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
992
|
+
? { ...n, width: entry.before.width, height: entry.before.height }
|
|
993
|
+
: n));
|
|
994
|
+
// Update edit state
|
|
995
|
+
updateEditState((prev) => {
|
|
996
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
997
|
+
newDimensions.set(entry.nodeId, entry.before);
|
|
998
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
999
|
+
});
|
|
1000
|
+
break;
|
|
1001
|
+
case 'edgeCreate':
|
|
1002
|
+
// Remove the created edge
|
|
1003
|
+
setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
|
|
1004
|
+
// Update edit state
|
|
1005
|
+
updateEditState((prev) => ({
|
|
1006
|
+
...prev,
|
|
1007
|
+
createdEdges: prev.createdEdges.filter((e) => e.id !== entry.edge.id),
|
|
1008
|
+
}));
|
|
1009
|
+
break;
|
|
1010
|
+
case 'edgeDelete':
|
|
1011
|
+
// Restore the deleted edge
|
|
1012
|
+
setLocalEdges((edges) => [...edges, entry.edge]);
|
|
1013
|
+
// Update edit state
|
|
1014
|
+
updateEditState((prev) => ({
|
|
1015
|
+
...prev,
|
|
1016
|
+
deletedEdges: prev.deletedEdges.filter((e) => e.id !== entry.edge.id),
|
|
1017
|
+
}));
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}, [undoFromStack, updateEditState]);
|
|
1022
|
+
// Apply redo - re-apply the effects of history entries
|
|
1023
|
+
const applyRedo = useCallback(() => {
|
|
1024
|
+
const batch = redoFromStack();
|
|
1025
|
+
if (!batch)
|
|
1026
|
+
return;
|
|
1027
|
+
// Process entries in original order for redo
|
|
1028
|
+
for (const entry of batch) {
|
|
1029
|
+
switch (entry.type) {
|
|
1030
|
+
case 'position':
|
|
1031
|
+
// Apply new position
|
|
1032
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
1033
|
+
? { ...n, position: entry.after }
|
|
1034
|
+
: n));
|
|
1035
|
+
// Update edit state
|
|
1036
|
+
updateEditState((prev) => {
|
|
1037
|
+
const newPositions = new Map(prev.positionChanges);
|
|
1038
|
+
newPositions.set(entry.nodeId, entry.after);
|
|
1039
|
+
return { ...prev, positionChanges: newPositions };
|
|
1040
|
+
});
|
|
1041
|
+
break;
|
|
1042
|
+
case 'dimension':
|
|
1043
|
+
// Apply new dimensions
|
|
1044
|
+
setXyflowLocalNodes((nodes) => nodes.map((n) => n.id === entry.nodeId
|
|
1045
|
+
? { ...n, width: entry.after.width, height: entry.after.height }
|
|
1046
|
+
: n));
|
|
1047
|
+
// Update edit state
|
|
1048
|
+
updateEditState((prev) => {
|
|
1049
|
+
const newDimensions = new Map(prev.dimensionChanges);
|
|
1050
|
+
newDimensions.set(entry.nodeId, entry.after);
|
|
1051
|
+
return { ...prev, dimensionChanges: newDimensions };
|
|
1052
|
+
});
|
|
1053
|
+
break;
|
|
1054
|
+
case 'edgeCreate':
|
|
1055
|
+
// Re-create the edge
|
|
1056
|
+
setLocalEdges((edges) => [...edges, entry.edge]);
|
|
1057
|
+
// Update edit state
|
|
1058
|
+
updateEditState((prev) => ({
|
|
1059
|
+
...prev,
|
|
1060
|
+
createdEdges: [
|
|
1061
|
+
...prev.createdEdges,
|
|
1062
|
+
{
|
|
1063
|
+
id: entry.edge.id,
|
|
1064
|
+
from: entry.edge.from,
|
|
1065
|
+
to: entry.edge.to,
|
|
1066
|
+
type: entry.edge.type,
|
|
1067
|
+
sourceHandle: entry.edge.sourceHandle,
|
|
1068
|
+
targetHandle: entry.edge.targetHandle,
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
}));
|
|
1072
|
+
break;
|
|
1073
|
+
case 'edgeDelete':
|
|
1074
|
+
// Re-delete the edge
|
|
1075
|
+
setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
|
|
1076
|
+
// Update edit state
|
|
1077
|
+
updateEditState((prev) => ({
|
|
1078
|
+
...prev,
|
|
1079
|
+
deletedEdges: [
|
|
1080
|
+
...prev.deletedEdges,
|
|
1081
|
+
{
|
|
1082
|
+
id: entry.edge.id,
|
|
1083
|
+
from: entry.edge.from,
|
|
1084
|
+
to: entry.edge.to,
|
|
1085
|
+
type: entry.edge.type,
|
|
1086
|
+
},
|
|
1087
|
+
],
|
|
1088
|
+
}));
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}, [redoFromStack, updateEditState]);
|
|
1093
|
+
// Keyboard shortcuts for undo/redo
|
|
1094
|
+
useEffect(() => {
|
|
1095
|
+
if (!editable)
|
|
1096
|
+
return;
|
|
1097
|
+
const handleKeyDown = (e) => {
|
|
1098
|
+
// Cmd+Z (Mac) or Ctrl+Z (Windows/Linux) for undo
|
|
1099
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
|
|
1100
|
+
e.preventDefault();
|
|
1101
|
+
if (e.shiftKey) {
|
|
1102
|
+
// Cmd+Shift+Z for redo
|
|
1103
|
+
applyRedo();
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
applyUndo();
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Ctrl+Y for redo (Windows)
|
|
1110
|
+
if (e.ctrlKey && e.key === 'y') {
|
|
1111
|
+
e.preventDefault();
|
|
1112
|
+
applyRedo();
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1116
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1117
|
+
}, [editable, applyUndo, applyRedo]);
|
|
1118
|
+
// Set undo/redo functions in ref for outer component access
|
|
1119
|
+
useEffect(() => {
|
|
1120
|
+
undoRedoFunctionsRef.current = {
|
|
1121
|
+
applyUndo,
|
|
1122
|
+
applyRedo,
|
|
1123
|
+
};
|
|
1124
|
+
}, [applyUndo, applyRedo, undoRedoFunctionsRef]);
|
|
933
1125
|
// Handle node changes (drag and resize events)
|
|
934
1126
|
const handleNodesChange = useCallback((changes) => {
|
|
935
1127
|
const hasDraggableNodes = draggableNodeIds && draggableNodeIds.size > 0;
|
|
@@ -994,8 +1186,26 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
994
1186
|
}));
|
|
995
1187
|
});
|
|
996
1188
|
}
|
|
997
|
-
//
|
|
1189
|
+
// Capture positions at drag START for accurate undo "before" values
|
|
1190
|
+
const dragStartChanges = changes.filter((change) => change.type === 'position' &&
|
|
1191
|
+
'id' in change &&
|
|
1192
|
+
'dragging' in change &&
|
|
1193
|
+
change.dragging === true);
|
|
1194
|
+
// For each node that just started dragging, capture its current position
|
|
1195
|
+
for (const change of dragStartChanges) {
|
|
1196
|
+
if (!dragStartPositionsRef.current.has(change.id)) {
|
|
1197
|
+
const currentNode = xyflowNodesRef.current.find((n) => n.id === change.id);
|
|
1198
|
+
if (currentNode) {
|
|
1199
|
+
dragStartPositionsRef.current.set(change.id, {
|
|
1200
|
+
x: currentNode.position.x,
|
|
1201
|
+
y: currentNode.position.y,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// Track position changes on drag END
|
|
998
1207
|
const positionChanges = changes.filter((change) => change.type === 'position' &&
|
|
1208
|
+
'id' in change &&
|
|
999
1209
|
'position' in change &&
|
|
1000
1210
|
change.position !== undefined &&
|
|
1001
1211
|
'dragging' in change &&
|
|
@@ -1003,6 +1213,23 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
1003
1213
|
// Note: Dimension changes are tracked via onResizeEnd callback in GraphEditContext,
|
|
1004
1214
|
// not through onNodesChange (which only fires with resizing=true during drag).
|
|
1005
1215
|
if (positionChanges.length > 0) {
|
|
1216
|
+
// Use positions captured at drag start for accurate undo
|
|
1217
|
+
const historyEntries = [];
|
|
1218
|
+
for (const change of positionChanges) {
|
|
1219
|
+
const beforePos = dragStartPositionsRef.current.get(change.id) ?? { x: 0, y: 0 };
|
|
1220
|
+
historyEntries.push({
|
|
1221
|
+
type: 'position',
|
|
1222
|
+
nodeId: change.id,
|
|
1223
|
+
before: beforePos,
|
|
1224
|
+
after: {
|
|
1225
|
+
x: Math.round(change.position.x),
|
|
1226
|
+
y: Math.round(change.position.y),
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
// Clear the drag start position for this node
|
|
1230
|
+
dragStartPositionsRef.current.delete(change.id);
|
|
1231
|
+
}
|
|
1232
|
+
pushHistory(historyEntries);
|
|
1006
1233
|
updateEditState((prev) => {
|
|
1007
1234
|
const newPositions = new Map(prev.positionChanges);
|
|
1008
1235
|
for (const change of positionChanges) {
|
|
@@ -1014,7 +1241,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
|
|
|
1014
1241
|
return { ...prev, positionChanges: newPositions };
|
|
1015
1242
|
});
|
|
1016
1243
|
}
|
|
1017
|
-
}, [editable, updateEditState, selectedNodeIds, draggableNodeIds]);
|
|
1244
|
+
}, [editable, updateEditState, selectedNodeIds, draggableNodeIds, pushHistory]);
|
|
1018
1245
|
const xyflowEdgesBase = useMemo(() => {
|
|
1019
1246
|
const converted = convertToXYFlowEdges(edges, configuration, violations);
|
|
1020
1247
|
// Filter out edges connected to inactive or hidden nodes
|
|
@@ -1378,6 +1605,10 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1378
1605
|
const editStateRef = useRef(createEmptyEditState());
|
|
1379
1606
|
// Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
|
|
1380
1607
|
const resetVisualStateRef = useRef(null);
|
|
1608
|
+
// Undo/redo management
|
|
1609
|
+
const { canUndo: canUndoState, canRedo: canRedoState, undo: undoFromStack, redo: redoFromStack, pushHistory, clearHistory, } = useUndoRedo();
|
|
1610
|
+
// Ref to hold undo/redo apply functions - will be set by inner component
|
|
1611
|
+
const undoRedoFunctionsRef = useRef(null);
|
|
1381
1612
|
// Expose imperative handle - must be before any conditional returns
|
|
1382
1613
|
useImperativeHandle(ref, () => ({
|
|
1383
1614
|
getPendingChanges: () => {
|
|
@@ -1416,6 +1647,8 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1416
1647
|
editStateRef.current = createEmptyEditState();
|
|
1417
1648
|
// Also reset visual state (node positions/dimensions) if available
|
|
1418
1649
|
resetVisualStateRef.current?.();
|
|
1650
|
+
// Clear undo/redo history
|
|
1651
|
+
clearHistory();
|
|
1419
1652
|
},
|
|
1420
1653
|
hasUnsavedChanges: () => {
|
|
1421
1654
|
const state = editStateRef.current;
|
|
@@ -1426,7 +1659,11 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1426
1659
|
state.createdEdges.length > 0 ||
|
|
1427
1660
|
state.deletedEdges.length > 0);
|
|
1428
1661
|
},
|
|
1429
|
-
|
|
1662
|
+
canUndo: () => canUndoState,
|
|
1663
|
+
canRedo: () => canRedoState,
|
|
1664
|
+
undo: () => undoRedoFunctionsRef.current?.applyUndo(),
|
|
1665
|
+
redo: () => undoRedoFunctionsRef.current?.applyRedo(),
|
|
1666
|
+
}), [canUndoState, canRedoState, clearHistory]);
|
|
1430
1667
|
// Validate we have required data
|
|
1431
1668
|
if (!canvasData) {
|
|
1432
1669
|
return (_jsx("div", { className: className, style: {
|
|
@@ -1442,7 +1679,7 @@ export const GraphRenderer = forwardRef((props, ref) => {
|
|
|
1442
1679
|
const { configuration, nodes, edges } = canvasData;
|
|
1443
1680
|
// Extract only the props that inner component needs
|
|
1444
1681
|
const { violations, configName, showMinimap, showControls, showBackground, backgroundVariant, backgroundGap, showCenterIndicator, showTooltips, fitViewDuration, highlightedNodeId, activeNodeIds, events, onEventProcessed, editable, onPendingChangesChange, onNodeClick, fitViewToNodeIds, fitViewPadding, draggableNodeIds, onNodeDragStop, onCopy, } = props;
|
|
1445
|
-
return (_jsx("div", { ref: containerRef, className: className, style: { width, height, position: 'relative' }, children: _jsx(TooltipPortalContext.Provider, { value: portalTarget, children: _jsx(ReactFlowProvider, { children: _jsx(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, backgroundVariant: backgroundVariant, backgroundGap: backgroundGap, showCenterIndicator: showCenterIndicator, showTooltips: showTooltips, fitViewDuration: fitViewDuration, highlightedNodeId: highlightedNodeId, activeNodeIds: activeNodeIds, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef, resetVisualStateRef: resetVisualStateRef, onNodeClick: onNodeClick, fitViewToNodeIds: fitViewToNodeIds, fitViewPadding: fitViewPadding, draggableNodeIds: draggableNodeIds, onNodeDragStop: onNodeDragStop, onCopy: onCopy }) }) }) }));
|
|
1682
|
+
return (_jsx("div", { ref: containerRef, className: className, style: { width, height, position: 'relative' }, children: _jsx(TooltipPortalContext.Provider, { value: portalTarget, children: _jsx(ReactFlowProvider, { children: _jsx(GraphRendererInner, { configuration: configuration, nodes: nodes, edges: edges, violations: violations, configName: configName, showMinimap: showMinimap, showControls: showControls, showBackground: showBackground, backgroundVariant: backgroundVariant, backgroundGap: backgroundGap, showCenterIndicator: showCenterIndicator, showTooltips: showTooltips, fitViewDuration: fitViewDuration, highlightedNodeId: highlightedNodeId, activeNodeIds: activeNodeIds, events: events, onEventProcessed: onEventProcessed, editable: editable, onPendingChangesChange: onPendingChangesChange, editStateRef: editStateRef, resetVisualStateRef: resetVisualStateRef, undoRedoFunctionsRef: undoRedoFunctionsRef, pushHistory: pushHistory, clearHistory: clearHistory, undoFromStack: undoFromStack, redoFromStack: redoFromStack, onNodeClick: onNodeClick, fitViewToNodeIds: fitViewToNodeIds, fitViewPadding: fitViewPadding, draggableNodeIds: draggableNodeIds, onNodeDragStop: onNodeDragStop, onCopy: onCopy }) }) }) }));
|
|
1446
1683
|
});
|
|
1447
1684
|
GraphRenderer.displayName = 'GraphRenderer';
|
|
1448
1685
|
//# sourceMappingURL=GraphRenderer.js.map
|