refactor defaultNodes/defaultEdges handling (#3882)

* refactor(react): cleanup defaultnodes/defaultedges handling

* chore(examples): cleanup

* chore(examples): cleanup

* fix(react): selection for uncontrolled flows
This commit is contained in:
Moritz Klack
2024-02-07 17:09:39 +01:00
committed by GitHub
parent 76606c67da
commit 6b18c9daeb
10 changed files with 96 additions and 172 deletions
@@ -41,7 +41,6 @@ const initNodes: MyNode[] = [
data: {}, data: {},
position: { x: 100, y: 0 }, position: { x: 100, y: 0 },
}, },
{ {
id: '2', id: '2',
type: 'text', type: 'text',
@@ -10,6 +10,8 @@ export function ReactFlowProvider({
children, children,
initialNodes, initialNodes,
initialEdges, initialEdges,
defaultNodes,
defaultEdges,
initialWidth, initialWidth,
initialHeight, initialHeight,
fitView, fitView,
@@ -17,6 +19,8 @@ export function ReactFlowProvider({
children: ReactNode; children: ReactNode;
initialNodes?: Node[]; initialNodes?: Node[];
initialEdges?: Edge[]; initialEdges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
fitView?: boolean; fitView?: boolean;
@@ -27,6 +31,8 @@ export function ReactFlowProvider({
storeRef.current = createRFStore({ storeRef.current = createRFStore({
nodes: initialNodes, nodes: initialNodes,
edges: initialEdges, edges: initialEdges,
defaultNodes,
defaultEdges,
width: initialWidth, width: initialWidth,
height: initialHeight, height: initialHeight,
fitView, fitView,
@@ -15,8 +15,6 @@ import { initNodeOrigin } from '../../container/ReactFlow';
const reactFlowFieldsToTrack = [ const reactFlowFieldsToTrack = [
'nodes', 'nodes',
'edges', 'edges',
'defaultNodes',
'defaultEdges',
'onConnect', 'onConnect',
'onConnectStart', 'onConnectStart',
'onConnectEnd', 'onConnectEnd',
@@ -76,7 +74,6 @@ const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes, setNodes: s.setNodes,
setEdges: s.setEdges, setEdges: s.setEdges,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setMinZoom: s.setMinZoom, setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom, setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent, setTranslateExtent: s.setTranslateExtent,
@@ -85,22 +82,13 @@ const selector = (s: ReactFlowState) => ({
}); });
export function StoreUpdater(props: StoreUpdaterProps) { export function StoreUpdater(props: StoreUpdaterProps) {
const { const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore(
setNodes, selector,
setEdges, shallow
setDefaultNodesAndEdges, );
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
} = useStore(selector, shallow);
const store = useStoreApi(); const store = useStoreApi();
useEffect(() => { useEffect(() => {
const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
return () => { return () => {
reset(); reset();
}; };
@@ -8,6 +8,8 @@ export function Wrapper({
children, children,
nodes, nodes,
edges, edges,
defaultNodes,
defaultEdges,
width, width,
height, height,
fitView, fitView,
@@ -15,6 +17,8 @@ export function Wrapper({
children: ReactNode; children: ReactNode;
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
@@ -31,6 +35,8 @@ export function Wrapper({
<ReactFlowProvider <ReactFlowProvider
initialNodes={nodes} initialNodes={nodes}
initialEdges={edges} initialEdges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
initialWidth={width} initialWidth={width}
initialHeight={height} initialHeight={height}
fitView={fitView} fitView={fitView}
@@ -166,7 +166,15 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
data-testid="rf__wrapper" data-testid="rf__wrapper"
id={id} id={id}
> >
<Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView}> <Wrapper
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
width={width}
height={height}
fitView={fitView}
>
<GraphView <GraphView
onInit={onInit} onInit={onInit}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
@@ -234,8 +242,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
<StoreUpdater <StoreUpdater
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onConnect={onConnect} onConnect={onConnect}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
+48 -94
View File
@@ -10,8 +10,7 @@ import {
updateConnectionLookup, updateConnectionLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { updateNodesAndEdgesSelections } from './utils';
import getInitialState from './initialState'; import getInitialState from './initialState';
import type { import type {
ReactFlowState, ReactFlowState,
@@ -28,19 +27,23 @@ import type {
const createRFStore = ({ const createRFStore = ({
nodes, nodes,
edges, edges,
defaultNodes,
defaultEdges,
width, width,
height, height,
fitView, fitView,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
}) => }) =>
createWithEqualityFn<ReactFlowState>( createWithEqualityFn<ReactFlowState>(
(set, get) => ({ (set, get) => ({
...getInitialState({ nodes, edges, width, height, fitView }), ...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions: // 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 // When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations. // 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 }); const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
set({ nodes: nodesWithInternalData }); set({ nodes: nodesWithInternalData });
@@ -61,38 +63,6 @@ const createRFStore = ({
set({ edges }); 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 // Every node gets registerd at a ResizeObserver. Whenever a node
// changes its dimensions, this function is called to measure the // changes its dimensions, this function is called to measure the
// new dimensions and update the nodes. // new dimensions and update the nodes.
@@ -166,81 +136,67 @@ const createRFStore = ({
get().triggerNodeChanges(changes); get().triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes) => { triggerNodeChanges: (changes) => {
const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get(); const { onNodesChange, setNodes, nodes, hasDefaultNodes } = get();
if (changes?.length) { if (changes?.length) {
if (hasDefaultNodes) { if (hasDefaultNodes) {
const updatedNodes = applyNodeChanges(changes, nodes); const updatedNodes = applyNodeChanges(changes, nodes);
const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, { setNodes(updatedNodes);
nodeOrigin,
elevateNodesOnSelect,
});
set({ nodes: nextNodes });
} }
onNodesChange?.(changes); 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) => { addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edges, nodes } = get(); const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null;
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
} else { triggerNodeChanges(nodeChanges as NodeSelectionChange[]);
changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true); return;
changedEdges = getSelectionChanges(edges);
} }
updateNodesAndEdgesSelections({ triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true));
changedNodes, triggerEdgeChanges(getSelectionChanges(edges));
changedEdges,
get,
set,
});
}, },
addSelectedEdges: (selectedEdgeIds) => { addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edges, nodes } = get(); const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null;
if (multiSelectionActive) { if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
} else { triggerEdgeChanges(changedEdges as EdgeSelectionChange[]);
changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds])); return;
changedNodes = getSelectionChanges(nodes, new Set(), true);
} }
updateNodesAndEdgesSelections({ triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds])));
changedNodes, triggerNodeChanges(getSelectionChanges(nodes, new Set(), true));
changedEdges,
get,
set,
});
}, },
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => { unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { edges: storeEdges, nodes: storeNodes } = get(); const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
const nodesToUnselect = nodes ? nodes : storeNodes; const nodesToUnselect = nodes ? nodes : storeNodes;
const edgesToUnselect = edges ? edges : storeEdges; const edgesToUnselect = edges ? edges : storeEdges;
const changedNodes = nodesToUnselect.map((n) => { const nodeChanges = nodesToUnselect.map((n) => {
n.selected = false; n.selected = false;
return createSelectionChange(n.id, 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) => { setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get(); const { panZoom, maxZoom } = get();
@@ -260,21 +216,19 @@ const createRFStore = ({
set({ translateExtent }); set({ translateExtent });
}, },
resetSelectedElements: () => { resetSelectedElements: () => {
const { edges, nodes } = get(); const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
const nodesToUnselect = nodes const nodeChanges = nodes.reduce<NodeSelectionChange[]>(
.filter((e) => e.selected) (res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res),
.map((n) => createSelectionChange(n.id, false)) as NodeSelectionChange[]; []
const edgesToUnselect = edges );
.filter((e) => e.selected) const edgeChanges = edges.reduce<EdgeSelectionChange[]>(
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[]; (res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res),
[]
);
updateNodesAndEdgesSelections({ triggerNodeChanges(nodeChanges);
changedNodes: nodesToUnselect, triggerEdgeChanges(edgeChanges);
changedEdges: edgesToUnselect,
get,
set,
});
}, },
setNodeExtent: (nodeExtent) => { setNodeExtent: (nodeExtent) => {
const { nodes } = get(); const { nodes } = get();
+13 -7
View File
@@ -12,14 +12,18 @@ import {
import type { Edge, Node, ReactFlowStore } from '../types'; import type { Edge, Node, ReactFlowStore } from '../types';
const getInitialState = ({ const getInitialState = ({
nodes = [], nodes,
edges = [], edges,
defaultNodes,
defaultEdges,
width, width,
height, height,
fitView, fitView,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
@@ -27,9 +31,11 @@ const getInitialState = ({
const nodeLookup = new Map(); const nodeLookup = new Map();
const connectionLookup = new Map(); const connectionLookup = new Map();
const edgeLookup = new Map(); const edgeLookup = new Map();
const storeEdges = defaultEdges ?? edges ?? [];
const storeNodes = defaultNodes ?? nodes ?? [];
updateConnectionLookup(connectionLookup, edgeLookup, edges); updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
}); });
@@ -51,13 +57,13 @@ const getInitialState = ({
transform, transform,
nodes: nextNodes, nodes: nextNodes,
nodeLookup, nodeLookup,
edges, edges: storeEdges,
edgeLookup, edgeLookup,
connectionLookup, connectionLookup,
onNodesChange: null, onNodesChange: null,
onEdgesChange: null, onEdgesChange: null,
hasDefaultNodes: false, hasDefaultNodes: defaultNodes !== undefined,
hasDefaultEdges: false, hasDefaultEdges: defaultEdges !== undefined,
panZoom: null, panZoom: null,
minZoom: 0.5, minZoom: 0.5,
maxZoom: 2, maxZoom: 2,
-44
View File
@@ -1,44 +0,0 @@
import type { StoreApi } from 'zustand';
import type { Edge, EdgeSelectionChange, Node, NodeSelectionChange, ReactFlowState } from '../types';
export function handleControlledSelectionChange<NodeOrEdge extends Node | Edge>(
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<ReactFlowState>['getState'];
set: StoreApi<ReactFlowState>['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);
}
}
+3 -2
View File
@@ -43,6 +43,7 @@ import type {
OnDelete, OnDelete,
OnNodeDrag, OnNodeDrag,
OnBeforeDelete, OnBeforeDelete,
EdgeChange,
} from '.'; } from '.';
export type ReactFlowStore = { export type ReactFlowStore = {
@@ -150,7 +151,6 @@ export type ReactFlowStore = {
export type ReactFlowActions = { export type ReactFlowActions = {
setNodes: (nodes: Node[]) => void; setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void; setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void; updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void;
updateNodePositions: UpdateNodePositions; updateNodePositions: UpdateNodePositions;
resetSelectedElements: () => void; resetSelectedElements: () => void;
@@ -164,7 +164,8 @@ export type ReactFlowActions = {
cancelConnection: () => void; cancelConnection: () => void;
updateConnection: UpdateConnection; updateConnection: UpdateConnection;
reset: () => void; reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void; triggerNodeChanges: (changes: NodeChange[] | null) => void;
triggerEdgeChanges: (changes: EdgeChange[] | null) => void;
panBy: PanBy; panBy: PanBy;
fitView: (nodes: Node[], options?: FitViewOptions) => boolean; fitView: (nodes: Node[], options?: FitViewOptions) => boolean;
}; };
+7 -5
View File
@@ -219,11 +219,13 @@ export function applyEdgeChanges<EdgeType extends Edge = Edge>(
return applyChanges(changes, edges) as EdgeType[]; return applyChanges(changes, edges) as EdgeType[];
} }
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({ export function createSelectionChange(id: string, selected: boolean) {
id, return {
type: 'select', id,
selected, type: 'select',
}); selected,
};
}
export function getSelectionChanges( export function getSelectionChanges(
items: any[], items: any[],