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: {},
position: { x: 100, y: 0 },
},
{
id: '2',
type: 'text',
@@ -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,
@@ -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();
};
@@ -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({
<ReactFlowProvider
initialNodes={nodes}
initialEdges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
initialWidth={width}
initialHeight={height}
fitView={fitView}
@@ -166,7 +166,15 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
data-testid="rf__wrapper"
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
onInit={onInit}
onNodeClick={onNodeClick}
@@ -234,8 +242,6 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
<StoreUpdater
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
+48 -94
View File
@@ -10,8 +10,7 @@ import {
updateConnectionLookup,
} from '@xyflow/system';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { updateNodesAndEdgesSelections } from './utils';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState';
import type {
ReactFlowState,
@@ -28,19 +27,23 @@ import type {
const createRFStore = ({
nodes,
edges,
defaultNodes,
defaultEdges,
width,
height,
fitView,
}: {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
width?: number;
height?: number;
fitView?: boolean;
}) =>
createWithEqualityFn<ReactFlowState>(
(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<NodeSelectionChange[]>(
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res),
[]
);
const edgeChanges = edges.reduce<EdgeSelectionChange[]>(
(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();
+13 -7
View File
@@ -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,
-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,
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<string, NodeDimensionUpdate>) => 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;
};
+7 -5
View File
@@ -219,11 +219,13 @@ export function applyEdgeChanges<EdgeType extends Edge = Edge>(
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[],