From 634b592b3a8494cab8fea5859fa308adad88174d Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 18 Jan 2022 13:05:41 +0100 Subject: [PATCH] feat(general): add defaultNodes, defaultEdges and defaultEdgeOptions --- example/src/DefaultNodes/index.tsx | 98 +++++++++++++++++++++ example/src/index.tsx | 5 ++ src/components/Handle/index.tsx | 20 ++++- src/components/StoreUpdater/index.tsx | 47 +++++++--- src/container/ReactFlow/index.tsx | 18 +++- src/hooks/useGlobalKeyHandler.ts | 28 ++++-- src/hooks/useUpdateEdges.ts | 23 +++++ src/hooks/useUpdateNodes.ts | 24 +++++ src/index.ts | 2 + src/store/index.ts | 122 +++++++++++++++++++------- src/store/initialState.ts | 1 + src/store/utils.ts | 27 ++++++ src/types/edges.ts | 5 ++ src/types/general.ts | 4 + 14 files changed, 364 insertions(+), 60 deletions(-) create mode 100644 example/src/DefaultNodes/index.tsx create mode 100644 src/hooks/useUpdateEdges.ts create mode 100644 src/hooks/useUpdateNodes.ts diff --git a/example/src/DefaultNodes/index.tsx b/example/src/DefaultNodes/index.tsx new file mode 100644 index 00000000..3e2f8dcf --- /dev/null +++ b/example/src/DefaultNodes/index.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import ReactFlow, { + Background, + BackgroundVariant, + Node, + Edge, + ReactFlowInstance, + ReactFlowProvider, + useUpdateNodes, + useUpdateEdges, +} from 'react-flow-renderer'; + +const defaultNodes: Node[] = [ + { id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' }, + { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' }, + { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' }, + { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' }, +]; + +const defaultEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2' }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const defaultEdgeOptions = { + animated: true, +}; + +const DefaultNodes = () => { + const [rfInstance, setRfInstance] = useState(null); + const updateNodes = useUpdateNodes(); + const updateEdges = useUpdateEdges(); + + const logToObject = () => console.log(rfInstance?.toObject()); + const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); + + const updateNodePositions = () => { + updateNodes((nodes, setNodes) => { + setNodes( + nodes.map((node) => { + node.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return node; + }) + ); + }); + }; + + const updateEdgeColors = () => { + updateEdges((edges, setEdges) => { + setEdges( + edges.map((edge) => { + edge.style = { + stroke: '#ff5050', + }; + + return edge; + }) + ); + }); + }; + + return ( + + + +
+ + + + +
+
+ ); +}; + +export default function App() { + return ( + + + + ); +} diff --git a/example/src/index.tsx b/example/src/index.tsx index 7f596dec..84d583fe 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -31,6 +31,7 @@ import NodeTypesObjectChange from './NodeTypesObjectChange'; import SaveRestore from './SaveRestore'; import SwitchFlow from './Switch'; import Validation from './Validation'; +import DefaultNodes from './DefaultNodes'; import './index.css'; @@ -43,6 +44,10 @@ const routes = [ path: '/basic', component: Basic, }, + { + path: '/default-nodes', + component: DefaultNodes, + }, { path: '/custom-connectionline', component: CustomConnectionLine, diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 2bb21e51..1529ea95 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -7,6 +7,7 @@ import NodeIdContext from '../../contexts/NodeIdContext'; import { HandleProps, Connection, ReactFlowState, Position } from '../../types'; import { checkElementBelowIsValid, onMouseDown } from './handler'; import { getHostForElement } from '../../utils'; +import { addEdge } from '../../utils/graph'; const alwaysValid = () => true; @@ -20,6 +21,7 @@ const selector = (s: ReactFlowState) => ({ connectionMode: s.connectionMode, connectionStartHandle: s.connectionStartHandle, connectOnClick: s.connectOnClick, + isControlled: s.isControlled, }); const Handle = forwardRef( @@ -47,6 +49,7 @@ const Handle = forwardRef( connectionMode, connectionStartHandle, connectOnClick, + isControlled, } = useStore(selector, shallow); const handleId = id || null; @@ -54,10 +57,21 @@ const Handle = forwardRef( const onConnectExtended = useCallback( (params: Connection) => { - onConnectAction?.(params); - onConnect?.(params); + const { defaultEdgeOptions } = store.getState(); + + const edgeParams = { + ...defaultEdgeOptions, + ...params, + }; + if (isControlled) { + const { edges } = store.getState(); + store.setState({ edges: addEdge(edgeParams, edges) }); + } else { + onConnectAction?.(edgeParams); + } + onConnect?.(edgeParams); }, - [onConnectAction, onConnect] + [isControlled, onConnectAction, onConnect] ); const onMouseDownHandler = useCallback( diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index 99cb1d40..519e102f 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -16,11 +16,14 @@ import { OnEdgesChange, ConnectionMode, SnapGrid, + DefaultEdgeOptions, } from '../../types'; interface StoreUpdaterProps { - nodes: Node[]; - edges: Edge[]; + nodes?: Node[]; + edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; onConnect?: OnConnect; onConnectStart?: OnConnectStart; onConnectStop?: OnConnectStop; @@ -39,11 +42,13 @@ interface StoreUpdaterProps { translateExtent?: CoordinateExtent; fitViewOnInit: boolean; connectOnClick: boolean; + defaultEdgeOptions?: DefaultEdgeOptions; } const selector = (s: ReactFlowState) => ({ setNodes: s.setNodes, setEdges: s.setEdges, + setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, setMinZoom: s.setMinZoom, setMaxZoom: s.setMaxZoom, setTranslateExtent: s.setTranslateExtent, @@ -71,6 +76,8 @@ function useDirectStoreUpdater(key: keyof ReactFlowState, value: any, setState: const StoreUpdater = ({ nodes, edges, + defaultNodes, + defaultEdges, onConnect, onConnectStart, onConnectStop, @@ -89,26 +96,31 @@ const StoreUpdater = ({ translateExtent, fitViewOnInit, connectOnClick, + defaultEdgeOptions, }: StoreUpdaterProps) => { - const { setNodes, setEdges, setMinZoom, setMaxZoom, setTranslateExtent, setNodeExtent, reset } = useStore( - selector, - shallow - ); + const { + setNodes, + setEdges, + setDefaultNodesAndEdges, + setMinZoom, + setMaxZoom, + setTranslateExtent, + setNodeExtent, + reset, + } = useStore(selector, shallow); const store = useStoreApi(); useEffect(() => { + if (defaultNodes) { + setDefaultNodesAndEdges(defaultNodes, defaultEdges); + } + return () => { reset(); }; }, []); - useStoreUpdater(nodes, setNodes); - useStoreUpdater(edges, setEdges); - useStoreUpdater(minZoom, setMinZoom); - useStoreUpdater(maxZoom, setMaxZoom); - useStoreUpdater(translateExtent, setTranslateExtent); - useStoreUpdater(nodeExtent, setNodeExtent); - + useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState); useDirectStoreUpdater('connectionMode', connectionMode, store.setState); useDirectStoreUpdater('onConnect', onConnect, store.setState); useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState); @@ -124,6 +136,15 @@ const StoreUpdater = ({ useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState); useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState); + useStoreUpdater(nodes, setNodes); + useStoreUpdater(edges, setEdges); + useStoreUpdater(defaultNodes, setNodes); + useStoreUpdater(defaultEdges, setEdges); + useStoreUpdater(minZoom, setMinZoom); + useStoreUpdater(maxZoom, setMaxZoom); + useStoreUpdater(translateExtent, setTranslateExtent); + useStoreUpdater(nodeExtent, setNodeExtent); + return null; }; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index ef961b53..82252283 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -43,6 +43,7 @@ import { OnPaneReady, ProOptions, AttributionPosition, + DefaultEdgeOptions, } from '../../types'; import '../../style.css'; @@ -62,8 +63,11 @@ const defaultEdgeTypes = { }; export interface ReactFlowProps extends Omit, 'onPaneReady'> { - nodes: Node[]; - edges: Edge[]; + nodes?: Node[]; + edges?: Edge[]; + defaultNodes?: Node[]; + defaultEdges?: Edge[]; + defaultEdgeOptions?: DefaultEdgeOptions; onNodesChange?: (nodeChanges: NodeChange[]) => void; onEdgesChange?: (edgeChanges: EdgeChange[]) => void; onNodeClick?: (event: React.MouseEvent, node: Node) => void; @@ -150,8 +154,10 @@ const initDefaultPosition: [number, number] = [0, 0]; const ReactFlow: FunctionComponent = forwardRef( ( { - nodes = [], - edges = [], + nodes, + edges, + defaultNodes, + defaultEdges, className, nodeTypes = defaultNodeTypes, edgeTypes = defaultEdgeTypes, @@ -230,6 +236,7 @@ const ReactFlow: FunctionComponent = forwardRef = forwardRef = forwardRef {onSelectionChange && } {children} diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 22e17d40..cd05cab8 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -25,7 +25,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { - const { nodeInternals, edges } = store.getState(); + const { nodeInternals, edges, isControlled } = store.getState(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); const selectedNodes = nodes.filter((n) => n.selected); @@ -33,15 +33,27 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { if (deleteKeyPressed && (selectedNodes || selectedEdges)) { const connectedEdges = getConnectedEdges(selectedNodes, edges); + const edgeIdsToRemove = [...selectedEdges, ...connectedEdges].map((e) => e.id); - const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' })); - const edgeChanges: EdgeChange[] = [...selectedEdges, ...connectedEdges].map((e) => ({ - id: e.id, - type: 'remove', - })); + if (isControlled) { + selectedNodes.forEach((node) => { + nodeInternals.delete(node.id); + }); - onNodesChange?.(nodeChanges); - onEdgesChange?.(edgeChanges); + store.setState({ + nodeInternals: new Map(nodeInternals), + edges: edges.filter((e) => !edgeIdsToRemove.includes(e.id)), + }); + } else { + const nodeChanges: NodeChange[] = selectedNodes.map((n) => ({ id: n.id, type: 'remove' })); + const edgeChanges: EdgeChange[] = edgeIdsToRemove.map((id) => ({ + id, + type: 'remove', + })); + + onNodesChange?.(nodeChanges); + onEdgesChange?.(edgeChanges); + } store.setState({ nodesSelectionActive: false }); diff --git a/src/hooks/useUpdateEdges.ts b/src/hooks/useUpdateEdges.ts new file mode 100644 index 00000000..d46a03de --- /dev/null +++ b/src/hooks/useUpdateEdges.ts @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; +import { useStoreApi } from '../store'; +import { Edge } from '../types'; + +type FunctionParams = (nodes: Edge[], setEdges: (nodes: Edge[]) => void) => void; + +function useUpdateEdges() { + const store = useStoreApi(); + + const update = useMemo(() => { + return (action: FunctionParams): void => { + const { edges, setEdges } = store.getState(); + + if (typeof action === 'function') { + action(edges, setEdges); + } + }; + }, []); + + return update; +} + +export default useUpdateEdges; diff --git a/src/hooks/useUpdateNodes.ts b/src/hooks/useUpdateNodes.ts new file mode 100644 index 00000000..8a1deb16 --- /dev/null +++ b/src/hooks/useUpdateNodes.ts @@ -0,0 +1,24 @@ +import { useMemo } from 'react'; +import { useStoreApi } from '../store'; +import { Node } from '../types'; + +type FunctionParams = (nodes: Node[], setNodes: (nodes: Node[]) => void) => void; + +function useUpdateNodes() { + const store = useStoreApi(); + + const update = useMemo(() => { + return (action: FunctionParams): void => { + const { nodeInternals, setNodes } = store.getState(); + const nodes = Array.from(nodeInternals.values()) as Node[]; + + if (typeof action === 'function') { + action(nodes, setNodes); + } + }; + }, []); + + return update; +} + +export default useUpdateNodes; diff --git a/src/index.ts b/src/index.ts index 4afb04d8..e69b3ef6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export { default as useNodeBounds } from './hooks/useNodeBounds'; +export { default as useUpdateNodes } from './hooks/useUpdateNodes'; +export { default as useUpdateEdges } from './hooks/useUpdateEdges'; export * from './hooks/useNodesEdgesState'; export * from './additional-components'; diff --git a/src/store/index.ts b/src/store/index.ts index 7b4b0d47..2c4f20e6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,6 +2,7 @@ import create from 'zustand'; import createContext from 'zustand/context'; import { clampPosition, getDimensions } from '../utils'; +import { applyNodeChanges } from '../utils/changes'; import { ReactFlowState, Node, @@ -9,13 +10,20 @@ import { NodeDimensionUpdate, NodeDiffUpdate, CoordinateExtent, - NodeChange, - EdgeChange, NodeDimensionChange, + EdgeSelectionChange, + NodeSelectionChange, } from '../types'; import { getHandleBounds } from '../components/Nodes/utils'; import { createSelectionChange, getSelectionChanges } from '../utils/changes'; -import { createNodeInternals, createPositionChange, fitView, isParentSelected } from './utils'; +import { + createNodeInternals, + createPositionChange, + fitView, + handleControlledEdgeSelectionChange, + handleControlledNodeSelectionChange, + isParentSelected, +} from './utils'; import initialState from './initialState'; const { Provider, useStore, useStoreApi } = createContext(); @@ -24,17 +32,25 @@ const createStore = () => create((set, get) => ({ ...initialState, setNodes: (nodes: Node[]) => { - const nodeInternals = createNodeInternals(nodes, get().nodeInternals); - - set({ nodeInternals }); + set({ nodeInternals: createNodeInternals(nodes, get().nodeInternals) }); }, setEdges: (edges: Edge[]) => { - set({ edges }); + const { defaultEdgeOptions } = get(); + + if (defaultEdgeOptions) { + set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) }); + } else { + set({ edges }); + } + }, + setDefaultNodesAndEdges: (nodes: Node[], edges: Edge[] = []) => { + const nodeInternals = createNodeInternals(nodes, get().nodeInternals); + set({ nodeInternals, edges, isControlled: true }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { const { onNodesChange, transform, nodeInternals, fitViewOnInit } = get(); - const changes: NodeChange[] = updates.reduce((res, update) => { + const changes: NodeDimensionChange[] = updates.reduce((res, update) => { const node = nodeInternals.get(update.id); if (node) { @@ -57,7 +73,7 @@ const createStore = () => id: node.id, type: 'dimensions', dimensions, - } as NodeChange); + }); } } @@ -65,7 +81,6 @@ const createStore = () => }, []); const fitViewOnInitDone = fitViewOnInit && fitView(get); - set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone }); if (changes?.length > 0) { @@ -73,9 +88,9 @@ const createStore = () => } }, updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { - const { onNodesChange, nodeExtent, nodeInternals } = get(); + const { onNodesChange, nodeExtent, nodeInternals, isControlled } = get(); - if (onNodesChange) { + if (isControlled || onNodesChange) { const changes: NodeDimensionChange[] = []; nodeInternals.forEach((node) => { @@ -89,57 +104,83 @@ const createStore = () => }); if (changes?.length) { - onNodesChange(changes); + if (isControlled) { + const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); + const nextNodeInternals = createNodeInternals(nodes, nodeInternals); + set({ nodeInternals: nextNodeInternals }); + } else { + onNodesChange?.(changes); + } } } }, addSelectedNodes: (selectedNodeIds: string[]) => { - const { multiSelectionActive, onNodesChange, nodeInternals } = get(); + const { multiSelectionActive, onNodesChange, nodeInternals, isControlled } = get(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); - let changedNodes; + let changedNodes: NodeSelectionChange[]; if (multiSelectionActive) { - changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)); + changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; } else { changedNodes = getSelectionChanges(nodes, selectedNodeIds); } if (changedNodes.length) { - onNodesChange?.(changedNodes as NodeChange[]); + if (isControlled) { + set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) }); + } else if (onNodesChange) { + onNodesChange(changedNodes); + } } }, addSelectedEdges: (selectedEdgeIds: string[]) => { - const { multiSelectionActive, onEdgesChange, edges } = get(); + const { multiSelectionActive, onEdgesChange, edges, isControlled } = get(); - let changedEdges; + let changedEdges: EdgeSelectionChange[]; if (multiSelectionActive) { - changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)); + changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; } else { changedEdges = getSelectionChanges(edges, selectedEdgeIds); } if (changedEdges.length) { - onEdgesChange?.(changedEdges as EdgeChange[]); + if (isControlled) { + set({ + edges: handleControlledEdgeSelectionChange(changedEdges, edges), + }); + } else if (onEdgesChange) { + onEdgesChange(changedEdges); + } } }, unselectNodesAndEdges: () => { - const { nodeInternals, edges, onNodesChange, onEdgesChange } = get(); + const { nodeInternals, edges, onNodesChange, onEdgesChange, isControlled } = get(); // @TODO: work with nodeInternals instead of converting it to an array const nodes = Array.from(nodeInternals).map(([_, node]) => node); const nodesToUnselect = nodes.map((n) => { n.selected = false; return createSelectionChange(n.id, false); - }) as NodeChange[]; - const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeChange[]; + }) as NodeSelectionChange[]; + const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[]; if (nodesToUnselect.length) { - onNodesChange?.(nodesToUnselect); + if (isControlled) { + set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals) }); + } else if (onNodesChange) { + onNodesChange(nodesToUnselect); + } } if (edgesToUnselect.length) { - onEdgesChange?.(edgesToUnselect); + if (isControlled) { + set({ + edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges), + }); + } else if (onEdgesChange) { + onEdgesChange(edgesToUnselect); + } } }, setMinZoom: (minZoom: number) => { @@ -161,17 +202,34 @@ const createStore = () => set({ translateExtent }); }, resetSelectedElements: () => { - const { nodeInternals, edges, onNodesChange, onEdgesChange } = get(); + const { nodeInternals, edges, onNodesChange, onEdgesChange, isControlled } = get(); // @TODO: work with nodeInternals instead of converting it to an array - const nodes = Array.from(nodeInternals).map(([_, node]) => node); - const nodesToUnselect = nodes.filter((e) => e.selected).map((n) => createSelectionChange(n.id, false)); - const edgesToUnselect = edges.filter((e) => e.selected).map((e) => createSelectionChange(e.id, false)); + const nodes = Array.from(nodeInternals.values()); + + 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[]; if (nodesToUnselect.length) { - onNodesChange?.(nodesToUnselect as NodeChange[]); + if (isControlled) { + set({ + nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals), + }); + } else if (onNodesChange) { + onNodesChange(nodesToUnselect); + } } if (edgesToUnselect.length) { - onEdgesChange?.(edgesToUnselect as EdgeChange[]); + if (isControlled) { + set({ + edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges), + }); + } else if (onEdgesChange) { + onEdgesChange(edgesToUnselect); + } } }, setNodeExtent: (nodeExtent: CoordinateExtent) => { diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 7d7326c0..ef826882 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -13,6 +13,7 @@ const initialState: ReactFlowStore = { edges: [], onNodesChange: null, onEdgesChange: null, + isControlled: false, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, d3Selection: null, diff --git a/src/store/utils.ts b/src/store/utils.ts index c548ae68..82375189 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -2,10 +2,13 @@ import { zoomIdentity } from 'd3-zoom'; import { GetState } from 'zustand'; import { CoordinateExtent, + Edge, + EdgeSelectionChange, Node, NodeDimensionChange, NodeInternals, NodeInternalsItem, + NodeSelectionChange, ReactFlowState, XYPosition, XYZPosition, @@ -163,3 +166,27 @@ export function fitView(get: GetState) { return fitViewOnInitDone; } + +export function handleControlledNodeSelectionChange(nodeChanges: NodeSelectionChange[], nodeInternals: NodeInternals) { + nodeChanges.forEach((change) => { + const node = nodeInternals.get(change.id); + if (node) { + nodeInternals.set(node.id, { + ...node, + selected: change.selected, + }); + } + }); + + return new Map(nodeInternals); +} + +export function handleControlledEdgeSelectionChange(edgeChanges: EdgeSelectionChange[], edges: Edge[]) { + return edges.map((e) => { + const change = edgeChanges.find((change) => change.id === e.id); + if (change) { + e.selected = change.selected; + } + return e; + }); +} diff --git a/src/types/edges.ts b/src/types/edges.ts index a9fa6d29..56500c37 100644 --- a/src/types/edges.ts +++ b/src/types/edges.ts @@ -32,6 +32,11 @@ export interface Edge { zIndex?: number; } +export type DefaultEdgeOptions = Omit< + Edge, + 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode' +>; + // props that get passed to a custom edge export interface EdgeProps { id: string; diff --git a/src/types/general.ts b/src/types/general.ts index 686f259e..b2284a1b 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -6,6 +6,7 @@ import { NodeChange, EdgeChange } from './changes'; import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes'; import { Edge } from './edges'; import { HandleType, StartHandle } from './handles'; +import { DefaultEdgeOptions } from '.'; export type NodeTypesType = { [key: string]: ReactNode }; export type EdgeTypesType = NodeTypesType; @@ -142,6 +143,7 @@ export type ReactFlowStore = { selectedNodesBbox: Rect; onNodesChange: OnNodesChange | null; onEdgesChange: OnEdgesChange | null; + isControlled: boolean; d3Zoom: ZoomBehavior | null; d3Selection: D3Selection | null; @@ -182,11 +184,13 @@ export type ReactFlowStore = { onConnectEnd?: OnConnectEnd; connectOnClick: boolean; + defaultEdgeOptions?: DefaultEdgeOptions; }; export type ReactFlowActions = { setNodes: (nodes: Node[]) => void; setEdges: (edges: Edge[]) => void; + setDefaultNodesAndEdges: (nodes: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodePosition: (update: NodeDiffUpdate) => void; resetSelectedElements: () => void;