From feca64048ab6f903212444e89af9353d92791e96 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 20 Aug 2019 17:48:04 +0200 Subject: [PATCH] refactor(state): use easy-peasy --- package-lock.json | 61 ++++++++ package.json | 1 + src/ConnectionContext/index.js | 27 ---- src/EdgeRenderer/EdgeTypes/wrapEdge.js | 6 +- src/EdgeRenderer/index.js | 30 ++-- src/GlobalKeyHandler/index.js | 10 +- src/GraphContext/index.js | 73 ---------- src/GraphView/index.js | 60 ++++---- src/NodeRenderer/HandleTypes/SourceHandle.js | 14 +- src/NodeRenderer/HandleTypes/TargetHandle.js | 8 +- src/NodeRenderer/NodeTypes/wrapNode.js | 20 +-- src/NodeRenderer/index.js | 23 +-- src/NodesSelection/index.js | 21 +-- src/Plugins/MiniMap/index.js | 14 +- src/ReactGraph/ElementUpdater.js | 59 ++++++++ src/ReactGraph/index.js | 13 +- src/UserSelection/index.js | 18 +-- src/actions.js | 102 +++++++++++++ src/graph-utils.js | 31 +++- src/hooks/useD3Zoom.js | 22 ++- src/state/actions.js | 52 ------- src/state/index.js | 142 ------------------- src/state/view-actions.js | 32 ----- src/store.js | 30 ++++ 24 files changed, 434 insertions(+), 435 deletions(-) delete mode 100644 src/ConnectionContext/index.js delete mode 100644 src/GraphContext/index.js create mode 100644 src/ReactGraph/ElementUpdater.js create mode 100644 src/actions.js delete mode 100644 src/state/actions.js delete mode 100644 src/state/index.js delete mode 100644 src/state/view-actions.js create mode 100644 src/store.js diff --git a/package-lock.json b/package-lock.json index f07e400f..ef5955e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3057,6 +3057,20 @@ "readable-stream": "^2.0.2" } }, + "easy-peasy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/easy-peasy/-/easy-peasy-3.0.2.tgz", + "integrity": "sha512-qcsr81F6rZN5hEZ1lYRDUHtatxn5Qb4cF5uj5cNXKEXLXmATCxktNC53B2XvccA7K8zSCwUeoFckrCOvLeINWA==", + "requires": { + "immer-peasy": "3.1.3", + "memoizerific": "^1.11.3", + "prop-types": "^15.6.2", + "redux": "^4.0.4", + "redux-thunk": "^2.3.0", + "shallowequal": "^1.1.0", + "ts-toolbelt": "^3.8.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4523,6 +4537,11 @@ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, + "immer-peasy": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/immer-peasy/-/immer-peasy-3.1.3.tgz", + "integrity": "sha512-WzoZ96A93jOmcDOLNChMWAqy+ZU8vEYQx2DcKjgo7P5SToiJs+GL+5yQbWzH8X02Lhvv6xrGgVNa1xbki66Eow==" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -5207,6 +5226,11 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "map-or-similar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", + "integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg=" + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -5287,6 +5311,14 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "dev": true }, + "memoizerific": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", + "integrity": "sha1-fIekZGREwy11Q4VwkF8tvRsagFo=", + "requires": { + "map-or-similar": "^1.5.0" + } + }, "merge-source-map": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", @@ -7090,6 +7122,20 @@ "readable-stream": "^2.0.2" } }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -7700,6 +7746,11 @@ "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=", "dev": true }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -8179,6 +8230,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -8321,6 +8377,11 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "ts-toolbelt": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.1.tgz", + "integrity": "sha512-XDAWEHVscqto6pG0gPSbYaEfEBcl9V0JK5pQIQJ3M+6sYnxK5ZOPyyRA/aq78WxKlLvBSXCPJ1fPX+7qCCg2Zg==" + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", diff --git a/package.json b/package.json index c17a5fce..b28620ca 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "classnames": "^2.2.6", "d3-selection": "^1.4.0", "d3-zoom": "^1.7.3", + "easy-peasy": "^3.0.2", "fast-deep-equal": "^3.0.0-beta.1", "react": "^16.9.0", "react-dom": "^16.9.0", diff --git a/src/ConnectionContext/index.js b/src/ConnectionContext/index.js deleted file mode 100644 index 1e286b18..00000000 --- a/src/ConnectionContext/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import React, { createContext, useState, memo, useMemo } from 'react'; - -export const ConnectionContext = createContext({}); - -export const Provider = memo(({ onConnect, children }) => { - const [sourceId, setSourceId] = useState(null); - const [position, setPosition] = useState({ x: 0, y: 0 }); - - const connectionContext = useMemo(() => ({ - sourceId, - setSourceId, - position, - setPosition, - onConnect - }), [sourceId, position.x, position.y]); - - return ( - - {children} - - ); -}); - -Provider.displayName = 'ConnectionProvider'; -Provider.whyDidYouRender = false; - -export const { Consumer } = ConnectionContext; diff --git a/src/EdgeRenderer/EdgeTypes/wrapEdge.js b/src/EdgeRenderer/EdgeTypes/wrapEdge.js index 7cc25650..7a88aa25 100644 --- a/src/EdgeRenderer/EdgeTypes/wrapEdge.js +++ b/src/EdgeRenderer/EdgeTypes/wrapEdge.js @@ -1,14 +1,14 @@ import React, { memo } from 'react'; import cx from 'classnames'; -import { setSelectedElements } from '../../state/actions'; import { isInputNode } from '../../utils'; +import store from '../../store'; export default EdgeComponent => { const EdgeWrapper = memo((props) => { const { source, target, animated, type, - dispatch, selected, onClick + selected, onClick } = props; const edgeClasses = cx('react-graph__edge', { selected, animated }); const onEdgeClick = (evt) => { @@ -16,7 +16,7 @@ export default EdgeComponent => { return false; } - dispatch(setSelectedElements({ source, target })); + store.dispatch.setSelectedElements({ source, target }); onClick({ source, target, type }); }; diff --git a/src/EdgeRenderer/index.js b/src/EdgeRenderer/index.js index 0e5afa54..c66e09ec 100644 --- a/src/EdgeRenderer/index.js +++ b/src/EdgeRenderer/index.js @@ -1,7 +1,6 @@ -import React, { memo, useContext } from 'react'; +import React, { memo } from 'react'; +import { useStoreState } from 'easy-peasy'; -import { GraphContext } from '../GraphContext'; -import { ConnectionContext } from '../ConnectionContext'; import ConnectionLine from '../ConnectionLine'; import { isEdge } from '../graph-utils'; @@ -36,10 +35,10 @@ function getEdgePositions(sourceNode, targetNode) { }; } -function renderEdge(e, props, graphContext) { +function renderEdge(e, props, state) { const edgeType = e.type || 'default'; - const sourceNode = graphContext.state.nodes.find(n => n.id === e.source); - const targetNode = graphContext.state.nodes.find(n => n.id === e.target); + const sourceNode = state.nodes.find(n => n.id === e.source); + const targetNode = state.nodes.find(n => n.id === e.target); if (!sourceNode) { throw new Error(`couldn't create edge for source id: ${e.source}`); @@ -51,7 +50,7 @@ function renderEdge(e, props, graphContext) { const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; const { sourceX, sourceY, targetX, targetY } = getEdgePositions(sourceNode, targetNode); - const selected = graphContext.state.selectedElements + const selected = state.selectedElements .filter(isEdge) .find(elm => elm.source === e.source && elm.target === e.target); @@ -62,7 +61,6 @@ function renderEdge(e, props, graphContext) { type={e.type} onClick={props.onElementClick} selected={selected} - dispatch={graphContext.dispatch} animated={e.animated} style={e.style} source={e.source} @@ -76,8 +74,14 @@ function renderEdge(e, props, graphContext) { } const EdgeRenderer = memo((props) => { - const graphContext = useContext(GraphContext); - const { position, sourceId : connectionSourceId } = useContext(ConnectionContext); + const state = useStoreState(s => ({ + nodes: s.nodes, + edges: s.edges, + transform: s.transform, + selectedElements: s.selectedElements, + connectionSourceId: s.connectionSourceId, + position: s.connectionPosition + })); const { width, height, connectionLineStyle, connectionLineType } = props; @@ -86,7 +90,7 @@ const EdgeRenderer = memo((props) => { return null; } - const { transform, edges, nodes } = graphContext.state; + const { transform, edges, nodes, connectionSourceId, position } = state; const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`; return ( @@ -96,7 +100,7 @@ const EdgeRenderer = memo((props) => { className="react-graph__edges" > - {edges.map(e => renderEdge(e, props, graphContext))} + {edges.map(e => renderEdge(e, props, state))} {connectionSourceId && ( { ); }); +EdgeRenderer.displayName = 'EdgeRenderer'; + export default EdgeRenderer; diff --git a/src/GlobalKeyHandler/index.js b/src/GlobalKeyHandler/index.js index 2c079f1e..96b38832 100644 --- a/src/GlobalKeyHandler/index.js +++ b/src/GlobalKeyHandler/index.js @@ -1,12 +1,12 @@ -import { useEffect, useContext, memo } from 'react'; +import { useEffect, memo } from 'react'; +import { useStoreState, useStoreActions } from 'easy-peasy'; import useKeyPress from '../hooks/useKeyPress'; -import { setNodesSelection } from '../state/actions'; -import { GraphContext } from '../GraphContext'; import { isEdge, getConnectedEdges } from '../graph-utils'; export default memo((props) => { - const { state, dispatch } = useContext(GraphContext); + const state = useStoreState(s => ({ selectedElements: s.selectedElements, edges: s.edges })) + const setNodesSelection = useStoreActions(a => a.setNodesSelection); const removePressed = useKeyPress('Backspace'); useEffect(() => { @@ -20,7 +20,7 @@ export default memo((props) => { } props.onElementsRemove(elementsToRemove); - dispatch(setNodesSelection({ isActive: false })); + setNodesSelection({ isActive: false }); } }, [removePressed]) diff --git a/src/GraphContext/index.js b/src/GraphContext/index.js deleted file mode 100644 index f6c2223a..00000000 --- a/src/GraphContext/index.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, { createContext, useEffect, useReducer, useMemo } from 'react'; -import PropTypes from 'prop-types'; -import isEqual from 'fast-deep-equal'; - -import { reducer, initialState } from '../state'; -import { setNodes, setEdges } from '../state/actions'; -import { parseElement, isNode, isEdge } from '../graph-utils'; - -export const GraphContext = createContext({}); - -export const Provider = ({ - elements, - children, -}) => { - const [state, dispatch] = useReducer(reducer, initialState); - - useEffect(() => { - const nodes = elements.filter(isNode); - const edges = elements.filter(isEdge).map(parseElement, state.transform); - - const nextNodes = nodes.map(propNode => { - const existingNode = state.nodes.find(n => n.id === propNode.id); - - if (existingNode) { - const data = !isEqual(existingNode.data, propNode.data) ? - { ...existingNode.data, ...propNode.data } : existingNode.data; - - return { - ...existingNode, - data - }; - } - - return parseElement(propNode, state.transform); - }); - - const nodesChanged = !isEqual(state.nodes, nextNodes); - const edgesChanged = !isEqual(state.edges, edges); - - if (nodesChanged) { - dispatch(setNodes(nextNodes)); - } - - if (edgesChanged) { - dispatch(setEdges(edges)); - } - }); - - const graphContext = useMemo(() => ({ - state, - dispatch - }), [state]); - - return ( - - {children} - - ); -}; - -export const { Consumer } = GraphContext; - -Provider.displayName = 'GraphProvider'; - -Provider.propTypes = { - elements: PropTypes.arrayOf(PropTypes.object) -}; - -Provider.defaultProps = { - elements: [], -}; diff --git a/src/GraphView/index.js b/src/GraphView/index.js index 4d21bab1..0b2789f2 100644 --- a/src/GraphView/index.js +++ b/src/GraphView/index.js @@ -1,25 +1,33 @@ -import React, { useEffect, useContext, useRef, memo } from 'react'; +import React, { useEffect, useRef, memo } from 'react'; +import { useStoreState, useStoreActions } from 'easy-peasy'; -import { Provider } from '../ConnectionContext'; -import { GraphContext } from '../GraphContext'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import UserSelection from '../UserSelection'; import NodesSelection from '../NodesSelection'; -import { setNodesSelection } from '../state/actions'; -import { updateSize, fitView, zoomIn, zoomOut } from '../state/view-actions'; import useKeyPress from '../hooks/useKeyPress'; import useD3Zoom from '../hooks/useD3Zoom'; import { getDimensions } from '../utils'; +import { fitView, zoomIn, zoomOut } from '../graph-utils'; const GraphView = memo((props) => { const zoomPane = useRef(); const rendererNode = useRef(); - const { state, dispatch } = useContext(GraphContext); + const state = useStoreState(s => ({ + width: s.width, + height: s.height, + nodes: s.nodes, + edges: s.edges, + d3Initialised: s.d3Initialised, + nodesSelectionActive: s.nodesSelectionActive + })); + const updateSize = useStoreActions(actions => actions.updateSize); + const setNodesSelection = useStoreActions(actions => actions.setNodesSelection); + const shiftPressed = useKeyPress('Shift'); const updateDimensions = () => { const size = getDimensions(rendererNode.current); - dispatch(updateSize(size)); + updateSize(size); }; useEffect(() => { @@ -36,37 +44,33 @@ const GraphView = memo((props) => { useEffect(() => { if (state.d3Initialised) { props.onLoad({ - nodes: state.nodes, - edges: state.edges, - fitView: opts => dispatch(fitView(opts)), - zoomIn: () => dispatch(zoomIn()), - zoomOut: () => dispatch(zoomOut()) + fitView, + zoomIn, + zoomOut }); } }, [state.d3Initialised]); return (
- - - - + + {shiftPressed && } {state.nodesSelectionActive && }
dispatch(setNodesSelection({ isActive: false }))} + onClick={() => setNodesSelection({ isActive: false })} ref={zoomPane} />
diff --git a/src/NodeRenderer/HandleTypes/SourceHandle.js b/src/NodeRenderer/HandleTypes/SourceHandle.js index 9bb161d3..10b7d50d 100644 --- a/src/NodeRenderer/HandleTypes/SourceHandle.js +++ b/src/NodeRenderer/HandleTypes/SourceHandle.js @@ -1,12 +1,16 @@ import React, { memo, useContext } from 'react'; +import { useStoreActions, useStoreState } from 'easy-peasy'; import BaseHandle from './BaseHandle'; import NodeIdContext from '../NodeIdContext' -import { ConnectionContext } from '../../ConnectionContext'; -export default memo((props) => { +const SourceHandle = memo((props) => { const nodeId = useContext(NodeIdContext); - const { setPosition, setSourceId, onConnect } = useContext(ConnectionContext); + const { setPosition, setSourceId } = useStoreActions(a => ({ + setPosition: a.setConnectionPosition, + setSourceId: a.setConnectionSourceId + })); + const onConnect = useStoreState(s => s.onConnect); return ( { /> ); }); + +SourceHandle.displayName = 'SourceHandle'; + +export default SourceHandle; diff --git a/src/NodeRenderer/HandleTypes/TargetHandle.js b/src/NodeRenderer/HandleTypes/TargetHandle.js index 436ec23d..b8ba75c0 100644 --- a/src/NodeRenderer/HandleTypes/TargetHandle.js +++ b/src/NodeRenderer/HandleTypes/TargetHandle.js @@ -1,12 +1,16 @@ import React, { memo, useContext } from 'react'; +import { useStoreActions, useStoreState } from 'easy-peasy'; import BaseHandle from './BaseHandle'; -import { ConnectionContext } from '../../ConnectionContext'; import NodeIdContext from '../NodeIdContext' const TargetHandle = memo((props) => { const nodeId = useContext(NodeIdContext); - const { setPosition, setSourceId, onConnect } = useContext(ConnectionContext); + const { setPosition, setSourceId } = useStoreActions(a => ({ + setPosition: a.setConnectionPosition, + setSourceId: a.setConnectionSourceId + })); + const onConnect = useStoreState(s => s.onConnect); return ( ( e.target.className && @@ -29,7 +29,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => { }; }; -const onStart = (evt, { dispatch, setOffset, onClick, id, type, data, position, transform }) => { +const onStart = (evt, { setOffset, onClick, id, type, data, position, transform }) => { if (isInputNode(evt) || isHandle(evt)) { return false; } @@ -42,22 +42,22 @@ const onStart = (evt, { dispatch, setOffset, onClick, id, type, data, position, const offsetY = scaledClient.y - position.y - transform[1]; const node = { id, type, position, data }; - dispatch(setSelectedElements({ id, type })); + store.dispatch.setSelectedElements({ id, type }); setOffset({ x: offsetX, y: offsetY }); onClick(node); }; -const onDrag = (evt, { dispatch, setDragging, id, offset, transform }) => { +const onDrag = (evt, { setDragging, id, offset, transform }) => { const scaledClient = { x: evt.clientX * (1 / transform[2]), y: evt.clientY * (1 / transform[2]) }; setDragging(true); - dispatch(updateNodePos(id, { + store.dispatch.updateNodePos({ id, pos: { x: scaledClient.x - transform[0] - offset.x, y: scaledClient.y - transform[1] - offset.y - })); + }}); }; const onStop = ({ onNodeDragStop, setDragging, isDragging, id, type, position, data }) => { @@ -78,7 +78,7 @@ export default NodeComponent => { const [isDragging, setDragging] = useState(false); const { id, type, data, transform, xPos, yPos, selected, - dispatch, onClick, onNodeDragStop, style + onClick, onNodeDragStop, style } = props; const position = { x: xPos, y: yPos }; @@ -93,13 +93,13 @@ export default NodeComponent => { target: getHandleBounds('.target', nodeElement.current, bounds, transform[2]) }; - dispatch(updateNodeData(id, { ...dimensions, handleBounds })); + store.dispatch.updateNodeData({ id, ...dimensions, handleBounds }); }, []); return ( onStart(evt, { dispatch, onClick, id, type, data, setOffset, transform, position })} - onDrag={evt => onDrag(evt, { dispatch, setDragging, id, offset, transform })} + onStart={evt => onStart(evt, { onClick, id, type, data, setOffset, transform, position })} + onDrag={evt => onDrag(evt, { setDragging, id, offset, transform })} onStop={() => onStop({ onNodeDragStop, isDragging, setDragging, id, type, position, data })} scale={transform[2]} > diff --git a/src/NodeRenderer/index.js b/src/NodeRenderer/index.js index 5393261a..516a3e7a 100644 --- a/src/NodeRenderer/index.js +++ b/src/NodeRenderer/index.js @@ -1,9 +1,9 @@ -import React, { memo, useContext } from 'react'; +import React, { memo } from 'react'; +import { useStoreState } from 'easy-peasy'; -import { GraphContext } from '../GraphContext'; import { isNode } from '../graph-utils'; -function renderNode(d, props, graphContext) { +function renderNode(d, props, state) { const nodeType = d.type || 'default'; if (!props.nodeTypes[nodeType]) { @@ -11,7 +11,7 @@ function renderNode(d, props, graphContext) { } const NodeComponent = props.nodeTypes[nodeType] || props.nodeTypes.default; - const selected = graphContext.state.selectedElements + const selected = state.selectedElements .filter(isNode) .map(e => e.id) .includes(d.id); @@ -26,9 +26,7 @@ function renderNode(d, props, graphContext) { yPos={d.__rg.position.y} onClick={props.onElementClick} onNodeDragStop={props.onNodeDragStop} - dispatch={graphContext.dispatch} - transform={graphContext.state.transform} - getNodeById={graphContext.getNodeById} + transform={state.transform} selected={selected} style={d.style} /> @@ -36,8 +34,13 @@ function renderNode(d, props, graphContext) { } const NodeRenderer = memo((props) => { - const graphContext = useContext(GraphContext); - const { transform, nodes } = graphContext.state; + const state = useStoreState(s => ({ + nodes: s.nodes, + transform: s.transform, + selectedElements: s.selectedElements + })); + + const { transform, nodes } = state; const transformStyle = { transform : `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` }; return ( @@ -45,7 +48,7 @@ const NodeRenderer = memo((props) => { className="react-graph__nodes" style={transformStyle} > - {nodes.map(d => renderNode(d, props, graphContext))} + {nodes.map(d => renderNode(d, props, state))}
); }); diff --git a/src/NodesSelection/index.js b/src/NodesSelection/index.js index 66440b17..b0f5de8a 100644 --- a/src/NodesSelection/index.js +++ b/src/NodesSelection/index.js @@ -1,9 +1,8 @@ -import React, { useContext, useState, memo } from 'react'; +import React, { useState, memo } from 'react'; import ReactDraggable from 'react-draggable'; +import { useStoreState, useStoreActions } from 'easy-peasy'; -import { GraphContext } from '../GraphContext'; import { isNode } from '../graph-utils'; -import { updateNodePos } from '../state/actions'; function getStartPositions(elements) { return elements @@ -21,10 +20,14 @@ function getStartPositions(elements) { } export default memo(() => { - const graphContext = useContext(GraphContext); const [offset, setOffset] = useState({ x: 0, y: 0 }); const [startPositions, setStartPositions] = useState({}); - const { state, dispatch } = graphContext; + const state = useStoreState(s => ({ + transform: s.transform, + selectedNodesBbox: s.selectedNodesBbox, + selectedElements: s.selectedElements + })); + const updateNodePos = useStoreActions(a => a.updateNodePos); const [x, y, k] = state.transform; const position = state.selectedNodesBbox; @@ -48,10 +51,10 @@ export default memo(() => { }; state.selectedElements.filter(isNode).forEach(node => { - dispatch(updateNodePos(node.id, { + updateNodePos({ id: node.id, pos: { x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - x , y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - y - })); + }}); }); }; @@ -59,7 +62,7 @@ export default memo(() => {
{
); -}); \ No newline at end of file +}); diff --git a/src/Plugins/MiniMap/index.js b/src/Plugins/MiniMap/index.js index 716db8d1..a1b824c2 100644 --- a/src/Plugins/MiniMap/index.js +++ b/src/Plugins/MiniMap/index.js @@ -1,22 +1,26 @@ -import React, { useRef, useContext, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; +import { useStoreState } from 'easy-peasy'; import classnames from 'classnames'; import { isFunction } from '../../utils' import { getNodesInside } from '../../graph-utils'; -import { GraphContext } from '../../GraphContext'; const baseStyle = { position: 'absolute', zIndex: 5, bottom: 10, right: 10, - width: 200, + width: 200 }; export default ({ style = {}, className, bgColor = '#f8f8f8', nodeColor = '#ddd' }) => { const canvasNode = useRef(null); - const { state } = useContext(GraphContext); - const mapClasses = classnames('react-graph__minimap', className); + const state = useStoreState(s => ({ + width: s.width, + height: s.height, + nodes: s.nodes, + transform: s.transform, + })); const mapClasses = classnames('react-graph__minimap', className); const nodePositions = state.nodes.map(n => n.__rg.position); const width = style.width || baseStyle.width; const height = (state.height / (state.width || 1)) * width; diff --git a/src/ReactGraph/ElementUpdater.js b/src/ReactGraph/ElementUpdater.js new file mode 100644 index 00000000..f7f4dec7 --- /dev/null +++ b/src/ReactGraph/ElementUpdater.js @@ -0,0 +1,59 @@ +import { useEffect, memo } from 'react'; +import { useStoreState, useStoreActions } from 'easy-peasy'; +import isEqual from 'fast-deep-equal'; + +import { parseElement, isNode, isEdge } from '../graph-utils'; + +const ElementUpdater = (props) => { + const state = useStoreState(s => ({ + nodes: s.nodes, + edges: s.edges, + transform: s.transform + })); + + const setNodes = useStoreActions(a => a.setNodes); + const setEdges = useStoreActions(a => a.setEdges); + const setOnConnect = useStoreActions(a => a.setOnConnect); + + useEffect(() => { + const nodes = props.elements.filter(isNode); + const edges = props.elements.filter(isEdge).map(parseElement); + + const nextNodes = nodes.map(propNode => { + const existingNode = state.nodes.find(n => n.id === propNode.id); + + if (existingNode) { + const data = !isEqual(existingNode.data, propNode.data) ? + { ...existingNode.data, ...propNode.data } : existingNode.data; + + return { + ...existingNode, + data + }; + } + + return parseElement(propNode, state.transform); + }); + + const nodesChanged = !isEqual(state.nodes, nextNodes); + const edgesChanged = !isEqual(state.edges, edges); + + if (nodesChanged) { + setNodes(nextNodes); + } + + if (edgesChanged) { + setEdges(edges); + } + }); + + useEffect(() => { + setOnConnect(props.onConnect); + }, []); + + return null; +}; + +ElementUpdater.displayName = 'ElementUpdater'; + +export default memo(ElementUpdater); diff --git a/src/ReactGraph/index.js b/src/ReactGraph/index.js index 629285bf..bb5ac7b1 100644 --- a/src/ReactGraph/index.js +++ b/src/ReactGraph/index.js @@ -1,4 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect } from 'react'; +import isEqual from 'fast-deep-equal'; +import { StoreProvider } from 'easy-peasy'; if (process.env.NODE_ENV !== 'production') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); @@ -7,7 +9,6 @@ if (process.env.NODE_ENV !== 'production') { import GraphView from '../GraphView'; import GlobalKeyHandler from '../GlobalKeyHandler'; -import { Provider } from '../GraphContext'; import DefaultNode from '../NodeRenderer/NodeTypes/DefaultNode'; import InputNode from '../NodeRenderer/NodeTypes/InputNode'; @@ -18,6 +19,8 @@ import BezierEdge from '../EdgeRenderer/EdgeTypes/BezierEdge'; import StraightEdge from '../EdgeRenderer/EdgeTypes/StraightEdge'; import StepEdge from '../EdgeRenderer/EdgeTypes/StepEdge'; import { createEdgeTypes } from '../EdgeRenderer/utils'; +import store from '../store'; +import ElementUpdater from './ElementUpdater'; import '../style.css'; @@ -31,7 +34,8 @@ const ReactGraph = ({ return (
- + + {children} - +
); }; diff --git a/src/UserSelection/index.js b/src/UserSelection/index.js index e4ad8d95..529ff979 100644 --- a/src/UserSelection/index.js +++ b/src/UserSelection/index.js @@ -1,7 +1,5 @@ -import React, { useEffect, useRef, useState, useContext, memo } from 'react'; - -import { GraphContext } from '../GraphContext'; -import { updateSelection, setSelection, setNodesSelection } from '../state/actions'; +import React, { useEffect, useRef, useState, memo } from 'react'; +import { useStoreActions } from 'easy-peasy'; const initialRect = { startX: 0, @@ -25,7 +23,9 @@ function getMousePosition(evt) { export default memo(() => { const selectionPane = useRef(null); const [rect, setRect] = useState(initialRect); - const { dispatch } = useContext(GraphContext); + const setSelection = useStoreActions(a => a.setSelection); + const updateSelection = useStoreActions(a => a.updateSelection); + const setNodesSelection = useStoreActions(a => a.setNodesSelection); useEffect(() => { function onMouseDown(evt) { @@ -40,7 +40,7 @@ export default memo(() => { draw: true })); - dispatch(setSelection(true)); + setSelection(true); } function onMouseMove(evt) { @@ -60,7 +60,7 @@ export default memo(() => { height: negativeY ? currentRect.startY - mousePos.y : mousePos.y - currentRect.startY, }; - dispatch(updateSelection(nextRect)); + updateSelection(nextRect); return nextRect; }); @@ -68,8 +68,8 @@ export default memo(() => { function onMouseUp() { setRect((currentRect) => { - dispatch(setNodesSelection({ isActive: true, selection: currentRect })); - dispatch(setSelection(false)); + setNodesSelection({ isActive: true, selection: currentRect }); + setSelection(false); return { ...currentRect, diff --git a/src/actions.js b/src/actions.js new file mode 100644 index 00000000..8853702d --- /dev/null +++ b/src/actions.js @@ -0,0 +1,102 @@ +import { action } from 'easy-peasy'; +import isEqual from 'fast-deep-equal'; + +import { getBoundingBox, getNodesInside, getConnectedEdges } from './graph-utils'; + +export default { + setOnConnect: action((state, onConnect) => { + state.onConnect = onConnect; + }), + + setNodes: action((state, nodes) => { + state.nodes = nodes; + }), + + setEdges: action((state, edges) => { + state.edges = edges; + }), + + updateNodeData: action((state, { id, ...data }) => { + state.nodes.forEach((n) => { + if (n.id === id) { + n.__rg = { + ...n.__rg, + ...data + }; + } + }); + }), + + updateNodePos: action((state, { id, pos }) => { + state.nodes.forEach((n) => { + if (n.id === id) { + n.__rg = { + ...n.__rg, + position: pos + }; + } + }); + }), + + setSelection: action((state, isActive) => { + state.selectionActive = isActive; + }), + + setNodesSelection: action((state, { isActive, selection }) => { + if (!isActive) { + state.nodesSelectionActive = false; + state.selectedElements = []; + + return; + } + const selectedNodes = getNodesInside(state.nodes, selection, state.transform); + const selectedNodesBbox = getBoundingBox(selectedNodes); + + state.selection = selection; + state.nodesSelectionActive = true; + state.selectedNodesBbox = selectedNodesBbox; + state.nodesSelectionActive = true; + }), + + setSelectedElements: action((state, elements) => { + const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; + const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements); + const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements; + + state.selectedElements = selectedElements; + }), + + updateSelection: action((state, selection) => { + const selectedNodes = getNodesInside(state.nodes, selection, state.transform); + const selectedEdges = getConnectedEdges(selectedNodes, state.edges); + + const nextSelectedElements = [...selectedNodes, ...selectedEdges]; + const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); + + state.selection = selection; + state.selectedElements = selectedElementsUpdated ? nextSelectedElements: state.selectedElements + }), + + updateTransform: action((state, transform) => { + state.transform = [transform.x, transform.y, transform.k]; + }), + + updateSize: action((state, size) => { + state.width = size.width; + state.height = size.height; + }), + + initD3: action((state, { zoom, selection }) => { + state.d3Zoom = zoom; + state.d3Selection = selection; + state.d3Initialised = true; + }), + + setConnectionPosition: action((state, position) => { + state.connectionPosition = position; + }), + + setConnectionSourceId: action((state, sourceId) => { + state.connectionSourceId = sourceId; + }) +}; \ No newline at end of file diff --git a/src/graph-utils.js b/src/graph-utils.js index c244d8c2..580638ff 100644 --- a/src/graph-utils.js +++ b/src/graph-utils.js @@ -1,3 +1,6 @@ +import { zoomIdentity } from 'd3-zoom'; +import store from './store'; + export const isEdge = element => element.source && element.target; export const isNode = element => !element.source && !element.target; @@ -142,11 +145,37 @@ export const getConnectedEdges = (nodes, edges) => { return edges.filter(e => nodeIds.includes(e.source) || nodeIds.includes(e.target)) }; +export const fitView = ({ padding = 0 } = {}) => { + const state = store.getState(); + const bounds = getBoundingBox(state.nodes); + const maxBoundsSize = Math.max(bounds.width, bounds.height); + const k = Math.min(state.width, state.height) / (maxBoundsSize + (maxBoundsSize * padding)); + const boundsCenterX = bounds.x + (bounds.width / 2); + const boundsCenterY = bounds.y + (bounds.height / 2); + const transform = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)]; + const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k); + + state.d3Selection.call(state.d3Zoom.transform, fittedTransform); +}; + +export const zoomIn = () => { + const state = store.getState(); + state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] + 0.2); +}; + +export const zoomOut = () => { + const state = store.getState(); + state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] - 0.2); +}; + export default { isEdge, separateElements, getBoundingBox, graphPosToZoomedPos, getConnectedEdges, - parseElement + parseElement, + fitView, + zoomIn, + zoomOut }; diff --git a/src/hooks/useD3Zoom.js b/src/hooks/useD3Zoom.js index 0e48446e..ed05100e 100644 --- a/src/hooks/useD3Zoom.js +++ b/src/hooks/useD3Zoom.js @@ -1,18 +1,26 @@ -import { useEffect, useContext } from 'react'; +import { useEffect } from 'react'; import * as d3Zoom from 'd3-zoom'; import { select, event } from 'd3-selection'; - -import { updateTransform, initD3 } from '../state/view-actions'; -import { GraphContext } from '../GraphContext'; +import { useStoreState, useStoreActions } from 'easy-peasy'; const d3ZoomInstance = d3Zoom.zoom().scaleExtent([0.5, 2]); export default function useD3Zoom(zoomPane, onMove, shiftPressed) { - const { state, dispatch } = useContext(GraphContext); + const state = useStoreState(s => ({ + transform: s.transform, + d3Selection: s.d3Selection, + d3Zoom: s.d3Zoom, + edges: s.edged, + d3Initialised: s.d3Initialised, + nodesSelectionActive: s.nodesSelectionActive + })); + + const initD3 = useStoreActions(actions => actions.initD3); + const updateTransform = useStoreActions(actions => actions.updateTransform); useEffect(() => { const selection = select(zoomPane.current).call(d3ZoomInstance); - dispatch(initD3({ zoom: d3ZoomInstance, selection })); + initD3({ zoom: d3ZoomInstance, selection }); }, []); useEffect(() => { @@ -24,7 +32,7 @@ export default function useD3Zoom(zoomPane, onMove, shiftPressed) { return false; } - dispatch(updateTransform(event.transform)); + updateTransform(event.transform); onMove(); }); diff --git a/src/state/actions.js b/src/state/actions.js deleted file mode 100644 index af3017f9..00000000 --- a/src/state/actions.js +++ /dev/null @@ -1,52 +0,0 @@ -import { - SET_NODES, SET_EDGES, UPDATE_NODE_DATA, UPDATE_NODE_POS, - UPDATE_SELECTION, SET_SELECTION, SET_NODES_SELECTION, - SET_SELECTED_ELEMENTS, REMOVE_NODES, -} from './index'; - -export const setNodes = nodes => ({ - type: SET_NODES, - payload: { nodes } -}); - -export const setEdges = edges => ({ - type: SET_EDGES, - payload: { edges } -}); - -export const updateNodeData = (id, data) => ({ - type: UPDATE_NODE_DATA, - payload: { id, data } -}); - -export const updateNodePos = (id, pos) => ({ - type: UPDATE_NODE_POS, - payload: { id, pos } -}); - -export const setSelection = isActive => ({ - type: SET_SELECTION, - payload: { selectionActive: isActive } -}); - -export const setNodesSelection = ({ isActive, selection }) => ({ - type: SET_NODES_SELECTION, - payload: { nodesSelectionActive: isActive, selection } -}); - -export const setSelectedElements = elements => ({ - type: SET_SELECTED_ELEMENTS, - payload: { - selectedElements: Array.isArray(elements) ? elements : [elements] - } -}); - -export const removeNodes = ids => ({ - type: REMOVE_NODES, - payload: { ids: Array.isArray(ids) ? ids : [ids] } -}); - -export const updateSelection = selection => ({ - type: UPDATE_SELECTION, - payload: { selection } -}); diff --git a/src/state/index.js b/src/state/index.js deleted file mode 100644 index c93de77a..00000000 --- a/src/state/index.js +++ /dev/null @@ -1,142 +0,0 @@ -import { zoomIdentity } from 'd3-zoom'; -import isEqual from 'fast-deep-equal'; - -import { getBoundingBox, getNodesInside, getConnectedEdges } from '../graph-utils'; - -export const SET_EDGES = 'SET_EDGES'; -export const SET_NODES = 'SET_NODES'; -export const UPDATE_NODE_DATA = 'UPDATE_NODE_DATA'; -export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; -export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM'; -export const UPDATE_SIZE = 'UPDATE_SIZE'; -export const INIT_D3 = 'INIT_D3'; -export const FIT_VIEW = 'FIT_VIEW'; -export const ZOOM_IN = 'ZOOM_IN'; -export const ZOOM_OUT = 'ZOOM_OUT'; -export const UPDATE_SELECTION = 'UPDATE_SELECTION'; -export const SET_SELECTION = 'SET_SELECTION'; -export const SET_NODES_SELECTION = 'SET_NODES_SELECTION'; -export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS'; -export const REMOVE_NODES = 'REMOVE_NODES'; - -export const initialState = { - width: 0, - height: 0, - transform: [0, 0, 1], - nodes: [], - edges: [], - selectedElements: [], - selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, - - d3Zoom: null, - d3Selection: null, - d3Initialised: false, - - nodesSelectionActive: false, - selectionActive: false, - selection: {} -}; - -export const reducer = (state, action) => { - switch (action.type) { - case UPDATE_NODE_DATA: { - return { - ...state, - nodes: state.nodes.map((n) => { - if (n.id === action.payload.id) { - n.__rg = { - ...n.__rg, - ...action.payload.data - }; - } - return n; - }) - }; - } - case UPDATE_NODE_POS: { - return { - ...state, - nodes: state.nodes.map((n) => { - if (n.id === action.payload.id) { - n.__rg = { - ...n.__rg, - position: action.payload.pos - }; - } - - return n; - }) - }; - } - case FIT_VIEW: { - const bounds = getBoundingBox(state.nodes); - const maxBoundsSize = Math.max(bounds.width, bounds.height); - const k = Math.min(state.width, state.height) / (maxBoundsSize + (maxBoundsSize * action.payload.padding)); - const boundsCenterX = bounds.x + (bounds.width / 2); - const boundsCenterY = bounds.y + (bounds.height / 2); - const transform = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)]; - const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k); - - state.d3Selection.call(state.d3Zoom.transform, fittedTransform); - - return state; - } - case ZOOM_IN: { - const { transform } = state; - state.d3Zoom.scaleTo(state.d3Selection, transform[2] + 0.2); - - return state; - } - case ZOOM_OUT: { - const { transform } = state; - state.d3Zoom.scaleTo(state.d3Selection, transform[2] - 0.2); - - return state; - } - case UPDATE_SELECTION: { - const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform); - const selectedEdges = getConnectedEdges(selectedNodes, state.edges); - - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); - - return { - ...state, - ...action.payload, - selectedElements: selectedElementsUpdated ? nextSelectedElements: state.selectedElements - }; - } - case SET_NODES_SELECTION: { - if (!action.payload.nodesSelectionActive) { - return { ...state, nodesSelectionActive: false, selectedElements: [] }; - } - const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform); - const selectedNodesBbox = getBoundingBox(selectedNodes); - - return { ...state, ...action.payload, selectedNodesBbox }; - } - case SET_SELECTED_ELEMENTS: { - const selectedElementsUpdated = !isEqual(action.payload.selectedElements, state.selectedElements); - const selectedElements = selectedElementsUpdated ? action.payload.selectedElements : state.selectedElements; - - return { ...state, selectedElements, odesSelectionActive: false }; - } - // unused - case REMOVE_NODES: { - const { ids } = action.payload; - const nextEdges = state.edges.filter(e => !ids.includes(e.target) && !ids.includes(e.source)); - const nextNodes = state.nodes.filter(n => !ids.includes(n.id)); - - return { ...state, nodes: nextNodes, edges: nextEdges }; - } - case SET_NODES: - case SET_EDGES: - case UPDATE_TRANSFORM: - case INIT_D3: - case UPDATE_SIZE: - case SET_SELECTION: - return { ...state, ...action.payload }; - default: - return state; - } -}; \ No newline at end of file diff --git a/src/state/view-actions.js b/src/state/view-actions.js deleted file mode 100644 index 74f63c0a..00000000 --- a/src/state/view-actions.js +++ /dev/null @@ -1,32 +0,0 @@ -import { - UPDATE_TRANSFORM, UPDATE_SIZE, INIT_D3, - FIT_VIEW, ZOOM_IN, ZOOM_OUT -} from './index'; - -export const updateTransform = transform => ({ - type: UPDATE_TRANSFORM, - payload: { transform: [transform.x, transform.y, transform.k] } -}); - -export const updateSize = size => ({ - type: UPDATE_SIZE, - payload: size -}); - -export const initD3 = ({ zoom, selection }) => ({ - type: INIT_D3, - payload: { d3Zoom: zoom, d3Selection: selection, d3Initialised: true } -}); - -export const fitView = ({ padding = 0 } = {}) => ({ - type: FIT_VIEW, - payload: { padding } -}); - -export const zoomIn = () => ({ - type: ZOOM_IN -}); - -export const zoomOut = () => ({ - type: ZOOM_OUT -}); diff --git a/src/store.js b/src/store.js new file mode 100644 index 00000000..e9ec92d0 --- /dev/null +++ b/src/store.js @@ -0,0 +1,30 @@ +import { createStore } from 'easy-peasy'; + +import actions from './actions'; + +const store = createStore({ + width: 0, + height: 0, + transform: [0, 0, 1], + nodes: [], + edges: [], + selectedElements: [], + selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, + + d3Zoom: null, + d3Selection: null, + d3Initialised: false, + + nodesSelectionActive: false, + selectionActive: false, + selection: {}, + + connectionSourceId: null, + connectionPosition: { x: 0, y: 0 }, + + onConnect: () => {}, + + ...actions +}); + +export default store;