@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.
@@ -52,6 +52,8 @@ import {
52
52
  convertToXYFlowNodes,
53
53
  convertToXYFlowEdges,
54
54
  } from '../utils/graphConverter';
55
+ import { GraphEditProvider } from '../contexts/GraphEditContext';
56
+ import { useUndoRedo, type HistoryEntry } from '../hooks/useUndoRedo';
55
57
 
56
58
  /**
57
59
  * Context for providing a portal target for tooltips.
@@ -107,6 +109,14 @@ export interface GraphRendererHandle {
107
109
  resetEditState: () => void;
108
110
  /** Check if there are unsaved changes */
109
111
  hasUnsavedChanges: () => boolean;
112
+ /** Whether there are changes that can be undone */
113
+ canUndo: () => boolean;
114
+ /** Whether there are changes that can be redone */
115
+ canRedo: () => boolean;
116
+ /** Undo the last change */
117
+ undo: () => void;
118
+ /** Redo the last undone change */
119
+ redo: () => void;
110
120
  }
111
121
 
112
122
  /** Base props shared by all render modes */
@@ -443,6 +453,12 @@ const CenterIndicator: React.FC<{ color: string }> = ({ color }) => {
443
453
  };
444
454
 
445
455
  /** Inner component receives normalized legacy format */
456
+ /** Ref for undo/redo functions that inner component will populate */
457
+ interface UndoRedoFunctionsRef {
458
+ applyUndo: () => void;
459
+ applyRedo: () => void;
460
+ }
461
+
446
462
  interface GraphRendererInnerProps {
447
463
  configuration: GraphConfiguration;
448
464
  nodes: NodeState[];
@@ -465,6 +481,12 @@ interface GraphRendererInnerProps {
465
481
  onPendingChangesChange?: (hasChanges: boolean) => void;
466
482
  onEditStateChange?: (editState: EditState) => void;
467
483
  editStateRef: React.MutableRefObject<EditState>;
484
+ resetVisualStateRef: React.MutableRefObject<(() => void) | null>;
485
+ undoRedoFunctionsRef: React.MutableRefObject<UndoRedoFunctionsRef | null>;
486
+ pushHistory: (entries: HistoryEntry[]) => void;
487
+ clearHistory: () => void;
488
+ undoFromStack: () => HistoryEntry[] | null;
489
+ redoFromStack: () => HistoryEntry[] | null;
468
490
  onNodeClick?: (nodeId: string, event: React.MouseEvent) => void;
469
491
  fitViewToNodeIds?: string[] | null;
470
492
  fitViewPadding?: number;
@@ -498,6 +520,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
498
520
  onPendingChangesChange,
499
521
  onEditStateChange,
500
522
  editStateRef,
523
+ resetVisualStateRef,
524
+ undoRedoFunctionsRef,
525
+ pushHistory,
526
+ clearHistory,
527
+ undoFromStack,
528
+ redoFromStack,
501
529
  onNodeClick: onNodeClickProp,
502
530
  fitViewToNodeIds,
503
531
  fitViewPadding = 0.2,
@@ -650,6 +678,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
650
678
  onPendingChangesChange?.(false);
651
679
  // Clear animation state to prevent stale animations from affecting new edges
652
680
  setAnimationState({ nodeAnimations: {}, edgeAnimations: {} });
681
+ // Clear undo/redo history when props change
682
+ clearHistory();
653
683
  }
654
684
  }, [propNodes, propEdges, editStateRef, onEditStateChange, onPendingChangesChange]);
655
685
 
@@ -669,6 +699,12 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
669
699
  const nodes = localNodes;
670
700
  const edges = editable ? localEdges : propEdges;
671
701
 
702
+ // Ref to track current xyflow nodes for undo/redo (set later via useEffect)
703
+ const xyflowNodesRef = useRef<Node<CustomNodeData>[]>([]);
704
+
705
+ // Ref to capture node positions at drag start (for accurate undo "before" values)
706
+ const dragStartPositionsRef = useRef<Map<string, { x: number; y: number }>>(new Map());
707
+
672
708
  // Helper to check if there are pending changes
