refactor(state): replace redux with zustand

This commit is contained in:
moklick
2021-10-13 12:54:01 +02:00
parent e01249a87c
commit ee0d29029a
38 changed files with 786 additions and 896 deletions
-176
View File
@@ -1,176 +0,0 @@
import { createAction } from './utils';
import {
Node,
Edge,
Elements,
OnConnectEndFunc,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
NodeDimensionUpdate,
NodePosUpdate,
NodeDiffUpdate,
XYPosition,
Transform,
Dimensions,
InitD3ZoomPayload,
TranslateExtent,
SetConnectionId,
SnapGrid,
ConnectionMode,
NodeExtent,
OnElementsChange,
} from '../types';
import * as constants from './contants';
export const setOnConnect = (onConnect: OnConnectFunc) =>
createAction(constants.SET_ON_CONNECT, {
onConnect,
});
export const setOnConnectStart = (onConnectStart: OnConnectStartFunc) =>
createAction(constants.SET_ON_CONNECT_START, {
onConnectStart,
});
export const setOnConnectStop = (onConnectStop: OnConnectStopFunc) =>
createAction(constants.SET_ON_CONNECT_STOP, {
onConnectStop,
});
export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) =>
createAction(constants.SET_ON_CONNECT_END, {
onConnectEnd,
});
export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes);
export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges);
export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) =>
createAction(constants.UPDATE_NODE_DIMENSIONS, updates);
export const updateNodePos = (payload: NodePosUpdate) => createAction(constants.UPDATE_NODE_POS, payload);
export const updateNodePosDiff = (payload: NodeDiffUpdate) => createAction(constants.UPDATE_NODE_POS_DIFF, payload);
export const setUserSelection = (mousePos: XYPosition) => createAction(constants.SET_USER_SELECTION, mousePos);
export const updateUserSelection = (mousePos: XYPosition) => createAction(constants.UPDATE_USER_SELECTION, mousePos);
export const unsetUserSelection = () => createAction(constants.UNSET_USER_SELECTION);
export const setSelection = (selectionActive: boolean) =>
createAction(constants.SET_SELECTION, {
selectionActive,
});
export const unsetNodesSelection = () =>
createAction(constants.UNSET_NODES_SELECTION, {
nodesSelectionActive: false,
});
export const resetSelectedElements = () =>
createAction(constants.RESET_SELECTED_ELEMENTS, {
selectedElements: null,
});
export const setSelectedElements = (elements: Elements) => createAction(constants.SET_SELECTED_ELEMENTS, elements);
export const addSelectedElements = (elements: Elements) => createAction(constants.ADD_SELECTED_ELEMENTS, elements);
export const updateTransform = (transform: Transform) => createAction(constants.UPDATE_TRANSFORM, { transform });
export const updateSize = (size: Dimensions) =>
createAction(constants.UPDATE_SIZE, {
width: size.width || 500,
height: size.height || 500,
});
export const initD3Zoom = (payload: InitD3ZoomPayload) => createAction(constants.INIT_D3ZOOM, payload);
export const setMinZoom = (minZoom: number) => createAction(constants.SET_MINZOOM, minZoom);
export const setMaxZoom = (maxZoom: number) => createAction(constants.SET_MAXZOOM, maxZoom);
export const setTranslateExtent = (translateExtent: TranslateExtent) =>
createAction(constants.SET_TRANSLATEEXTENT, translateExtent);
export const setConnectionPosition = (connectionPosition: XYPosition) =>
createAction(constants.SET_CONNECTION_POSITION, { connectionPosition });
export const setConnectionNodeId = (payload: SetConnectionId) => createAction(constants.SET_CONNECTION_NODEID, payload);
export const setSnapToGrid = (snapToGrid: boolean) => createAction(constants.SET_SNAPTOGRID, { snapToGrid });
export const setSnapGrid = (snapGrid: SnapGrid) => createAction(constants.SET_SNAPGRID, { snapGrid });
export const setInteractive = (isInteractive: boolean) =>
createAction(constants.SET_INTERACTIVE, {
nodesDraggable: isInteractive,
nodesConnectable: isInteractive,
elementsSelectable: isInteractive,
});
export const setNodesDraggable = (nodesDraggable: boolean) =>
createAction(constants.SET_NODES_DRAGGABLE, { nodesDraggable });
export const setNodesConnectable = (nodesConnectable: boolean) =>
createAction(constants.SET_NODES_CONNECTABLE, { nodesConnectable });
export const setElementsSelectable = (elementsSelectable: boolean) =>
createAction(constants.SET_ELEMENTS_SELECTABLE, { elementsSelectable });
export const setMultiSelectionActive = (multiSelectionActive: boolean) =>
createAction(constants.SET_MULTI_SELECTION_ACTIVE, { multiSelectionActive });
export const setConnectionMode = (connectionMode: ConnectionMode) =>
createAction(constants.SET_CONNECTION_MODE, { connectionMode });
export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent);
export const setOnNodesChange = (onNodesChange: OnElementsChange) =>
createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange });
export const setOnEdgesChange = (onEdgesChange: OnElementsChange) =>
createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange });
export type ReactFlowAction = ReturnType<
| typeof setOnConnect
| typeof setOnConnectStart
| typeof setOnConnectStop
| typeof setOnConnectEnd
| typeof setNodes
| typeof setEdges
| typeof updateNodeDimensions
| typeof updateNodePos
| typeof updateNodePosDiff
| typeof setUserSelection
| typeof updateUserSelection
| typeof unsetUserSelection
| typeof setSelection
| typeof unsetNodesSelection
| typeof resetSelectedElements
| typeof setSelectedElements
| typeof addSelectedElements
| typeof updateTransform
| typeof updateSize
| typeof initD3Zoom
| typeof setMinZoom
| typeof setMaxZoom
| typeof setTranslateExtent
| typeof setConnectionPosition
| typeof setConnectionNodeId
| typeof setSnapToGrid
| typeof setSnapGrid
| typeof setInteractive
| typeof setNodesDraggable
| typeof setNodesConnectable
| typeof setElementsSelectable
| typeof setMultiSelectionActive
| typeof setConnectionMode
| typeof setNodeExtent
| typeof setOnNodesChange
| typeof setOnEdgesChange
>;
-11
View File
@@ -1,11 +0,0 @@
import { createStore, applyMiddleware, Store } from 'redux';
import thunk from 'redux-thunk';
import { ReactFlowState } from '../types';
import { ReactFlowAction } from './actions';
import reactFlowReducer from './reducer';
export default function configureStore(preloadedState: ReactFlowState): Store<ReactFlowState, ReactFlowAction> {
const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk));
return store;
}
-36
View File
@@ -1,36 +0,0 @@
export const SET_ON_CONNECT = 'SET_ON_CONNECT';
export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START';
export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP';
export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END';
export const SET_NODES = 'SET_NODES';
export const SET_EDGES = 'SET_EDGES';
export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS';
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF';
export const SET_USER_SELECTION = 'SET_USER_SELECTION';
export const UPDATE_USER_SELECTION = 'UPDATE_USER_SELECTION';
export const UNSET_USER_SELECTION = 'UNSET_USER_SELECTION';
export const SET_SELECTION = 'SET_SELECTION';
export const UNSET_NODES_SELECTION = 'UNSET_NODES_SELECTION';
export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS';
export const RESET_SELECTED_ELEMENTS = 'RESET_SELECTED_ELEMENTS';
export const ADD_SELECTED_ELEMENTS = 'ADD_SELECTED_ELEMENTS';
export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
export const UPDATE_SIZE = 'UPDATE_SIZE';
export const INIT_D3ZOOM = 'INIT_D3ZOOM';
export const SET_MINZOOM = 'SET_MINZOOM';
export const SET_MAXZOOM = 'SET_MAXZOOM';
export const SET_TRANSLATEEXTENT = 'SET_TRANSLATEEXTENT';
export const SET_CONNECTION_POSITION = 'SET_CONNECTION_POSITION';
export const SET_CONNECTION_NODEID = 'SET_CONNECTION_NODEID';
export const SET_SNAPTOGRID = 'SET_SNAPTOGRID';
export const SET_SNAPGRID = 'SET_SNAPGRID';
export const SET_INTERACTIVE = 'SET_INTERACTIVE';
export const SET_NODES_DRAGGABLE = 'SET_NODES_DRAGGABLE';
export const SET_NODES_CONNECTABLE = 'SET_NODES_CONNECTABLE';
export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE';
export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE';
export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE';
export const SET_NODE_EXTENT = 'SET_NODE_EXTENT';
export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE';
export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE';
-48
View File
@@ -1,48 +0,0 @@
import { bindActionCreators, Store, ActionCreator, ActionCreatorsMapObject } from 'redux';
import {
useStore as useStoreRedux,
useSelector,
useDispatch as reduxUseDispatch,
TypedUseSelectorHook,
} from 'react-redux';
import { useMemo } from 'react';
import { ReactFlowDispatch } from './index';
import * as actions from './actions';
import { ReactFlowAction } from './actions';
import { ReactFlowState } from '../types';
export const useTypedSelector: TypedUseSelectorHook<ReactFlowState> = useSelector;
export type ActionCreatorSelector<Action> = (acts: typeof actions) => ActionCreator<Action>;
export type ActionMapObjectSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action>;
export type ActionSelector<Action> = (acts: typeof actions) => ActionCreatorsMapObject<Action> | ActionCreator<Action>;
export function useStoreActions<Action extends ReactFlowAction>(
actionSelector: ActionCreatorSelector<Action>
): ActionCreator<Action>;
export function useStoreActions<Action extends ReactFlowAction>(
actionSelector: ActionMapObjectSelector<Action>
): ActionCreatorsMapObject<Action>;
export function useStoreActions<Action extends ReactFlowAction>(actionSelector: ActionSelector<Action>) {
const dispatch: ReactFlowDispatch = reduxUseDispatch();
const currAction = actionSelector(actions);
const action = useMemo(() => {
// this looks weird but required if both ActionSelector and ActionMapObjectSelector are supported
return typeof currAction === 'function'
? bindActionCreators(currAction, dispatch)
: bindActionCreators(currAction, dispatch);
}, [dispatch, currAction]);
return action;
}
export const useStoreState = useTypedSelector;
export const useStore = (): Store<ReactFlowState, ReactFlowAction> => {
const store = useStoreRedux<ReactFlowState, ReactFlowAction>();
return store;
};
export const useDispatch: ReactFlowDispatch = reduxUseDispatch;
+398 -54
View File
@@ -1,66 +1,410 @@
import configureStore from './configure-store';
import create from 'zustand';
import createContext from 'zustand/context';
import isEqual from 'fast-deep-equal';
import { ReactFlowState, ConnectionMode } from '../types';
import { clampPosition, getDimensions } from '../utils';
import {
ReactFlowState,
ConnectionMode,
Node,
Edge,
ElementChange,
NodeDimensionUpdate,
NodeDiffUpdate,
XYPosition,
Elements,
InitD3ZoomPayload,
TranslateExtent,
NodeExtent,
Transform,
Dimensions,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
SetConnectionId,
SnapGrid,
OnElementsChange,
} from '../types';
import { parseNode, parseEdge, isNode, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph';
import { getSourceTargetNodes } from '../container/EdgeRenderer/utils';
import { getHandleBounds } from '../components/Nodes/utils';
export const initialState: ReactFlowState = {
width: 0,
height: 0,
transform: [0, 0, 1],
nodes: [],
edges: [],
onNodesChange: null,
onEdgesChange: null,
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
nodeExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
nodesSelectionActive: false,
selectionActive: false,
userSelectionRect: {
startX: 0,
startY: 0,
x: 0,
y: 0,
const createStore = () =>
create<ReactFlowState>((set, get) => ({
width: 0,
height: 0,
draw: false,
},
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
transform: [0, 0, 1],
nodes: [],
edges: [],
onNodesChange: null,
onEdgesChange: null,
snapGrid: [15, 15],
snapToGrid: false,
selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
minZoom: 0.5,
maxZoom: 2,
translateExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
multiSelectionActive: false,
nodeExtent: [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
};
nodesSelectionActive: false,
selectionActive: false,
const store = configureStore(initialState);
userSelectionRect: {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
},
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionMode: ConnectionMode.Strict,
export type ReactFlowDispatch = typeof store.dispatch;
snapGrid: [15, 15],
snapToGrid: false,
export default store;
nodesDraggable: true,
nodesConnectable: true,
elementsSelectable: true,
multiSelectionActive: false,
reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-',
setNodes: (propNodes: Node[]) => {
const { nodes, edges, nodeExtent } = get();
const nextNodes = propNodes.map((propNode: Node) => {
const storeNode = nodes.find((node) => node.id === propNode.id);
if (storeNode) {
if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) {
const updatedNode: Node = {
...storeNode,
...propNode,
};
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.width = null;
return updatedNode;
}
}
return parseNode(propNode, nodeExtent);
});
const updatedEdges = edges.map((edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes);
if (sourceNode) {
edge.sourceNode = sourceNode;
}
if (targetNode) {
edge.targetNode = targetNode;
}
return edge;
});
set({
nodes: nextNodes,
edges: updatedEdges,
});
},
setEdges: (propEdges: Edge[]) => {
const { edges, nodes } = get();
const nextEdges = propEdges.map((propEdge: Edge) => {
const storeEdge = edges.find((se) => se.id === propEdge.id);
if (storeEdge) {
return parseEdge(propEdge);
} else {
const parsedEdge = parseEdge(propEdge);
const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, nodes);
if (sourceNode) {
parsedEdge.sourceNode = sourceNode;
}
if (targetNode) {
parsedEdge.targetNode = targetNode;
}
return parsedEdge;
}
});
set({ edges: nextEdges });
},
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
const { onNodesChange, nodes, transform } = get();
const initialChanges: ElementChange[] = [];
const nodesToChange: ElementChange[] = nodes.reduce((res, node) => {
const update = updates.find((u) => u.id === node.id);
if (update) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate =
dimensions.width &&
dimensions.height &&
(node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate);
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, transform[2]);
const change = {
id: node.id,
change: {
...dimensions,
handleBounds,
},
} as ElementChange;
res.push(change);
}
}
return res;
}, initialChanges);
if (onNodesChange) {
onNodesChange(nodesToChange);
}
},
updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => {
const { onNodesChange, nodes } = get();
if (onNodesChange && id && diff) {
const matchingNode = nodes.find((n) => n.id === id);
if (matchingNode) {
requestAnimationFrame(() =>
onNodesChange([
{
id,
change: {
position: {
x: matchingNode.position.x + diff.x,
y: matchingNode.position.y + diff.y,
isDragging,
},
},
},
])
);
}
}
},
setUserSelection: (mousePos: XYPosition) => {
set({
selectionActive: true,
userSelectionRect: {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
},
});
},
updateUserSelection: (mousePos: XYPosition) => {
const { userSelectionRect, nodes, edges, transform, selectedElements } = get();
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : userSelectionRect.x,
y: mousePos.y < startY ? mousePos.y : userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true);
const selectedEdges = getConnectedEdges(selectedNodes, edges);
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
const selectedElementsChanged = !isEqual(nextSelectedElements, selectedElements);
if (selectedElementsChanged) {
set({
selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null,
userSelectionRect: nextUserSelectRect,
});
} else {
set({
userSelectionRect: nextUserSelectRect,
});
}
},
unsetUserSelection: () => {
const { selectedElements, userSelectionRect } = get();
const selectedNodes = selectedElements?.filter((node) => isNode(node) && node.position) as Node[];
const stateUpdate = {
selectionActive: false,
userSelectionRect: {
...userSelectionRect,
draw: false,
},
selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
nodesSelectionActive: false,
};
if (selectedNodes && selectedNodes.length > 0) {
const selectedNodesBbox = getRectOfNodes(selectedNodes);
stateUpdate.selectedNodesBbox = selectedNodesBbox;
stateUpdate.nodesSelectionActive = true;
}
set(stateUpdate);
},
setSelectedElements: (elements: Elements) => {
const { selectedElements } = get();
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
const selectedElementsUpdated = !isEqual(selectedElementsArr, selectedElements);
set({
selectedElements: selectedElementsUpdated ? selectedElementsArr : selectedElements,
});
},
addSelectedElements: (elements: Elements) => {
const { multiSelectionActive, selectedElements } = get();
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
let nextElements = selectedElementsArr;
if (multiSelectionActive) {
nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
}
const selectedElementsUpdated = !isEqual(nextElements, selectedElements);
set({ selectedElements: selectedElementsUpdated ? nextElements : selectedElements });
},
initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => {
set({
d3Zoom,
d3Selection,
d3ZoomHandler,
transform,
});
},
setMinZoom: (minZoom: number) => {
const { d3Zoom, maxZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
set({ minZoom });
},
setMaxZoom: (maxZoom: number) => {
const { d3Zoom, minZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
set({ maxZoom });
},
setTranslateExtent: (translateExtent: TranslateExtent) => {
const { d3Zoom } = get();
d3Zoom?.translateExtent(translateExtent);
set({ translateExtent });
},
setNodeExtent: (nodeExtent: NodeExtent) => {
set({
nodeExtent,
nodes: get().nodes.map((node) => {
return {
...node,
position: clampPosition(node.position, nodeExtent),
__rf: {
...node.__rf,
},
};
}),
});
},
unsetNodesSelection: () => {
set({ nodesSelectionActive: false });
},
resetSelectedElements: () => {
set({ selectedElements: null });
},
updateTransform: (transform: Transform) => {
set({ transform });
},
updateSize: (size: Dimensions) => {
set({ width: size.width || 500, height: size.height || 500 });
},
setOnConnect: (onConnect: OnConnectFunc) => {
set({ onConnect });
},
setOnConnectStart: (onConnectStart: OnConnectStartFunc) => {
set({ onConnectStart });
},
setOnConnectStop: (onConnectStop: OnConnectStopFunc) => {
set({ onConnectStop });
},
setOnConnectEnd: (onConnectEnd: OnConnectEndFunc) => {
set({ onConnectEnd });
},
setConnectionPosition: (connectionPosition: XYPosition) => {
set({ connectionPosition });
},
setConnectionNodeId: (params: SetConnectionId) => {
set({ ...params });
},
setSnapToGrid: (snapToGrid: boolean) => {
set({ snapToGrid });
},
setSnapGrid: (snapGrid: SnapGrid) => {
set({ snapGrid });
},
setInteractive: (isInteractive: boolean) => {
set({
nodesDraggable: isInteractive,
nodesConnectable: isInteractive,
elementsSelectable: isInteractive,
});
},
setNodesDraggable: (nodesDraggable: boolean) => {
set({ nodesDraggable });
},
setNodesConnectable: (nodesConnectable: boolean) => {
set({ nodesConnectable });
},
setElementsSelectable: (elementsSelectable: boolean) => {
set({ elementsSelectable });
},
setMultiSelectionActive: (multiSelectionActive: boolean) => {
set({ multiSelectionActive });
},
setConnectionMode: (connectionMode: ConnectionMode) => {
set({ connectionMode });
},
setOnNodesChange: (onNodesChange: OnElementsChange) => {
set({ onNodesChange });
},
setOnEdgesChange: (onEdgesChange: OnElementsChange) => {
set({ onEdgesChange });
},
}));
export { Provider, useStore, createStore, useStoreApi };
-350
View File
@@ -1,350 +0,0 @@
import isEqual from 'fast-deep-equal';
import { clampPosition, getDimensions } from '../utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils';
import { getSourceTargetNodes } from '../container/EdgeRenderer/utils';
import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types';
import * as constants from './contants';
import { ReactFlowAction } from './actions';
import { initialState } from './index';
export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState {
switch (action.type) {
case constants.SET_NODES: {
const propNodes = action.payload;
const nextNodes = propNodes.map((propNode: Node) => {
const storeNode = state.nodes.find((node) => node.id === propNode.id);
if (storeNode) {
if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) {
const updatedNode: Node = {
...storeNode,
...propNode,
};
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.width = null;
return updatedNode;
}
}
return parseNode(propNode, state.nodeExtent);
});
const updatedEdges = state.edges.map((edge) => {
const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes);
if (sourceNode) {
edge.sourceNode = sourceNode;
}
if (targetNode) {
edge.targetNode = targetNode;
}
return edge;
});
return { ...state, nodes: nextNodes, edges: updatedEdges };
}
case constants.SET_EDGES: {
const propElements = action.payload;
const nextEdges = propElements.map((propEdge: Edge) => {
const storeEdge = state.edges.find((se) => se.id === propEdge.id);
if (storeEdge) {
return parseEdge(propEdge);
} else {
const parsedEdge = parseEdge(propEdge);
const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes);
if (sourceNode) {
parsedEdge.sourceNode = sourceNode;
}
if (targetNode) {
parsedEdge.targetNode = targetNode;
}
return parsedEdge;
}
});
return { ...state, edges: nextEdges };
}
case constants.UPDATE_NODE_DIMENSIONS: {
const initialChanges: ElementChange[] = [];
const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => {
const update = action.payload.find((u) => u.id === node.id);
if (update) {
const dimensions = getDimensions(update.nodeElement);
const doUpdate =
dimensions.width &&
dimensions.height &&
(node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate);
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]);
const change = {
id: node.id,
change: {
...dimensions,
handleBounds,
},
} as ElementChange;
res.push(change);
}
}
return res;
}, initialChanges);
if (state.onNodesChange) {
requestAnimationFrame(() => state.onNodesChange?.(nodesToChange));
}
return state;
}
case constants.UPDATE_NODE_POS: {
const { id, pos } = action.payload;
let position: XYPosition = pos;
if (state.snapToGrid) {
const [gridSizeX, gridSizeY] = state.snapGrid;
position = {
x: gridSizeX * Math.round(pos.x / gridSizeX),
y: gridSizeY * Math.round(pos.y / gridSizeY),
};
}
if (state.onNodesChange) {
state.onNodesChange([{ id, change: { position } }]);
return state;
}
const nextNodes = state.nodes.map((node) => {
if (node.id === id) {
return {
...node,
position,
__rf: {
...node.__rf,
},
};
}
return node;
});
return { ...state, nodes: nextNodes };
}
case constants.UPDATE_NODE_POS_DIFF: {
const { id, diff, isDragging } = action.payload;
if (state.onNodesChange && id && diff) {
const matchingNode = state.nodes.find((n) => n.id === id);
if (matchingNode) {
requestAnimationFrame(() =>
state.onNodesChange?.([
{
id,
change: {
position: {
x: matchingNode.position.x + diff.x,
y: matchingNode.position.y + diff.y,
isDragging,
},
},
},
])
);
}
}
return state;
}
case constants.SET_USER_SELECTION: {
const mousePos = action.payload;
return {
...state,
selectionActive: true,
userSelectionRect: {
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
},
};
}
case constants.UPDATE_USER_SELECTION: {
const mousePos = action.payload;
const startX = state.userSelectionRect.startX ?? 0;
const startY = state.userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...state.userSelectionRect,
x: mousePos.x < startX ? mousePos.x : state.userSelectionRect.x,
y: mousePos.y < startY ? mousePos.y : state.userSelectionRect.y,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform, false, true);
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
const selectedElementsChanged = !isEqual(nextSelectedElements, state.selectedElements);
const selectedElementsUpdate = selectedElementsChanged
? {
selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null,
}
: {};
return {
...state,
...selectedElementsUpdate,
userSelectionRect: nextUserSelectRect,
};
}
case constants.UNSET_USER_SELECTION: {
const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[];
const stateUpdate = {
...state,
selectionActive: false,
userSelectionRect: {
...state.userSelectionRect,
draw: false,
},
};
if (!selectedNodes || selectedNodes.length === 0) {
stateUpdate.selectedElements = null;
stateUpdate.nodesSelectionActive = false;
} else {
const selectedNodesBbox = getRectOfNodes(selectedNodes);
stateUpdate.selectedNodesBbox = selectedNodesBbox;
stateUpdate.nodesSelectionActive = true;
}
return stateUpdate;
}
case constants.SET_SELECTED_ELEMENTS: {
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
return {
...state,
selectedElements,
};
}
case constants.ADD_SELECTED_ELEMENTS: {
const { multiSelectionActive, selectedElements } = state;
const elements = action.payload;
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
let nextElements = selectedElementsArr;
if (multiSelectionActive) {
nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr;
}
const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements);
const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements;
return { ...state, selectedElements: nextSelectedElements };
}
case constants.INIT_D3ZOOM: {
const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload;
return {
...state,
d3Zoom,
d3Selection,
d3ZoomHandler,
transform,
};
}
case constants.SET_MINZOOM: {
const minZoom = action.payload;
state.d3Zoom?.scaleExtent([minZoom, state.maxZoom]);
return {
...state,
minZoom,
};
}
case constants.SET_MAXZOOM: {
const maxZoom = action.payload;
state.d3Zoom?.scaleExtent([state.minZoom, maxZoom]);
return {
...state,
maxZoom,
};
}
case constants.SET_TRANSLATEEXTENT: {
const translateExtent = action.payload;
state.d3Zoom?.translateExtent(translateExtent);
return {
...state,
translateExtent,
};
}
case constants.SET_NODE_EXTENT: {
const nodeExtent = action.payload;
return {
...state,
nodeExtent,
nodes: state.nodes.map((node) => {
return {
...node,
position: clampPosition(node.position, nodeExtent),
__rf: {
...node.__rf,
},
};
}),
};
}
case constants.SET_ON_CONNECT:
case constants.SET_ON_CONNECT_START:
case constants.SET_ON_CONNECT_STOP:
case constants.SET_ON_CONNECT_END:
case constants.RESET_SELECTED_ELEMENTS:
case constants.UNSET_NODES_SELECTION:
case constants.UPDATE_TRANSFORM:
case constants.UPDATE_SIZE:
case constants.SET_CONNECTION_POSITION:
case constants.SET_CONNECTION_NODEID:
case constants.SET_SNAPTOGRID:
case constants.SET_SNAPGRID:
case constants.SET_INTERACTIVE:
case constants.SET_NODES_DRAGGABLE:
case constants.SET_NODES_CONNECTABLE:
case constants.SET_ELEMENTS_SELECTABLE:
case constants.SET_MULTI_SELECTION_ACTIVE:
case constants.SET_CONNECTION_MODE:
case constants.SET_ON_NODES_CHANGE:
case constants.SET_ON_EDGES_CHANGE:
return { ...state, ...action.payload };
default:
return state;
}
}
-5
View File
@@ -1,5 +0,0 @@
export function createAction<T extends string>(type: T): { type: T };
export function createAction<T extends string, P extends any>(type: T, payload: P): { type: T; payload: P };
export function createAction(type: string, payload?: any) {
return { type, payload };
}