@principal-ai/principal-view-react 0.13.21 → 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.
@@ -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":"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"}
1
+ {"version":3,"file":"GraphRenderer.d.ts","sourceRoot":"","sources":["../../src/components/GraphRenderer.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState, useEffect, useLayoutEffect, useCallback, useRef, useImperativeHandle, forwardRef, createContext, } from 'react';
3
3
  import { ReactFlow, Background, BackgroundVariant, Controls, MiniMap, ReactFlowProvider, useReactFlow, useUpdateNodeInternals, useViewport, applyNodeChanges, applyEdgeChanges, } from '@xyflow/react';
4
4
  import '@xyflow/react/dist/style.css';
@@ -7,6 +7,8 @@ import { useTheme } from '@principal-ade/industry-theme';
7
7
  import { CustomNode } from '../nodes/CustomNode';
8
8
  import { CustomEdge } from '../edges/CustomEdge';
9
9
  import { convertToXYFlowNodes, convertToXYFlowEdges, } from '../utils/graphConverter';
10
+ import { GraphEditProvider } from '../contexts/GraphEditContext';
11
+ import { useUndoRedo } from '../hooks/useUndoRedo';
10
12
  /**
11
13
  * Context for providing a portal target for tooltips.
12
14
  * This allows tooltips to be rendered within the graph container
@@ -84,7 +86,7 @@ const CenterIndicator = ({ color }) => {
84
86
  /**
85
87
  * Inner component that uses ReactFlow hooks
86
88
  */
87
- 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, 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, }) => {
88
90
  const { fitView, fitBounds, getNodes } = useReactFlow();
89
91
  const updateNodeInternals = useUpdateNodeInternals();
90
92
  const { theme } = useTheme();
@@ -200,6 +202,8 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
200
202
  onPendingChangesChange?.(false);
201
203
  // Clear animation state to prevent stale animations from affecting new edges
202
204
  setAnimationState({ nodeAnimations: {}, edgeAnimations: {} });
205
+ // Clear undo/redo history when props change
206
+ clearHistory();
203
207
  }
204
208
  }, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
205
209
  // When draggableNodeIds is provided, positions are managed externally
@@ -218,6 +222,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
218
222
  // and receives state_changed event updates. localEdges only used in edit mode.
219
223
  const nodes = localNodes;
220
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());
221
229
  // Helper to check if there are pending changes
222
230
  const checkHasChanges = useCallback((state) => {
223
231
  return (state.positionChanges.size > 0 ||
@@ -244,6 +252,34 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
244
252
  onEditStateChange?.(newState);
245
253
  onPendingChangesChange?.(hasChanges);
246
254
  }, [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]);
255
+ // Handler for node resize end - called from CustomNode via context
256
+ const handleNodeResizeEnd = useCallback((nodeId, dimensions) => {
257
+ if (!editable)
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
+ ]);
273
+ updateEditState((prev) => {
274
+ const newDimensions = new Map(prev.dimensionChanges);
275
+ newDimensions.set(nodeId, dimensions);
276
+ return { ...prev, dimensionChanges: newDimensions };
277
+ });
278
+ }, [editable, updateEditState, pushHistory]);
279
+ // Memoize the context value to prevent unnecessary re-renders
280
+ const graphEditContextValue = useMemo(() => ({
281
+ onNodeResizeEnd: handleNodeResizeEnd,
282
+ }), [handleNodeResizeEnd]);
247
283
  // ============================================
248
284
  // ALIGNMENT GUIDES
249
285
  // ============================================
@@ -491,6 +527,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
491
527
  // Create edge helper
492
528
  const createEdge = useCallback((from, to, type, sourceHandle, targetHandle) => {
493
529
  const edgeId = `${from}-${to}-${type}-${Date.now()}`;
530
+ const now = Date.now();
494
531
  // Add to local state with handle information
495
532
  const newEdge = {
496
533
  id: edgeId,
@@ -498,12 +535,19 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
498
535
  from,
499
536
  to,
500
537
  data: {},
501
- createdAt: Date.now(),
502
- updatedAt: Date.now(),
538
+ createdAt: now,
539
+ updatedAt: now,
503
540
  sourceHandle,
504
541
  targetHandle,
505
542
  };
506
543
  setLocalEdges((prev) => [...prev, newEdge]);
544
+ // Push to history for undo
545
+ pushHistory([
546
+ {
547
+ type: 'edgeCreate',
548
+ edge: newEdge,
549
+ },
550
+ ]);
507
551
  // Track the change
508
552
  updateEditState((prev) => ({
509
553
  ...prev,
@@ -512,7 +556,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
512
556
  { id: edgeId, from, to, type, sourceHandle, targetHandle },
513
557
  ],
514
558
  }));
515
- }, [updateEditState]);
559
+ }, [updateEditState, pushHistory]);
516
560
  // Handle new connection from drag