673
709
  const checkHasChanges = useCallback((state: EditState): boolean => {
674
710
  return (
@@ -704,6 +740,44 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
704
740
  [editStateRef, onEditStateChange, onPendingChangesChange, checkHasChanges]
705
741
  );
706
742
 
743
+ // Handler for node resize end - called from CustomNode via context
744
+ const handleNodeResizeEnd = useCallback(
745
+ (nodeId: string, dimensions: { width: number; height: number }) => {
746
+ if (!editable) return;
747
+
748
+ // Capture before dimensions for undo
749
+ const currentNode = xyflowNodesRef.current.find((n) => n.id === nodeId);
750
+ const beforeDimensions = currentNode
751
+ ? { width: currentNode.width ?? 0, height: currentNode.height ?? 0 }
752
+ : { width: 0, height: 0 };
753
+
754
+ // Push to history
755
+ pushHistory([
756
+ {
757
+ type: 'dimension',
758
+ nodeId,
759
+ before: beforeDimensions,
760
+ after: dimensions,
761
+ },
762
+ ]);
763
+
764
+ updateEditState((prev) => {
765
+ const newDimensions = new Map(prev.dimensionChanges);
766
+ newDimensions.set(nodeId, dimensions);
767
+ return { ...prev, dimensionChanges: newDimensions };
768
+ });
769
+ },
770
+ [editable, updateEditState, pushHistory]
771
+ );
772
+
773
+ // Memoize the context value to prevent unnecessary re-renders
774
+ const graphEditContextValue = useMemo(
775
+ () => ({
776
+ onNodeResizeEnd: handleNodeResizeEnd,
777
+ }),
778
+ [handleNodeResizeEnd]
779
+ );
780
+
707
781
  // ============================================
708
782
  // ALIGNMENT GUIDES
709
783
  // ============================================
@@ -1008,6 +1082,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1008
1082
  const createEdge = useCallback(
1009
1083
  (from: string, to: string, type: string, sourceHandle?: string, targetHandle?: string) => {
1010
1084
  const edgeId = `${from}-${to}-${type}-${Date.now()}`;
1085
+ const now = Date.now();
1011
1086
 
1012
1087
  // Add to local state with handle information
1013
1088
  const newEdge: EdgeState & { sourceHandle?: string; targetHandle?: string } = {
@@ -1016,13 +1091,21 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1016
1091
  from,
1017
1092
  to,
1018
1093
  data: {},
1019
- createdAt: Date.now(),
1020
- updatedAt: Date.now(),
1094
+ createdAt: now,
1095
+ updatedAt: now,
1021
1096
  sourceHandle,
1022
1097
  targetHandle,
1023
1098
  };
1024
1099
  setLocalEdges((prev) => [...prev, newEdge]);
1025
1100
 
1101
+ // Push to history for undo
1102
+ pushHistory([
1103
+ {
1104
+ type: 'edgeCreate',
1105
+ edge: newEdge,
1106
+ },
1107
+ ]);
1108
+
1026
1109
  // Track the change
1027
1110
  updateEditState((prev) => ({
1028
1111
  ...prev,
@@ -1032,7 +1115,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1032
1115
  ],
1033
1116
  }));
1034
1117
  },
1035
- [updateEditState]
1118
+ [updateEditState, pushHistory]
1036
1119
  );
1037
1120
 
1038
1121
  // Handle new connection from drag
@@ -1438,6 +1521,11 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1438
1521
  // Local xyflow nodes state for dragging
1439
1522
  const [xyflowLocalNodes, setXyflowLocalNodes] = useState<Node<CustomNodeData>[]>(xyflowNodesBase);
1440
1523
 
1524
+ // Keep xyflowNodesRef in sync for undo/redo access to current node state
1525
+ useEffect(() => {
1526
+ xyflowNodesRef.current = xyflowLocalNodes;
1527
+ }, [xyflowLocalNodes]);
1528
+
1441
1529
  // Sync when base node IDs change
1442
1530
  const prevBaseNodesKeyRef = useRef(baseNodesKey);
1443
1531
  useEffect(() => {
@@ -1505,6 +1593,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1505
1593
  }
1506
1594
  }, [hasDraggableNodes, xyflowNodesBase, draggableNodeIds, updateNodeInternals]);
1507
1595
 
1596
+
1508
1597
  // Handle node drag to show alignment guides
