From 6b18c9daeb6e095a4c0d8b0827fbea07de8e3f78 Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Wed, 7 Feb 2024 17:09:39 +0100 Subject: [PATCH] refactor defaultNodes/defaultEdges handling (#3882) * refactor(react): cleanup defaultnodes/defaultedges handling * chore(examples): cleanup * chore(examples): cleanup * fix(react): selection for uncontrolled flows --- .../react/src/examples/UseNodesData/index.tsx | 1 - .../components/ReactFlowProvider/index.tsx | 6 + .../src/components/StoreUpdater/index.tsx | 20 +-- .../react/src/container/ReactFlow/Wrapper.tsx | 6 + .../react/src/container/ReactFlow/index.tsx | 12 +- packages/react/src/store/index.ts | 142 ++++++------------ packages/react/src/store/initialState.ts | 20 ++- packages/react/src/store/utils.ts | 44 ------ packages/react/src/types/store.ts | 5 +- packages/react/src/utils/changes.ts | 12 +- 10 files changed, 96 insertions(+), 172 deletions(-) delete mode 100644 packages/react/src/store/utils.ts diff --git a/examples/react/src/examples/UseNodesData/index.tsx b/examples/react/src/examples/UseNodesData/index.tsx index d99ddb43..cbeb2478 100644 --- a/examples/react/src/examples/UseNodesData/index.tsx +++ b/examples/react/src/examples/UseNodesData/index.tsx @@ -41,7 +41,6 @@ const initNodes: MyNode[] = [ data: {}, position: { x: 100, y: 0 }, }, - { id: '2', type: 'text', diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 34999c6f..829a304d 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -10,6 +10,8 @@ export function ReactFlowProvider({ children, initialNodes, initialEdges, + defaultNodes, + defaultEdges, initialWidth, initialHeight, fitView, @@ -17,6 +19,8 @@ export function ReactFlowProvider({ children: ReactNode; initialNodes?: Node[]; initialEdges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; initialWidth?: number; initialHeight?: number; fitView?: boolean; @@ -27,6 +31,8 @@ export function ReactFlowProvider({ storeRef.current = createRFStore({ nodes: initialNodes, edges: initialEdges, + defaultNodes, + defaultEdges, width: initialWidth, height: initialHeight, fitView, diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 3a12594c..98ad3bac 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -15,8 +15,6 @@ import { initNodeOrigin } from '../../container/ReactFlow'; const reactFlowFieldsToTrack = [ 'nodes', 'edges', - 'defaultNodes', - 'defaultEdges', 'onConnect', 'onConnectStart', 'onConnectEnd', @@ -76,7 +74,6 @@ const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const; const selector = (s: ReactFlowState) => ({ setNodes: s.setNodes, setEdges: s.setEdges, - setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, setMinZoom: s.setMinZoom, setMaxZoom: s.setMaxZoom, setTranslateExtent: s.setTranslateExtent, @@ -85,22 +82,13 @@ const selector = (s: ReactFlowState) => ({ }); export function StoreUpdater(props: StoreUpdaterProps) { - const { - setNodes, - setEdges, - setDefaultNodesAndEdges, - setMinZoom, - setMaxZoom, - setTranslateExtent, - setNodeExtent, - reset, - } = useStore(selector, shallow); + const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore( + selector, + shallow + ); const store = useStoreApi(); useEffect(() => { - const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions })); - setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults); - return () => { reset(); }; diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 4d3cb7b7..b82ca249 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -8,6 +8,8 @@ export function Wrapper({ children, nodes, edges, + defaultNodes, + defaultEdges, width, height, fitView, @@ -15,6 +17,8 @@ export function Wrapper({ children: ReactNode; nodes?: Node[]; edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; width?: number; height?: number; fitView?: boolean; @@ -31,6 +35,8 @@ export function Wrapper({ ( data-testid="rf__wrapper" id={id} > - + ( createWithEqualityFn( (set, get) => ({ - ...getInitialState({ nodes, edges, width, height, fitView }), + ...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); // setNodes() is called exclusively in response to user actions: @@ -49,7 +52,6 @@ const createRFStore = ({ // // When this happens, we take the note objects passed by the user and extend them with fields // relevant for internal React Flow operations. - // TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes. const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); set({ nodes: nodesWithInternalData }); @@ -61,38 +63,6 @@ const createRFStore = ({ set({ edges }); }, - // when the user works with an uncontrolled flow, - // we set a flag `hasDefaultNodes` / `hasDefaultEdges` - setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => { - const hasDefaultNodes = typeof nodes !== 'undefined'; - const hasDefaultEdges = typeof edges !== 'undefined'; - - const nextState: { - nodes?: Node[]; - edges?: Edge[]; - hasDefaultNodes: boolean; - hasDefaultEdges: boolean; - } = { - hasDefaultNodes, - hasDefaultEdges, - }; - - if (hasDefaultNodes) { - const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); - nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, { - nodeOrigin, - elevateNodesOnSelect, - }); - } - if (hasDefaultEdges) { - const { connectionLookup, edgeLookup } = get(); - updateConnectionLookup(connectionLookup, edgeLookup, edges); - - nextState.edges = edges; - } - - set(nextState); - }, // Every node gets registerd at a ResizeObserver. Whenever a node // changes its dimensions, this function is called to measure the // new dimensions and update the nodes. @@ -166,81 +136,67 @@ const createRFStore = ({ get().triggerNodeChanges(changes); }, - triggerNodeChanges: (changes) => { - const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); + const { onNodesChange, setNodes, nodes, hasDefaultNodes } = get(); if (changes?.length) { if (hasDefaultNodes) { const updatedNodes = applyNodeChanges(changes, nodes); - const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, { - nodeOrigin, - elevateNodesOnSelect, - }); - set({ nodes: nextNodes }); + setNodes(updatedNodes); } onNodesChange?.(changes); } }, + triggerEdgeChanges: (changes) => { + const { onEdgesChange, setEdges, edges, hasDefaultEdges } = get(); + if (changes?.length) { + if (hasDefaultEdges) { + const updatedEdges = applyEdgeChanges(changes, edges); + setEdges(updatedEdges); + } + + onEdgesChange?.(changes); + } + }, addSelectedNodes: (selectedNodeIds) => { - const { multiSelectionActive, edges, nodes } = get(); - let changedNodes: NodeSelectionChange[]; - let changedEdges: EdgeSelectionChange[] | null = null; + const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { - changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; - } else { - changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true); - changedEdges = getSelectionChanges(edges); + const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); + triggerNodeChanges(nodeChanges as NodeSelectionChange[]); + return; } - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, - }); + triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true)); + triggerEdgeChanges(getSelectionChanges(edges)); }, addSelectedEdges: (selectedEdgeIds) => { - const { multiSelectionActive, edges, nodes } = get(); - let changedEdges: EdgeSelectionChange[]; - let changedNodes: NodeSelectionChange[] | null = null; + const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); if (multiSelectionActive) { - changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; - } else { - changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds])); - changedNodes = getSelectionChanges(nodes, new Set(), true); + const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); + triggerEdgeChanges(changedEdges as EdgeSelectionChange[]); + return; } - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, - }); + triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds]))); + triggerNodeChanges(getSelectionChanges(nodes, new Set(), true)); }, unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { - const { edges: storeEdges, nodes: storeNodes } = get(); + const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get(); const nodesToUnselect = nodes ? nodes : storeNodes; const edgesToUnselect = edges ? edges : storeEdges; - const changedNodes = nodesToUnselect.map((n) => { + const nodeChanges = nodesToUnselect.map((n) => { n.selected = false; return createSelectionChange(n.id, false); - }) as NodeSelectionChange[]; - const changedEdges = edgesToUnselect.map((edge) => - createSelectionChange(edge.id, false) - ) as EdgeSelectionChange[]; - - updateNodesAndEdgesSelections({ - changedNodes, - changedEdges, - get, - set, }); + const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false)); + + triggerNodeChanges(nodeChanges as NodeSelectionChange[]); + triggerEdgeChanges(edgeChanges as EdgeSelectionChange[]); }, setMinZoom: (minZoom) => { const { panZoom, maxZoom } = get(); @@ -260,21 +216,19 @@ const createRFStore = ({ set({ translateExtent }); }, resetSelectedElements: () => { - const { edges, nodes } = get(); + const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get(); - const nodesToUnselect = nodes - .filter((e) => e.selected) - .map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[]; - const edgesToUnselect = edges - .filter((e) => e.selected) - .map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[]; + const nodeChanges = nodes.reduce( + (res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res), + [] + ); + const edgeChanges = edges.reduce( + (res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res), + [] + ); - updateNodesAndEdgesSelections({ - changedNodes: nodesToUnselect, - changedEdges: edgesToUnselect, - get, - set, - }); + triggerNodeChanges(nodeChanges); + triggerEdgeChanges(edgeChanges); }, setNodeExtent: (nodeExtent) => { const { nodes } = get(); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index aace0bb4..5a1f9562 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -12,14 +12,18 @@ import { import type { Edge, Node, ReactFlowStore } from '../types'; const getInitialState = ({ - nodes = [], - edges = [], + nodes, + edges, + defaultNodes, + defaultEdges, width, height, fitView, }: { nodes?: Node[]; edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; width?: number; height?: number; fitView?: boolean; @@ -27,9 +31,11 @@ const getInitialState = ({ const nodeLookup = new Map(); const connectionLookup = new Map(); const edgeLookup = new Map(); + const storeEdges = defaultEdges ?? edges ?? []; + const storeNodes = defaultNodes ?? nodes ?? []; - updateConnectionLookup(connectionLookup, edgeLookup, edges); - const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { + updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); + const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, }); @@ -51,13 +57,13 @@ const getInitialState = ({ transform, nodes: nextNodes, nodeLookup, - edges, + edges: storeEdges, edgeLookup, connectionLookup, onNodesChange: null, onEdgesChange: null, - hasDefaultNodes: false, - hasDefaultEdges: false, + hasDefaultNodes: defaultNodes !== undefined, + hasDefaultEdges: defaultEdges !== undefined, panZoom: null, minZoom: 0.5, maxZoom: 2, diff --git a/packages/react/src/store/utils.ts b/packages/react/src/store/utils.ts deleted file mode 100644 index 86cddde7..00000000 --- a/packages/react/src/store/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { StoreApi } from 'zustand'; -import type { Edge, EdgeSelectionChange, Node, NodeSelectionChange, ReactFlowState } from '../types'; - -export function handleControlledSelectionChange( - changes: NodeSelectionChange[] | EdgeSelectionChange[], - items: NodeOrEdge[] -): NodeOrEdge[] { - return items.map((item) => { - const change = changes.find((change) => change.id === item.id); - - if (change) { - item.selected = change.selected; - } - - return item; - }); -} - -type UpdateNodesAndEdgesParams = { - changedNodes: NodeSelectionChange[] | null; - changedEdges: EdgeSelectionChange[] | null; - get: StoreApi['getState']; - set: StoreApi['setState']; -}; - -export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get, set }: UpdateNodesAndEdgesParams) { - const { nodes, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get(); - - if (changedNodes?.length) { - if (hasDefaultNodes) { - set({ nodes: handleControlledSelectionChange(changedNodes, nodes) }); - } - - onNodesChange?.(changedNodes); - } - - if (changedEdges?.length) { - if (hasDefaultEdges) { - set({ edges: handleControlledSelectionChange(changedEdges, edges) }); - } - - onEdgesChange?.(changedEdges); - } -} diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index b3e0449e..46f83128 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -43,6 +43,7 @@ import type { OnDelete, OnNodeDrag, OnBeforeDelete, + EdgeChange, } from '.'; export type ReactFlowStore = { @@ -150,7 +151,6 @@ export type ReactFlowStore = { export type ReactFlowActions = { setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; - setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: Map) => void; updateNodePositions: UpdateNodePositions; resetSelectedElements: () => void; @@ -164,7 +164,8 @@ export type ReactFlowActions = { cancelConnection: () => void; updateConnection: UpdateConnection; reset: () => void; - triggerNodeChanges: (changes: NodeChange[]) => void; + triggerNodeChanges: (changes: NodeChange[] | null) => void; + triggerEdgeChanges: (changes: EdgeChange[] | null) => void; panBy: PanBy; fitView: (nodes: Node[], options?: FitViewOptions) => boolean; }; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 310b784c..5789a38a 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -219,11 +219,13 @@ export function applyEdgeChanges( return applyChanges(changes, edges) as EdgeType[]; } -export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({ - id, - type: 'select', - selected, -}); +export function createSelectionChange(id: string, selected: boolean) { + return { + id, + type: 'select', + selected, + }; +} export function getSelectionChanges( items: any[],