517
561
  const handleConnect = useCallback((connection) => {
518
562
  if (!editable || !connection.source || !connection.target)
@@ -837,6 +881,10 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
837
881
  }, [edges]);
838
882
  // Local xyflow nodes state for dragging
839
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]);
840
888
  // Sync when base node IDs change
841
889
  const prevBaseNodesKeyRef = useRef(baseNodesKey);
842
890
  useEffect(() => {
@@ -915,6 +963,165 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
915
963
  onNodeDragStopProp(node.id, node.position);
916
964
  }
917
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]);
918
1125
  // Handle node changes (drag and resize events)
919
1126
  const handleNodesChange = useCallback((changes) => {
920
1127
  const hasDraggableNodes = draggableNodeIds && draggableNodeIds.size > 0;
@@ -979,33 +1186,50 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
979
1186
  }));
980
1187
  });
981
1188
  }
982
- // Track position changes on drag end
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
983
1207
  const positionChanges = changes.filter((change) => change.type === 'position' &&
1208
+ 'id' in change &&
984
1209
  'position' in change &&
985
1210
  change.position !== undefined &&
986
1211
  'dragging' in change &&
987
1212
  change.dragging === false);
988
- // Track dimension changes (from NodeResizer)
989
- const dimensionChanges = changes.filter((change) => change.type === 'dimensions' &&
990
- 'dimensions' in change &&
991
- change.dimensions !== undefined &&
992
- 'resizing' in change &&
993
- change.resizing === false);
994
- if (dimensionChanges.length > 0) {
995
- updateEditState((prev) => {
996
- const newDimensions = new Map(prev.dimensionChanges);
997
- for (const change of dimensionChanges) {
998
- if (change.dimensions) {
999
- newDimensions.set(change.id, {
1000
- width: Math.round(change.dimensions.width),
1001
- height: Math.round(change.dimensions.height),
1002
- });
1003
- }
1004
- }
1005
- return { ...prev, dimensionChanges: newDimensions };
1006
- });
1007
- }
1213
+ // Note: Dimension changes are tracked via onResizeEnd callback in GraphEditContext,
1214
+ // not through onNodesChange (which only fires with resizing=true during drag).
1008
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);
1009
1233
  updateEditState((prev) => {
1010
1234
  const newPositions = new Map(prev.positionChanges);
1011
1235
  for (const change of positionChanges) {
@@ -1017,7 +1241,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
1017
1241
  return { ...prev, positionChanges: newPositions };
1018
1242
  });
1019
1243
  }
1020
- }, [editable, updateEditState, selectedNodeIds, draggableNodeIds]);
1244
+ }, [editable, updateEditState, selectedNodeIds, draggableNodeIds, pushHistory]);
1021
1245
  const xyflowEdgesBase = useMemo(() => {
1022
1246
  const converted = convertToXYFlowEdges(edges, configuration, violations);
1023
1247
  // Filter out edges connected to inactive or hidden nodes
@@ -1078,6 +1302,16 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
1078
1302
  setXyflowLocalEdges(xyflowEdgesBase);
1079
1303
  }
1080
1304
  }, [baseEdgesKey, xyflowEdgesBase]);
1305
+ // Set the reset visual state function for use by resetEditState
1306
+ // This resets both nodes and edges to their original state
1307
+ useEffect(() => {
1308
+ resetVisualStateRef.current = () => {
1309
+ setXyflowLocalNodes(xyflowNodesBase);
1310
+ setXyflowLocalEdges(xyflowEdgesBase);
1311
+ // Notify parent that changes have been cleared
1312
+ onPendingChangesChange?.(false);
1313
+ };
1314
+ }, [xyflowNodesBase, xyflowEdgesBase, onPendingChangesChange]);
1081
1315
  // Use local edges in edit mode, base edges otherwise
1082
1316
  const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesBase;
1083
1317
  // Handle edge changes (selection, reconnection, etc.)