1509
1598
  const handleNodeDrag = useCallback(
1510
1599
  (_event: React.MouseEvent, node: Node) => {
@@ -1534,6 +1623,192 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1534
1623
  [onNodeDragStopProp]
1535
1624
  );
1536
1625
 
1626
+ // ============================================
1627
+ // UNDO/REDO APPLY FUNCTIONS
1628
+ // ============================================
1629
+
1630
+ // Apply undo - reverse the effects of history entries
1631
+ const applyUndo = useCallback(() => {
1632
+ const batch = undoFromStack();
1633
+ if (!batch) return;
1634
+
1635
+ // Process entries in reverse order for undo
1636
+ for (const entry of batch.slice().reverse()) {
1637
+ switch (entry.type) {
1638
+ case 'position':
1639
+ // Restore previous position
1640
+ setXyflowLocalNodes((nodes) =>
1641
+ nodes.map((n) =>
1642
+ n.id === entry.nodeId
1643
+ ? { ...n, position: entry.before }
1644
+ : n
1645
+ )
1646
+ );
1647
+ // Update edit state
1648
+ updateEditState((prev) => {
1649
+ const newPositions = new Map(prev.positionChanges);
1650
+ newPositions.set(entry.nodeId, entry.before);
1651
+ return { ...prev, positionChanges: newPositions };
1652
+ });
1653
+ break;
1654
+
1655
+ case 'dimension':
1656
+ // Restore previous dimensions
1657
+ setXyflowLocalNodes((nodes) =>
1658
+ nodes.map((n) =>
1659
+ n.id === entry.nodeId
1660
+ ? { ...n, width: entry.before.width, height: entry.before.height }
1661
+ : n
1662
+ )
1663
+ );
1664
+ // Update edit state
1665
+ updateEditState((prev) => {
1666
+ const newDimensions = new Map(prev.dimensionChanges);
1667
+ newDimensions.set(entry.nodeId, entry.before);
1668
+ return { ...prev, dimensionChanges: newDimensions };
1669
+ });
1670
+ break;
1671
+
1672
+ case 'edgeCreate':
1673
+ // Remove the created edge
1674
+ setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
1675
+ // Update edit state
1676
+ updateEditState((prev) => ({
1677
+ ...prev,
1678
+ createdEdges: prev.createdEdges.filter((e) => e.id !== entry.edge.id),
1679
+ }));
1680
+ break;
1681
+
1682
+ case 'edgeDelete':
1683
+ // Restore the deleted edge
1684
+ setLocalEdges((edges) => [...edges, entry.edge]);
1685
+ // Update edit state
1686
+ updateEditState((prev) => ({
1687
+ ...prev,
1688
+ deletedEdges: prev.deletedEdges.filter((e) => e.id !== entry.edge.id),
1689
+ }));
1690
+ break;
1691
+ }
1692
+ }
1693
+ }, [undoFromStack, updateEditState]);
1694
+
1695
+ // Apply redo - re-apply the effects of history entries
1696
+ const applyRedo = useCallback(() => {
1697
+ const batch = redoFromStack();
1698
+ if (!batch) return;
1699
+
1700
+ // Process entries in original order for redo
1701
+ for (const entry of batch) {
1702
+ switch (entry.type) {
1703
+ case 'position':
1704
+ // Apply new position
1705
+ setXyflowLocalNodes((nodes) =>
1706
+ nodes.map((n) =>
1707
+ n.id === entry.nodeId
1708
+ ? { ...n, position: entry.after }
1709
+ : n
1710
+ )
1711
+ );
1712
+ // Update edit state
1713
+ updateEditState((prev) => {
1714
+ const newPositions = new Map(prev.positionChanges);
1715
+ newPositions.set(entry.nodeId, entry.after);
1716
+ return { ...prev, positionChanges: newPositions };
1717
+ });
1718
+ break;
1719
+
1720
+ case 'dimension':
1721
+ // Apply new dimensions
1722
+ setXyflowLocalNodes((nodes) =>
1723
+ nodes.map((n) =>
1724
+ n.id === entry.nodeId
1725
+ ? { ...n, width: entry.after.width, height: entry.after.height }
1726
+ : n
1727
+ )
1728
+ );
1729
+ // Update edit state
1730
+ updateEditState((prev) => {
1731
+ const newDimensions = new Map(prev.dimensionChanges);
1732
+ newDimensions.set(entry.nodeId, entry.after);
1733
+ return { ...prev, dimensionChanges: newDimensions };
1734
+ });
1735
+ break;
1736
+
1737
+ case 'edgeCreate':
1738
+ // Re-create the edge
1739
+ setLocalEdges((edges) => [...edges, entry.edge]);
1740
+ // Update edit state
1741
+ updateEditState((prev) => ({
1742
+ ...prev,
1743
+ createdEdges: [
1744
+ ...prev.createdEdges,
1745
+ {
1746
+ id: entry.edge.id,
1747
+ from: entry.edge.from,
1748
+ to: entry.edge.to,
1749
+ type: entry.edge.type,
1750
+ sourceHandle: entry.edge.sourceHandle,
1751
+ targetHandle: entry.edge.targetHandle,
1752
+ },
1753
+ ],
1754
+ }));
1755
+ break;
1756
+
1757
+ case 'edgeDelete':
1758
+ // Re-delete the edge
1759
+ setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
1760
+ // Update edit state
1761
+ updateEditState((prev) => ({
1762
+ ...prev,
1763
+ deletedEdges: [
1764
+ ...prev.deletedEdges,
1765
+ {
1766
+ id: entry.edge.id,
1767
+ from: entry.edge.from,
1768
+ to: entry.edge.to,
1769
+ type: entry.edge.type,
1770
+ },
1771
+ ],
1772
+ }));
1773
+ break;
1774
+ }
1775
+ }
1776
+ }, [redoFromStack, updateEditState]);
1777
+
1778
+ // Keyboard shortcuts for undo/redo
1779
+ useEffect(() => {
1780
+ if (!editable) return;
1781
+
1782
+ const handleKeyDown = (e: KeyboardEvent) => {
1783
+ // Cmd+Z (Mac) or Ctrl+Z (Windows/Linux) for undo
1784
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
1785
+ e.preventDefault();
1786
+ if (e.shiftKey) {
1787
+ // Cmd+Shift+Z for redo
1788
+ applyRedo();
1789
+ } else {
1790
+ applyUndo();
1791
+ }
1792
+ }
1793
+ // Ctrl+Y for redo (Windows)
1794
+ if (e.ctrlKey && e.key === 'y') {
1795
+ e.preventDefault();
1796
+ applyRedo();
1797
+ }
1798
+ };
1799
+
1800
+ window.addEventListener('keydown', handleKeyDown);
1801
+ return () => window.removeEventListener('keydown', handleKeyDown);
1802
+ }, [editable, applyUndo, applyRedo]);
1803
+
1804
+ // Set undo/redo functions in ref for outer component access
1805
+ useEffect(() => {
1806
+ undoRedoFunctionsRef.current = {
1807
+ applyUndo,
1808
+ applyRedo,
1809
+ };
1810
+ }, [applyUndo, applyRedo, undoRedoFunctionsRef]);
1811
+
1537
1812
  // Handle node changes (drag and resize events)
