@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.
@@ -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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"}
@@ -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: Date.now(),
517
- updatedAt: Date.now(),
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
- // 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
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