@@ -1144,7 +1378,7 @@ const GraphRendererInner = ({ configuration, nodes: propNodes, edges: propEdges,
1144
1378
  // ============================================
1145
1379
  // RENDER
1146
1380
  // ============================================
1147
- return (_jsxs(_Fragment, { children: [_jsxs(ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onNodeDrag: handleNodeDrag, onNodeDragStop: handleNodeDragStop, proOptions: { hideAttribution: true }, nodesDraggable: editable || (draggableNodeIds && draggableNodeIds.size > 0), elementsSelectable: editable, selectNodesOnDrag: false, nodesConnectable: editable, edgesReconnectable: editable, reconnectRadius: 25, elevateEdgesOnSelect: true, onNodesChange: handleNodesChange, onEdgesChange: handleEdgesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, onPaneClick: onPaneClick, onSelectionChange: handleSelectionChange, panOnDrag: !editable, panOnScroll: true, zoomOnScroll: false, zoomOnPinch: true, zoomOnDoubleClick: false, selectionOnDrag: false, selectionKeyCode: null, multiSelectionKeyCode: "Shift", children: [showBackground && (_jsx(Background, { color: backgroundVariant === 'dots' ? theme.colors.border : theme.colors.textMuted, gap: backgroundGap ?? (backgroundVariant === 'dots' ? 16 : 50), size: backgroundVariant === 'dots' ? 1 : 0.5, variant: backgroundVariant === 'dots'
1381
+ return (_jsxs(GraphEditProvider, { value: graphEditContextValue, children: [_jsxs(ReactFlow, { nodes: xyflowNodes, edges: xyflowEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.1, maxZoom: 4, defaultEdgeOptions: { type: 'custom' }, onEdgeClick: onEdgeClick, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onNodeDrag: handleNodeDrag, onNodeDragStop: handleNodeDragStop, proOptions: { hideAttribution: true }, nodesDraggable: editable || (draggableNodeIds && draggableNodeIds.size > 0), elementsSelectable: editable, selectNodesOnDrag: false, nodesConnectable: editable, edgesReconnectable: editable, reconnectRadius: 25, elevateEdgesOnSelect: true, onNodesChange: handleNodesChange, onEdgesChange: handleEdgesChange, onConnect: handleConnect, onReconnectStart: handleReconnectStart, onReconnect: handleReconnect, onReconnectEnd: handleReconnectEnd, onPaneClick: onPaneClick, onSelectionChange: handleSelectionChange, panOnDrag: !editable, panOnScroll: true, zoomOnScroll: false, zoomOnPinch: true, zoomOnDoubleClick: false, selectionOnDrag: false, selectionKeyCode: null, multiSelectionKeyCode: "Shift", children: [showBackground && (_jsx(Background, { color: backgroundVariant === 'dots' ? theme.colors.border : theme.colors.textMuted, gap: backgroundGap ?? (backgroundVariant === 'dots' ? 16 : 50), size: backgroundVariant === 'dots' ? 1 : 0.5, variant: backgroundVariant === 'dots'
1148
1382
  ? BackgroundVariant.Dots
1149
1383
  : backgroundVariant === 'lines'
1150
1384
  ? BackgroundVariant.Lines
@@ -1369,6 +1603,12 @@ export const GraphRenderer = forwardRef((props, ref) => {
1369
1603
  }
1370
1604
  // Internal edit state ref - must be before any conditional returns
1371
1605
  const editStateRef = useRef(createEmptyEditState());
1606
+ // Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
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);
1372
1612
  // Expose imperative handle - must be before any conditional returns
1373
1613
  useImperativeHandle(ref, () => ({
1374
1614
  getPendingChanges: () => {
@@ -1405,6 +1645,10 @@ export const GraphRenderer = forwardRef((props, ref) => {
1405
1645
  },
1406
1646
  resetEditState: () => {
1407
1647
  editStateRef.current = createEmptyEditState();
1648
+ // Also reset visual state (node positions/dimensions) if available
1649
+ resetVisualStateRef.current?.();
1650
+ // Clear undo/redo history
1651
+ clearHistory();
1408
1652
  },
1409
1653
  hasUnsavedChanges: () => {
1410
1654
  const state = editStateRef.current;
@@ -1415,7 +1659,11 @@ export const GraphRenderer = forwardRef((props, ref) => {
1415
1659
  state.createdEdges.length > 0 ||
1416
1660
  state.deletedEdges.length > 0);
1417
1661
  },
1418
- }), []);
1662
+ canUndo: () => canUndoState,
1663
+ canRedo: () => canRedoState,
1664
+ undo: () => undoRedoFunctionsRef.current?.applyUndo(),
1665
+ redo: () => undoRedoFunctionsRef.current?.applyRedo(),
1666
+ }), [canUndoState, canRedoState, clearHistory]);
1419
1667
  // Validate we have required data
1420
1668
  if (!canvasData) {
1421
1669
  return (_jsx("div", { className: className, style: {
@@ -1431,7 +1679,7 @@ export const GraphRenderer = forwardRef((props, ref) => {
1431
1679
  const { configuration, nodes, edges } = canvasData;
1432
1680
  // Extract only the props that inner component needs
1433
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;
1434
- 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, 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 }) }) }) }));
1435
1683
  });
1436
1684
  GraphRenderer.displayName = 'GraphRenderer';
1437
1685
  //# sourceMappingURL=GraphRenderer.js.map