1538
1813
  const handleNodesChange = useCallback(
1539
1814
  (changes: NodeChange[]) => {
@@ -1613,54 +1888,72 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1613
1888
  });
1614
1889
  }
1615
1890
 
1616
- // Track position changes on drag end
1891
+ // Capture positions at drag START for accurate undo "before" values
1892
+ const dragStartChanges = changes.filter(
1893
+ (change): change is NodeChange & {
1894
+ type: 'position';
1895
+ id: string;
1896
+ dragging: boolean;
1897
+ } =>
1898
+ change.type === 'position' &&
1899
+ 'id' in change &&
1900
+ 'dragging' in change &&
1901
+ change.dragging === true
1902
+ );
1903
+
1904
+ // For each node that just started dragging, capture its current position
1905
+ for (const change of dragStartChanges) {
1906
+ if (!dragStartPositionsRef.current.has(change.id)) {
1907
+ const currentNode = xyflowNodesRef.current.find((n) => n.id === change.id);
1908
+ if (currentNode) {
1909
+ dragStartPositionsRef.current.set(change.id, {
1910
+ x: currentNode.position.x,
1911
+ y: currentNode.position.y,
1912
+ });
1913
+ }
1914
+ }
1915
+ }
1916
+
1917
+ // Track position changes on drag END
1617
1918
  const positionChanges = changes.filter(
1618
1919
  (
1619
1920
  change
1620
1921
  ): change is NodeChange & {
1621
1922
  type: 'position';
1923
+ id: string;
1622
1924
  position: { x: number; y: number };
1623
1925
  dragging: boolean;
1624
1926
  } =>
1625
1927
  change.type === 'position' &&
1928
+ 'id' in change &&
1626
1929
  'position' in change &&
1627
1930
  change.position !== undefined &&
1628
1931
  'dragging' in change &&
1629
1932
  change.dragging === false
1630
1933
  );
