refactor(app): folder structure, components cleanup
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
|
||||
import ConnectionLine from '../../components/ConnectionLine';
|
||||
import { isEdge } from '../../graph-utils';
|
||||
|
||||
function getHandlePosition(position, node, handle = null) {
|
||||
if (!handle) {
|
||||
switch (position) {
|
||||
case 'top': return {
|
||||
x: node.__rg.width / 2,
|
||||
y: 0
|
||||
};
|
||||
case 'right': return {
|
||||
x: node.__rg.width,
|
||||
y: node.__rg.height / 2
|
||||
};
|
||||
case 'bottom': return {
|
||||
x: node.__rg.width / 2,
|
||||
y: node.__rg.height
|
||||
};
|
||||
case 'left': return {
|
||||
x: 0,
|
||||
y: node.__rg.height / 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case 'top': return {
|
||||
x: handle.x + (handle.width / 2),
|
||||
y: handle.y
|
||||
};
|
||||
case 'right': return {
|
||||
x: handle.x + handle.width,
|
||||
y: handle.y + (handle.height / 2)
|
||||
};
|
||||
case 'bottom': return {
|
||||
x: handle.x + (handle.width / 2),
|
||||
y: handle.y + handle.height
|
||||
};
|
||||
case 'left': return {
|
||||
x: handle.x,
|
||||
y: handle.y + (handle.height / 2)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getHandle(bounds, handleId) {
|
||||
let handle = null;
|
||||
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// there is no handleId when there are no multiple handles/ handles with ids
|
||||
// so we just pick the first one
|
||||
if (bounds.length === 1 || !handleId) {
|
||||
handle = bounds[0];
|
||||
} else if (handleId) {
|
||||
handle = bounds.find(d => d.id === handleId);
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
function getEdgePositions({ sourceNode, sourceHandle, sourcePosition, targetNode, targetHandle, targetPosition }) {
|
||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
||||
const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x;
|
||||
const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y;
|
||||
|
||||
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
|
||||
const targetX = targetNode.__rg.position.x + targetHandlePos.x;
|
||||
const targetY = targetNode.__rg.position.y + targetHandlePos.y;
|
||||
|
||||
return {
|
||||
sourceX, sourceY, targetX, targetY
|
||||
};
|
||||
}
|
||||
|
||||
function renderEdge(e, props, state) {
|
||||
const edgeType = e.type || 'default';
|
||||
|
||||
const hasSourceHandleId = e.source.includes('__');
|
||||
const hasTargetHandleId = e.target.includes('__');
|
||||
|
||||
const sourceId = hasSourceHandleId ? e.source.split('__')[0] : e.source;
|
||||
const targetId = hasTargetHandleId ? e.target.split('__')[0] : e.target;
|
||||
|
||||
const sourceHandleId = hasSourceHandleId ? e.source.split('__')[1] : null;
|
||||
const targetHandleId = hasTargetHandleId ? e.target.split('__')[1] : null;
|
||||
|
||||
const sourceNode = state.nodes.find(n => n.id === sourceId);
|
||||
const targetNode = state.nodes.find(n => n.id === targetId);
|
||||
|
||||
if (!sourceNode) {
|
||||
throw new Error(`couldn't create edge for source id: ${sourceId}`);
|
||||
}
|
||||
|
||||
if (!targetNode) {
|
||||
throw new Error(`couldn't create edge for target id: ${targetId}`);
|
||||
}
|
||||
|
||||
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
|
||||
const sourceHandle = getHandle(sourceNode.__rg.handleBounds.source, sourceHandleId);
|
||||
const targetHandle = getHandle(targetNode.__rg.handleBounds.target, targetHandleId);
|
||||
const sourcePosition = sourceHandle ? sourceHandle.position : 'bottom';
|
||||
const targetPosition = targetHandle ? targetHandle.position : 'top';
|
||||
|
||||
const { sourceX, sourceY, targetX, targetY } = getEdgePositions({
|
||||
sourceNode, sourceHandle, sourcePosition,
|
||||
targetNode, targetHandle, targetPosition
|
||||
});
|
||||
const selected = state.selectedElements
|
||||
.filter(isEdge)
|
||||
.find(elm => elm.source === sourceId && elm.target === targetId);
|
||||
|
||||
return (
|
||||
<EdgeComponent
|
||||
key={e.id}
|
||||
id={e.id}
|
||||
type={e.type}
|
||||
onClick={props.onElementClick}
|
||||
selected={selected}
|
||||
animated={e.animated}
|
||||
style={e.style}
|
||||
source={sourceId}
|
||||
target={targetId}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const EdgeRenderer = memo((props) => {
|
||||
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;
|
||||
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { transform, edges, nodes, connectionSourceId, position } = state;
|
||||
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
className="react-graph__edges"
|
||||
>
|
||||
<g transform={transformStyle}>
|
||||
{edges.map(e => renderEdge(e, props, state))}
|
||||
{connectionSourceId && (
|
||||
<ConnectionLine
|
||||
nodes={nodes}
|
||||
connectionSourceId={connectionSourceId}
|
||||
connectionPositionX={position.x}
|
||||
connectionPositionY={position.y}
|
||||
transform={transform}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
connectionLineType={connectionLineType}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
EdgeRenderer.displayName = 'EdgeRenderer';
|
||||
|
||||
export default EdgeRenderer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import StraightEdge from '../../components/Edges/StraightEdge';
|
||||
import BezierEdge from '../../components/Edges/BezierEdge';
|
||||
import wrapEdge from '../../components/Edges/wrapEdge';
|
||||
|
||||
export function createEdgeTypes(edgeTypes) {
|
||||
const standardTypes = {
|
||||
default: wrapEdge(edgeTypes.default || BezierEdge),
|
||||
straight: wrapEdge(edgeTypes.bezier || StraightEdge)
|
||||
};
|
||||
|
||||
const specialTypes = Object
|
||||
.keys(edgeTypes)
|
||||
.filter(k => !['default', 'bezier'].includes(k))
|
||||
.reduce((res, key) => {
|
||||
res[key] = wrapEdge(edgeTypes[key] ||BezierEdge);
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect, useRef, memo } from 'react';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
|
||||
import NodeRenderer from '../NodeRenderer';
|
||||
import EdgeRenderer from '../EdgeRenderer';
|
||||
import UserSelection from '../../components/UserSelection';
|
||||
import NodesSelection from '../../components/NodesSelection';
|
||||
import useKeyPress from '../../hooks/useKeyPress';
|
||||
import useD3Zoom from '../../hooks/useD3Zoom';
|
||||
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
|
||||
import useElementUpdater from '../../hooks/useElementUpdater'
|
||||
import { getDimensions } from '../../utils';
|
||||
import { fitView, zoomIn, zoomOut } from '../../graph-utils';
|
||||
|
||||
const GraphView = memo(({
|
||||
nodeTypes, edgeTypes, onMove, onLoad,
|
||||
onElementClick, onNodeDragStop, connectionLineType, connectionLineStyle,
|
||||
selectionKeyCode, onElementsRemove, deleteKeyCode, elements,
|
||||
onConnect
|
||||
}) => {
|
||||
const zoomPane = useRef();
|
||||
const rendererNode = useRef();
|
||||
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 setOnConnect = useStoreActions(a => a.setOnConnect);
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||
|
||||
const onZoomPaneClick = () => setNodesSelection({ isActive: false });
|
||||
|
||||
const updateDimensions = () => {
|
||||
const size = getDimensions(rendererNode.current);
|
||||
updateSize(size);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateDimensions();
|
||||
setOnConnect(onConnect);
|
||||
window.onresize = updateDimensions;
|
||||
|
||||
return () => {
|
||||
window.onresize = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useD3Zoom(zoomPane, onMove, selectionKeyPressed);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.d3Initialised) {
|
||||
onLoad({
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
});
|
||||
}
|
||||
}, [state.d3Initialised]);
|
||||
|
||||
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
||||
useElementUpdater({ elements });
|
||||
|
||||
return (
|
||||
<div className="react-graph__renderer" ref={rendererNode}>
|
||||
<NodeRenderer
|
||||
nodeTypes={nodeTypes}
|
||||
onElementClick={onElementClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
/>
|
||||
<EdgeRenderer
|
||||
width={state.width}
|
||||
height={state.height}
|
||||
edgeTypes={edgeTypes}
|
||||
onElementClick={onElementClick}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
/>
|
||||
{selectionKeyPressed && <UserSelection />}
|
||||
{state.nodesSelectionActive && <NodesSelection />}
|
||||
<div
|
||||
className="react-graph__zoompane"
|
||||
onClick={onZoomPaneClick}
|
||||
ref={zoomPane}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
GraphView.displayName = 'GraphView';
|
||||
|
||||
export default GraphView;
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
|
||||
import { isNode } from '../../graph-utils';
|
||||
|
||||
function renderNode(d, props, state) {
|
||||
const nodeType = d.type || 'default';
|
||||
|
||||
if (!props.nodeTypes[nodeType]) {
|
||||
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
|
||||
}
|
||||
|
||||
const NodeComponent = props.nodeTypes[nodeType] || props.nodeTypes.default;
|
||||
const selected = state.selectedElements
|
||||
.filter(isNode)
|
||||
.map(e => e.id)
|
||||
.includes(d.id);
|
||||
|
||||
return (
|
||||
<NodeComponent
|
||||
key={d.id}
|
||||
id={d.id}
|
||||
type={d.type}
|
||||
data={d.data}
|
||||
xPos={d.__rg.position.x}
|
||||
yPos={d.__rg.position.y}
|
||||
onClick={props.onElementClick}
|
||||
onNodeDragStop={props.onNodeDragStop}
|
||||
transform={state.transform}
|
||||
selected={selected}
|
||||
style={d.style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const NodeRenderer = memo((props) => {
|
||||
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 (
|
||||
<div
|
||||
className="react-graph__nodes"
|
||||
style={transformStyle}
|
||||
>
|
||||
{nodes.map(d => renderNode(d, props, state))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
NodeRenderer.displayName = 'NodeRenderer';
|
||||
NodeRenderer.whyDidYouRender = false;
|
||||
|
||||
export default NodeRenderer;
|
||||
@@ -0,0 +1,26 @@
|
||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
||||
import InputNode from '../../components/Nodes/InputNode';
|
||||
import OutputNode from '../../components/Nodes/OutputNode';
|
||||
import wrapNode from '../../components/Nodes/wrapNode';
|
||||
|
||||
export function createNodeTypes(nodeTypes) {
|
||||
const standardTypes = {
|
||||
input: wrapNode(nodeTypes.input || InputNode),
|
||||
default: wrapNode(nodeTypes.default || DefaultNode),
|
||||
output: wrapNode(nodeTypes.output || OutputNode)
|
||||
};
|
||||
|
||||
const specialTypes = Object
|
||||
.keys(nodeTypes)
|
||||
.filter(k => !['input', 'default', 'output'].includes(k))
|
||||
.reduce((res, key) => {
|
||||
res[key] = wrapNode(nodeTypes[key] || DefaultNode);
|
||||
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...standardTypes,
|
||||
...specialTypes
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user