1631
1934
 
1632
- // Track dimension changes (from NodeResizer)
1633
- const dimensionChanges = changes.filter(
1634
- (
1635
- change
1636
- ): change is NodeChange & {
1637
- type: 'dimensions';
1638
- dimensions: { width: number; height: number };
1639
- resizing: boolean;
1640
- } =>
1641
- change.type === 'dimensions' &&
1642
- 'dimensions' in change &&
1643
- change.dimensions !== undefined &&
1644
- 'resizing' in change &&
1645
- change.resizing === false
1646
- );
1647
-
1648
- if (dimensionChanges.length > 0) {
1649
- updateEditState((prev) => {
1650
- const newDimensions = new Map(prev.dimensionChanges);
1651
- for (const change of dimensionChanges) {
1652
- if (change.dimensions) {
1653
- newDimensions.set(change.id, {
1654
- width: Math.round(change.dimensions.width),
1655
- height: Math.round(change.dimensions.height),
1656
- });
1657
- }
1658
- }
1659
- return { ...prev, dimensionChanges: newDimensions };
1660
- });
1661
- }
1935
+ // Note: Dimension changes are tracked via onResizeEnd callback in GraphEditContext,
1936
+ // not through onNodesChange (which only fires with resizing=true during drag).
1662
1937
 
1663
1938
  if (positionChanges.length > 0) {
1939
+ // Use positions captured at drag start for accurate undo
1940
+ const historyEntries: HistoryEntry[] = [];
1941
+ for (const change of positionChanges) {
1942
+ const beforePos = dragStartPositionsRef.current.get(change.id) ?? { x: 0, y: 0 };
1943
+ historyEntries.push({
1944
+ type: 'position',
1945
+ nodeId: change.id,
1946
+ before: beforePos,
1947
+ after: {
1948
+ x: Math.round(change.position.x),
1949
+ y: Math.round(change.position.y),
1950
+ },
1951
+ });
1952
+ // Clear the drag start position for this node
1953
+ dragStartPositionsRef.current.delete(change.id);
1954
+ }
1955
+ pushHistory(historyEntries);
1956
+
1664
1957
  updateEditState((prev) => {
1665
1958
  const newPositions = new Map(prev.positionChanges);
1666
1959
  for (const change of positionChanges) {
@@ -1673,7 +1966,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1673
1966
  });
1674
1967
  }
1675
1968
  },
1676
- [editable, updateEditState, selectedNodeIds, draggableNodeIds]
1969
+ [editable, updateEditState, selectedNodeIds, draggableNodeIds, pushHistory]
1677
1970
  );
1678
1971
 
1679
1972
  const xyflowEdgesBase = useMemo(() => {
@@ -1742,6 +2035,17 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1742
2035
  }
1743
2036
  }, [baseEdgesKey, xyflowEdgesBase]);
1744
2037
 
2038
+ // Set the reset visual state function for use by resetEditState
2039
+ // This resets both nodes and edges to their original state
2040
+ useEffect(() => {
2041
+ resetVisualStateRef.current = () => {
2042
+ setXyflowLocalNodes(xyflowNodesBase);
2043
+ setXyflowLocalEdges(xyflowEdgesBase);
2044
+ // Notify parent that changes have been cleared
2045
+ onPendingChangesChange?.(false);
2046
+ };
2047
+ }, [xyflowNodesBase, xyflowEdgesBase, onPendingChangesChange]);
2048
+
1745
2049
  // Use local edges in edit mode, base edges otherwise
1746
2050
  const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesBase;
1747
2051
 
@@ -1830,7 +2134,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1830
2134
  // ============================================
1831
2135
 
1832
2136
  return (
1833
- <>
2137
+ <GraphEditProvider value={graphEditContextValue}>
1834
2138
  <ReactFlow
1835
2139
  key={`${baseNodesKey}-${baseEdgesKey}`}
1836
2140
  nodes={xyflowNodes}
@@ -1971,7 +2275,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
1971
2275
  </button>
1972
2276
  </div>
1973
2277
  )}
1974
- </>
2278
+ </GraphEditProvider>
1975
2279
  );
1976
2280
  };
1977
2281
 
@@ -2184,6 +2488,22 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2184
2488
  // Internal edit state ref - must be before any conditional returns
2185
2489
  const editStateRef = useRef<EditState>(createEmptyEditState());
2186
2490
 
2491
+ // Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
2492
+ const resetVisualStateRef = useRef<(() => void) | null>(null);
2493
+
2494
+ // Undo/redo management
2495
+ const {
2496
+ canUndo: canUndoState,
2497
+ canRedo: canRedoState,
2498
+ undo: undoFromStack,
2499
+ redo: redoFromStack,
2500
+ pushHistory,
2501
+ clearHistory,
2502
+ } = useUndoRedo();
2503
+
2504
+ // Ref to hold undo/redo apply functions - will be set by inner component
2505
+ const undoRedoFunctionsRef = useRef<UndoRedoFunctionsRef | null>(null);
2506
+
2187
2507
  // Expose imperative handle - must be before any conditional returns
2188
2508
  useImperativeHandle(
2189
2509
  ref,
@@ -2227,6 +2547,10 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2227
2547
  },
2228
2548
  resetEditState: () => {
2229
2549
  editStateRef.current = createEmptyEditState();
2550
+ // Also reset visual state (node positions/dimensions) if available
2551
+ resetVisualStateRef.current?.();
2552
+ // Clear undo/redo history
2553
+ clearHistory();
2230
2554
  },
2231
2555
  hasUnsavedChanges: (): boolean => {
2232
2556
  const state = editStateRef.current;
@@ -2239,8 +2563,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2239
2563
  state.deletedEdges.length > 0
2240
2564
  );
2241
2565
  },
2566
+ canUndo: () => canUndoState,
2567
+ canRedo: () => canRedoState,
2568
+ undo: () => undoRedoFunctionsRef.current?.applyUndo(),
2569
+ redo: () => undoRedoFunctionsRef.current?.applyRedo(),
2242
2570
  }),
2243
- []
2571
+ [canUndoState, canRedoState, clearHistory]
2244
2572
  );
2245
2573
 
2246
2574
  // Validate we have required data
@@ -2317,6 +2645,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
2317
2645
  editable={editable}
2318
2646
  onPendingChangesChange={onPendingChangesChange}
2319
2647
  editStateRef={editStateRef}
2648
+ resetVisualStateRef={resetVisualStateRef}
2649
+ undoRedoFunctionsRef={undoRedoFunctionsRef}
2650
+ pushHistory={pushHistory}
2651
+ clearHistory={clearHistory}
2652
+ undoFromStack={undoFromStack}
2653
+ redoFromStack={redoFromStack}
2320
2654
  onNodeClick={onNodeClick}
2321
2655
  fitViewToNodeIds={fitViewToNodeIds}
2322
2656
  fitViewPadding={fitViewPadding}
@@ -0,0 +1,21 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ export interface GraphEditContextValue {
4
+ /** Called when a node resize operation completes */
5
+ onNodeResizeEnd?: (nodeId: string, dimensions: { width: number; height: number }) => void;
6
+ }
7
+
8
+ const GraphEditContext = createContext<GraphEditContextValue>({});
9
+
10
+ export const GraphEditProvider: React.FC<{
11
+ children: React.ReactNode;
12
+ value: GraphEditContextValue;
13
+ }> = ({ children, value }) => {
14
+ return (
15
+ <GraphEditContext.Provider value={value}>
16
+ {children}
17
+ </GraphEditContext.Provider>
18
+ );
19
+ };
20
+
21
+ export const useGraphEdit = () => useContext(GraphEditContext);