Merge pull request #2773 from wbkd/feat/on-error

Feat/on error
This commit is contained in:
Moritz Klack
2023-01-25 21:08:36 +01:00
committed by GitHub
30 changed files with 260 additions and 223 deletions
+15 -13
View File
@@ -51,16 +51,16 @@ describe('Basic Flow Rendering', () => {
.type('{shift}', { release: false }) .type('{shift}', { release: false })
.wait(50) .wait(50)
.get('.react-flow__pane') .get('.react-flow__pane')
.trigger('mousedown', 1000, 50, { button: 0, force: true }) .trigger('mousedown', 1, 10, { button: 0, force: true })
.trigger('mousemove', 1, 400, { button: 0 }) .trigger('mousemove', 1000, 450, { button: 0 })
.wait(50) .wait(50)
.trigger('mouseup', 1, 200, { force: true }); .trigger('mouseup', 1000, 450, { button: 0, force: true });
cy.wait(100); cy.wait(200);
cy.get('.react-flow__node').eq(1).should('have.class', 'selected'); cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.not.class', 'selected'); cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect'); cy.get('.react-flow__nodesselection-rect');
@@ -74,11 +74,13 @@ describe('Basic Flow Rendering', () => {
.trigger('mousedown', 'topRight', { button: 0, force: true }) .trigger('mousedown', 'topRight', { button: 0, force: true })
.trigger('mousemove', 'bottomLeft', { button: 0 }) .trigger('mousemove', 'bottomLeft', { button: 0 })
.wait(50) .wait(50)
.trigger('mouseup', 'bottomLeft', { force: true }) .trigger('mouseup', 'bottomLeft', { button: 0, force: true })
.wait(50) .wait(400)
.get('.react-flow__node') .get('.react-flow__node')
.should('have.class', 'selected') .should('have.class', 'selected');
.get('.react-flow__nodesselection-rect');
cy.wait(200);
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true }); cy.get('body').type('{shift}', { release: true });
}); });
@@ -113,14 +115,14 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 3') .contains('Node 3')
.find('.react-flow__handle.source') .find('.react-flow__handle.source')
.trigger('mousedown', { button: 0 }); .trigger('mousedown', { force: true, button: 0 });
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 4') .contains('Node 4')
.find('.react-flow__handle.target') .find('.react-flow__handle.target')
.trigger('mousemove', { force: true }) .trigger('mousemove', { force: true, button: 0 })
.wait(50) .wait(200)
.trigger('mouseup', { force: true }); .trigger('mouseup', { force: true, button: 0 });
cy.get('.react-flow__edge').should('have.length', 3); cy.get('.react-flow__edge').should('have.length', 3);
}); });
@@ -48,8 +48,6 @@ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const defaultEdgeOptions = {}; const defaultEdgeOptions = {};
const BasicFlow = () => { const BasicFlow = () => {
@@ -96,7 +94,6 @@ const BasicFlow = () => {
selectNodesOnDrag={false} selectNodesOnDrag={false}
elevateEdgesOnSelect elevateEdgesOnSelect
elevateNodesOnSelect={false} elevateNodesOnSelect={false}
// nodeOrigin={nodeOrigin}
> >
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -1,9 +1,8 @@
import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'reactflow'; import { ConnectionLineComponentProps } from 'reactflow';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => { function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
return ( return (
<g> <>
<path <path
fill="none" fill="none"
stroke="#222" stroke="#222"
@@ -12,8 +11,8 @@ const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, t
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`} d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/> />
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} /> <circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g> </>
); );
}; }
export default ConnectionLine; export default ConnectionLine;
@@ -1,5 +1,5 @@
import { MouseEvent, useCallback } from 'react'; import { MouseEvent, useCallback } from 'react';
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState, NodeOrigin } from 'reactflow'; import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
@@ -100,7 +100,6 @@ const BasicFlow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
disableKeyboardA11y
onNodeDrag={onNodeDrag} onNodeDrag={onNodeDrag}
> >
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> <div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
@@ -1,4 +1,4 @@
import React, { MouseEvent, useCallback } from 'react'; import { MouseEvent, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
useReactFlow, useReactFlow,
NodeTypes, NodeTypes,
@@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els)); const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useReactFlow(); const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
@@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import { internalsSymbol } from '../../utils'; import { internalsSymbol } from '../../utils';
import type { ConnectionLineComponent, HandleType, ReactFlowStore } from '../../types'; import type { ConnectionLineComponent, HandleType, ReactFlowState, ReactFlowStore } from '../../types';
import { Position, ConnectionLineType, ConnectionMode } from '../../types'; import { Position, ConnectionLineType, ConnectionMode } from '../../types';
type ConnectionLineProps = { type ConnectionLineProps = {
connectionNodeId: string; nodeId: string;
connectionHandleType: HandleType; handleType: HandleType;
connectionLineType: ConnectionLineType; type: ConnectionLineType;
isConnectable: boolean; style?: CSSProperties;
connectionLineStyle?: CSSProperties; CustomComponent?: ConnectionLineComponent;
CustomConnectionLineComponent?: ConnectionLineComponent;
}; };
const oppositePosition = { const oppositePosition = {
@@ -26,69 +25,62 @@ const oppositePosition = {
}; };
const ConnectionLine = ({ const ConnectionLine = ({
connectionNodeId, nodeId,
connectionHandleType, handleType,
connectionLineStyle, style,
connectionLineType = ConnectionLineType.Bezier, type = ConnectionLineType.Bezier,
isConnectable, CustomComponent,
CustomConnectionLineComponent,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode } = useStore( const { fromNode, handleId, toX, toY, connectionMode } = useStore(
useCallback( useCallback(
(s: ReactFlowStore) => ({ (s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(connectionNodeId), fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId, handleId: s.connectionHandleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
}), }),
[connectionNodeId] [nodeId]
), ),
shallow shallow
); );
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
let handleBounds = fromHandleBounds?.[connectionHandleType]; let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) { if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
? handleBounds
: fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source'];
} }
if (!fromNode || !isConnectable || !handleBounds) { if (!fromNode || !handleBounds) {
return null; return null;
} }
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0;
const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX; const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX;
const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY; const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position; const fromPosition = fromHandle?.position;
const toPosition = fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition) { if (!fromPosition || !toPosition) {
return null; return null;
} }
const toPosition: Position = oppositePosition[fromPosition]; if (CustomComponent) {
if (CustomConnectionLineComponent) {
return ( return (
<g className="react-flow__connection"> <CustomComponent
<CustomConnectionLineComponent connectionLineType={type}
connectionLineType={connectionLineType} connectionLineStyle={style}
connectionLineStyle={connectionLineStyle} fromNode={fromNode}
fromNode={fromNode} fromHandle={fromHandle}
fromHandle={fromHandle} fromX={fromX}
fromX={fromX} fromY={fromY}
fromY={fromY} toX={toX}
toX={toX} toY={toY}
toY={toY} fromPosition={fromPosition}
fromPosition={fromPosition} toPosition={toPosition}
toPosition={toPosition} />
/>
</g>
); );
} }
@@ -103,29 +95,62 @@ const ConnectionLine = ({
targetPosition: toPosition, targetPosition: toPosition,
}; };
if (connectionLineType === ConnectionLineType.Bezier) { if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position // we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams); [dAttr] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) { } else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({ [dAttr] = getSmoothStepPath({
...pathParams, ...pathParams,
borderRadius: 0, borderRadius: 0,
}); });
} else if (connectionLineType === ConnectionLineType.SmoothStep) { } else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams); [dAttr] = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) { } else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams); [dAttr] = getSimpleBezierPath(pathParams);
} else { } else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`; dAttr = `M${fromX},${fromY} ${toX},${toY}`;
} }
return ( return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
<g className="react-flow__connection">
<path d={dAttr} fill="none" className="react-flow__connection-path" style={connectionLineStyle} />
</g>
);
}; };
ConnectionLine.displayName = 'ConnectionLine'; ConnectionLine.displayName = 'ConnectionLine';
export default ConnectionLine; type ConnectionLineWrapperProps = {
type: ConnectionLineType;
component?: ConnectionLineComponent;
containerStyle?: CSSProperties;
style?: CSSProperties;
};
const selector = (s: ReactFlowState) => ({
nodeId: s.connectionNodeId,
handleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
width: s.width,
height: s.height,
});
function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height } = useStore(selector, shallow);
const isValid = !!(nodeId && handleType && width && nodesConnectable);
if (!isValid) {
return null;
}
return (
<svg
style={containerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<g className="react-flow__connection">
<ConnectionLine nodeId={nodeId} handleType={handleType} style={style} type={type} CustomComponent={component} />
</g>
</svg>
);
}
export default ConnectionLineWrapper;
@@ -9,7 +9,7 @@ import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph'; import { getMarkerId } from '../../utils/graph';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
import { elementSelectionKeys } from '../../utils'; import { elementSelectionKeys } from '../../utils';
import type { EdgeProps, WrapEdgeProps, Connection } from '../../types'; import type { EdgeProps, WrapEdgeProps, Connection, HandleType } from '../../types';
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
@@ -115,7 +115,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
getState: store.getState, getState: store.getState,
setState: store.setState, setState: store.setState,
isValidConnection, isValidConnection,
elementEdgeUpdaterType: handleType, edgeUpdaterType: handleType,
onEdgeUpdateEnd: _onEdgeUpdateEnd, onEdgeUpdateEnd: _onEdgeUpdateEnd,
}); });
}; };
@@ -5,11 +5,12 @@ import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler'; import { handlePointerDown } from './handler';
import { devWarn, getHostForElement, isMouseEvent } from '../../utils'; import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph'; import { addEdge } from '../../utils/graph';
import { Position } from '../../types'; import { Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types'; import type { HandleProps, Connection, ReactFlowState } from '../../types';
import { isValidHandle } from './utils'; import { isValidHandle } from './utils';
import { errorMessages } from '../../contants';
const alwaysValid = () => true; const alwaysValid = () => true;
@@ -42,7 +43,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const nodeId = useNodeId(); const nodeId = useNodeId();
if (!nodeId) { if (!nodeId) {
devWarn('Handle: No node id found. Make sure to only use a Handle inside a custom Node.'); store.getState().onError?.('010', errorMessages['010']());
return null; return null;
} }
+1 -1
View File
@@ -98,7 +98,7 @@ export function isValidHandle(
const isValid = const isValid =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? (isTarget && handleIsSource) || (!isTarget && handleIsTarget) ? (isTarget && handleIsSource) || (!isTarget && handleIsTarget)
: handleNodeId !== handle.nodeId || handleId !== handle.id; : handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) { if (isValid) {
result.isValid = isValidConnection(connection); result.isValid = isValidConnection(connection);
@@ -47,6 +47,7 @@ type StoreUpdaterProps = Pick<
| 'elevateNodesOnSelect' | 'elevateNodesOnSelect'
| 'autoPanOnConnect' | 'autoPanOnConnect'
| 'autoPanOnNodeDrag' | 'autoPanOnNodeDrag'
| 'onError'
> & { rfId: string }; > & { rfId: string };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -123,6 +124,7 @@ const StoreUpdater = ({
rfId, rfId,
autoPanOnConnect, autoPanOnConnect,
autoPanOnNodeDrag, autoPanOnNodeDrag,
onError,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
@@ -178,6 +180,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('rfId', rfId, store.setState); useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState); useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState); useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges); useStoreUpdater<Edge[]>(edges, setEdges);
@@ -1,8 +1,9 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { devWarn } from '../../utils';
import { MarkerType } from '../../types'; import { MarkerType } from '../../types';
import type { EdgeMarker } from '../../types'; import type { EdgeMarker } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
import { errorMessages } from '../../contants';
type SymbolProps = Omit<EdgeMarker, 'type'>; type SymbolProps = Omit<EdgeMarker, 'type'>;
@@ -38,11 +39,12 @@ export const MarkerSymbols = {
}; };
export function useMarkerSymbol(type: MarkerType) { export function useMarkerSymbol(type: MarkerType) {
const store = useStoreApi();
const symbol = useMemo(() => { const symbol = useMemo(() => {
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type); const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
if (!symbolExists) { if (!symbolExists) {
devWarn(`Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`); store.getState().onError?.('009', errorMessages['009'](type));
return null; return null;
} }
@@ -1,27 +1,20 @@
import { memo } from 'react'; import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdges from '../../hooks/useVisibleEdges';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils'; import { getEdgePositions, getHandle, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import { devWarn } from '../../utils';
import { ConnectionMode, Position } from '../../types'; import { ConnectionMode, Position } from '../../types';
import type { Edge, ReactFlowState } from '../../types'; import type { Edge, ReactFlowState } from '../../types';
import { errorMessages } from '../../contants';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
| 'edgeTypes' | 'edgeTypes'
| 'connectionLineType'
| 'connectionLineType'
| 'connectionLineStyle'
| 'connectionLineComponent'
| 'connectionLineContainerStyle'
| 'connectionLineContainerStyle'
| 'onEdgeClick' | 'onEdgeClick'
| 'onEdgeDoubleClick' | 'onEdgeDoubleClick'
| 'defaultMarkerColor' | 'defaultMarkerColor'
@@ -40,11 +33,10 @@ type EdgeRendererProps = Pick<
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
> & { > & {
elevateEdgesOnSelect: boolean; elevateEdgesOnSelect: boolean;
children: ReactNode;
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable, edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
@@ -52,35 +44,38 @@ const selector = (s: ReactFlowState) => ({
height: s.height, height: s.height,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals, nodeInternals: s.nodeInternals,
onError: s.onError,
}); });
const EdgeRenderer = (props: EdgeRendererProps) => { const EdgeRenderer = ({
const { defaultMarkerColor,
connectionNodeId, onlyRenderVisibleElements,
connectionHandleType, elevateEdgesOnSelect,
nodesConnectable, rfId,
edgesFocusable, edgeTypes,
elementsSelectable, noPanClassName,
width, onEdgeUpdate,
height, onEdgeContextMenu,
connectionMode, onEdgeMouseEnter,
nodeInternals, onEdgeMouseMove,
} = useStore(selector, shallow); onEdgeMouseLeave,
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect); onEdgeClick,
edgeUpdaterRadius,
onEdgeDoubleClick,
onEdgeUpdateStart,
onEdgeUpdateEnd,
children,
}: EdgeRendererProps) => {
const { edgesFocusable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = useStore(
selector,
shallow
);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
if (!width) { if (!width) {
return null; return null;
} }
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return ( return (
<> <>
{edgeTree.map(({ level, edges, isMaxLevel }) => ( {edgeTree.map(({ level, edges, isMaxLevel }) => (
@@ -91,11 +86,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
height={height} height={height}
className="react-flow__edges react-flow__container" className="react-flow__edges react-flow__container"
> >
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={props.rfId} />} {isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g> <g>
{edges.map((edge: Edge) => { {edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!); const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!); const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) { if (!sourceIsValid || !targetIsValid) {
return null; return null;
@@ -103,32 +98,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
let edgeType = edge.type || 'default'; let edgeType = edge.type || 'default';
if (!props.edgeTypes[edgeType]) { if (!edgeTypes[edgeType]) {
devWarn( onError?.('011', errorMessages['011'](edgeType));
`Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
edgeType = 'default'; edgeType = 'default';
} }
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
// when connection type is loose we can define all handles as sources // when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles = const targetNodeHandles =
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target ? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null); const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null); const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) { if (!sourceHandle || !targetHandle) {
devWarn( onError?.('008', errorMessages['008'](sourceHandle, edge));
`Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: ${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
return null; return null;
} }
@@ -146,7 +134,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
<EdgeComponent <EdgeComponent
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
className={cc([edge.className, props.noPanClassName])} className={cc([edge.className, noPanClassName])}
type={edgeType} type={edgeType}
data={edge.data} data={edge.data}
selected={!!edge.selected} selected={!!edge.selected}
@@ -172,17 +160,17 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
elementsSelectable={elementsSelectable} elementsSelectable={elementsSelectable}
onEdgeUpdate={props.onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onContextMenu={props.onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter} onMouseEnter={onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove} onMouseMove={onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave} onMouseLeave={onEdgeMouseLeave}
onClick={props.onEdgeClick} onClick={onEdgeClick}
edgeUpdaterRadius={props.edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
rfId={props.rfId} rfId={rfId}
ariaLabel={edge.ariaLabel} ariaLabel={edge.ariaLabel}
isFocusable={isFocusable} isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
@@ -193,23 +181,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
</g> </g>
</svg> </svg>
))} ))}
{renderConnectionLine && ( {children}
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleType={connectionHandleType!}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
</> </>
); );
}; };
@@ -72,21 +72,18 @@ export function getHandlePosition(position: Position, nodeRect: Rect, handle: Ha
} }
} }
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null { export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
if (!bounds) { if (!bounds) {
return null; return null;
} }
// there is no handleId when there are no multiple handles/ handles with ids if (handleId) {
// so we just pick the first one return bounds.find((d) => d.id === handleId)!;
let handle: HandleElement | null = null; } else if (bounds.length === 1) {
if (bounds.length === 1 || !handleId) { return bounds[0];
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId)!;
} }
return typeof handle === 'undefined' ? null : handle; return null;
} }
interface EdgePositions { interface EdgePositions {
@@ -167,16 +164,15 @@ export function isEdgeVisible({
return overlappingArea > 0; return overlappingArea > 0;
} }
export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean] { export function getNodeData(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
const handleBounds = node?.[internalsSymbol]?.handleBounds || null; const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isInvalid = const isValid =
!node || handleBounds &&
!handleBounds || node?.width &&
!node.width || node?.height &&
!node.height || typeof node?.positionAbsolute?.x !== 'undefined' &&
typeof node.positionAbsolute?.x === 'undefined' || typeof node?.positionAbsolute?.y !== 'undefined';
typeof node.positionAbsolute?.y === 'undefined';
return [ return [
{ {
@@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean
height: node?.height || 0, height: node?.height || 0,
}, },
handleBounds, handleBounds,
!isInvalid, !!isValid,
]; ];
} }
@@ -6,6 +6,7 @@ import EdgeRenderer from '../EdgeRenderer';
import ViewportWrapper from '../Viewport'; import ViewportWrapper from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler'; import useOnInitHandler from '../../hooks/useOnInitHandler';
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types'; import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
import ConnectionLine from '../../components/ConnectionLine';
export type GraphViewProps = Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> & export type GraphViewProps = Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> &
Required< Required<
@@ -149,10 +150,6 @@ const GraphView = ({
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick} onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements} onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu} onEdgeContextMenu={onEdgeContextMenu}
@@ -167,7 +164,14 @@ const GraphView = ({
elevateEdgesOnSelect={!!elevateEdgesOnSelect} elevateEdgesOnSelect={!!elevateEdgesOnSelect}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId} rfId={rfId}
/> >
<ConnectionLine
style={connectionLineStyle}
type={connectionLineType}
component={connectionLineComponent}
containerStyle={connectionLineContainerStyle}
/>
</EdgeRenderer>
<div className="react-flow__edgelabel-renderer" /> <div className="react-flow__edgelabel-renderer" />
<NodeRenderer <NodeRenderer
@@ -4,12 +4,13 @@ import { shallow } from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { clampPosition, devWarn, internalsSymbol } from '../../utils'; import { clampPosition, internalsSymbol } from '../../utils';
import { containerStyle } from '../../styles'; import { containerStyle } from '../../styles';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import { getPositionWithOrigin } from './utils'; import { getPositionWithOrigin } from './utils';
import { Position } from '../../types'; import { Position } from '../../types';
import type { ReactFlowState, WrapNodeProps } from '../../types'; import type { ReactFlowState, WrapNodeProps } from '../../types';
import { errorMessages } from '../../contants';
type NodeRendererProps = Pick< type NodeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -36,13 +37,12 @@ const selector = (s: ReactFlowState) => ({
nodesFocusable: s.nodesFocusable, nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions, updateNodeDimensions: s.updateNodeDimensions,
onError: s.onError,
}); });
const NodeRenderer = (props: NodeRendererProps) => { const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore( const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
selector, useStore(selector, shallow);
shallow
);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>(); const resizeObserverRef = useRef<ResizeObserver>();
@@ -78,9 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
let nodeType = node.type || 'default'; let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) { if (!props.nodeTypes[nodeType]) {
devWarn( onError?.('003', errorMessages['003'](nodeType));
`Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
nodeType = 'default'; nodeType = 'default';
} }
@@ -5,7 +5,6 @@ import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode'; import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode'; import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode'; import wrapNode from '../../components/Nodes/wrapNode';
import { devWarn } from '../../utils';
import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types'; import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped; export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
@@ -51,7 +50,6 @@ export const getPositionWithOrigin = ({
} }
if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) { if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
devWarn('nodeOrigin must be between 0 and 1');
return { x, y }; return { x, y };
} }
@@ -163,6 +163,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
autoPanOnConnect = true, autoPanOnConnect = true,
autoPanOnNodeDrag = true, autoPanOnNodeDrag = true,
connectionRadius = 20, connectionRadius = 20,
onError,
style, style,
id, id,
...rest ...rest
@@ -292,6 +293,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
rfId={rfId} rfId={rfId}
autoPanOnConnect={autoPanOnConnect} autoPanOnConnect={autoPanOnConnect}
autoPanOnNodeDrag={autoPanOnNodeDrag} autoPanOnNodeDrag={autoPanOnNodeDrag}
onError={onError}
/> />
<SelectionListener onSelectionChange={onSelectionChange} /> <SelectionListener onSelectionChange={onSelectionChange} />
{children} {children}
@@ -5,6 +5,7 @@ import { devWarn } from '../../utils';
import { CreateEdgeTypes } from '../EdgeRenderer/utils'; import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils'; import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types'; import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { errorMessages } from '../../contants';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
@@ -16,9 +17,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes); const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) { if (shallow(typesKeysRef.current, typeKeys)) {
devWarn( devWarn('002', errorMessages['002']());
"It looks like you have created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them. Help: https://reactflow.dev/error#200"
);
} }
typesKeysRef.current = typeKeys; typesKeysRef.current = typeKeys;
+20
View File
@@ -0,0 +1,20 @@
import { Edge, HandleElement } from './types';
export const errorMessages = {
'001': () =>
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001',
'002': () =>
"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",
'003': (nodeType: string) => `Node type "${nodeType}" not found. Using fallback type "default".`,
'004': () => 'The React Flow parent container needs a width and a height to render the graph.',
'005': () => 'Only child nodes can use a parent extent.',
'006': () => "Can't create edge. An edge needs a source and a target.",
'007': (id: string) => `The old edge with id=${id} does not exist.`,
'009': (type: string) => `Marker type "${type}" doesn't exist.`,
'008': (sourceHandle: HandleElement | null, edge: Edge) =>
`Couldn't create edge for ${!sourceHandle ? 'source' : 'target'} handle id: "${
!sourceHandle ? edge.sourceHandle : edge.targetHandle
}", edge id: ${edge.id}.`,
'010': () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
'011': (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
};
+2 -1
View File
@@ -61,6 +61,7 @@ function useDrag({
snapGrid, snapGrid,
snapToGrid, snapToGrid,
nodeOrigin, nodeOrigin,
onError,
} = store.getState(); } = store.getState();
lastPos.current = { x, y }; lastPos.current = { x, y };
@@ -75,7 +76,7 @@ function useDrag({
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
} }
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin); const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin, onError);
// we want to make sure that we only fire a change event when there is a changes // we want to make sure that we only fire a change event when there is a changes
hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y; hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y;
+6 -4
View File
@@ -1,8 +1,9 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { clampPosition, devWarn, isNumeric } from '../../utils'; import { clampPosition, isNumeric } from '../../utils';
import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, OnError, XYPosition } from '../../types';
import { getNodePositionWithOrigin } from '../../utils/graph'; import { getNodePositionWithOrigin } from '../../utils/graph';
import { errorMessages } from '../../contants';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) { if (!node.parentNode) {
@@ -62,7 +63,8 @@ export function calcNextPosition(
nextPosition: XYPosition, nextPosition: XYPosition,
nodeInternals: NodeInternals, nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent, nodeExtent?: CoordinateExtent,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0],
onError?: OnError
): { position: XYPosition; positionAbsolute: XYPosition } { ): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent; let currentExtent = node.extent || nodeExtent;
@@ -81,7 +83,7 @@ export function calcNextPosition(
] ]
: currentExtent; : currentExtent;
} else { } else {
devWarn('Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500'); onError?.('005', errorMessages['005']());
currentExtent = nodeExtent; currentExtent = nodeExtent;
} }
+3 -4
View File
@@ -2,7 +2,8 @@ import { useEffect } from 'react';
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import { useStoreApi } from '../hooks/useStore'; import { useStoreApi } from '../hooks/useStore';
import { devWarn, getDimensions } from '../utils'; import { getDimensions } from '../utils';
import { errorMessages } from '../contants';
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void { function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi(); const store = useStoreApi();
@@ -18,9 +19,7 @@ function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>)
const size = getDimensions(rendererNode.current); const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) { if (size.height === 0 || size.width === 0) {
devWarn( store.getState().onError?.('004', errorMessages['004']());
'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
);
} }
store.setState({ width: size.width || 500, height: size.height || 500 }); store.setState({ width: size.width || 500, height: size.height || 500 });
+4 -4
View File
@@ -3,10 +3,10 @@ import { useStore as useZustandStore } from 'zustand';
import type { StoreApi } from 'zustand'; import type { StoreApi } from 'zustand';
import StoreContext from '../contexts/RFStoreContext'; import StoreContext from '../contexts/RFStoreContext';
import { errorMessages } from '../contants';
import type { ReactFlowState } from '../types'; import type { ReactFlowState } from '../types';
const errorMessage = const zustandErrorMessage = errorMessages['001']();
'[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#100';
type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never; type ExtractState = StoreApi<ReactFlowState> extends { getState: () => infer T } ? T : never;
@@ -17,7 +17,7 @@ function useStore<StateSlice = ExtractState>(
const store = useContext(StoreContext); const store = useContext(StoreContext);
if (store === null) { if (store === null) {
throw new Error(errorMessage); throw new Error(zustandErrorMessage);
} }
return useZustandStore(store, selector, equalityFn); return useZustandStore(store, selector, equalityFn);
@@ -27,7 +27,7 @@ const useStoreApi = () => {
const store = useContext(StoreContext); const store = useContext(StoreContext);
if (store === null) { if (store === null) {
throw new Error(errorMessage); throw new Error(zustandErrorMessage);
} }
return useMemo( return useMemo(
@@ -7,7 +7,8 @@ function useUpdateNodePositions() {
const store = useStoreApi(); const store = useStoreApi();
const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => {
const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid } = store.getState(); const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid, onError } =
store.getState();
const selectedNodes = getNodes().filter((n) => n.selected); const selectedNodes = getNodes().filter((n) => n.selected);
// by default a node moves 5px on each key press, or 20px if shift is pressed // by default a node moves 5px on each key press, or 20px if shift is pressed
// if snap grid is enabled, we use that for the velocity. // if snap grid is enabled, we use that for the velocity.
@@ -27,10 +28,17 @@ function useUpdateNodePositions() {
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
} }
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent); const { positionAbsolute, position } = calcNextPosition(
n,
nextPosition,
nodeInternals,
nodeExtent,
undefined,
onError
);
n.position = updatedPos.position; n.position = position;
n.positionAbsolute = updatedPos.positionAbsolute; n.positionAbsolute = positionAbsolute;
} }
return n; return n;
+2
View File
@@ -1,3 +1,4 @@
import { devWarn } from '../utils';
import { ConnectionMode } from '../types'; import { ConnectionMode } from '../types';
import type { CoordinateExtent, ReactFlowStore } from '../types'; import type { CoordinateExtent, ReactFlowStore } from '../types';
@@ -59,6 +60,7 @@ const initialState: ReactFlowStore = {
autoPanOnConnect: true, autoPanOnConnect: true,
autoPanOnNodeDrag: true, autoPanOnNodeDrag: true,
connectionRadius: 20, connectionRadius: 20,
onError: devWarn,
}; };
export default initialState; export default initialState;
+1 -1
View File
@@ -98,7 +98,7 @@
.react-flow__connection { .react-flow__connection {
pointer-events: none; pointer-events: none;
&.animated { .animated {
stroke-dasharray: 5; stroke-dasharray: 5;
animation: dashdraw 0.5s linear infinite; animation: dashdraw 0.5s linear infinite;
} }
@@ -36,6 +36,7 @@ import type {
EdgeMouseHandler, EdgeMouseHandler,
HandleType, HandleType,
SelectionMode, SelectionMode,
OnError,
} from '.'; } from '.';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & { export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
@@ -142,6 +143,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
autoPanOnNodeDrag?: boolean; autoPanOnNodeDrag?: boolean;
autoPanOnConnect?: boolean; autoPanOnConnect?: boolean;
connectionRadius?: number; connectionRadius?: number;
onError?: OnError;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+3
View File
@@ -206,6 +206,7 @@ export type ReactFlowStore = {
onNodesDelete?: OnNodesDelete; onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete; onEdgesDelete?: OnEdgesDelete;
onError?: OnError;
// event handlers // event handlers
onViewportChangeStart?: OnViewportChange; onViewportChangeStart?: OnViewportChange;
@@ -270,3 +271,5 @@ export type SelectionRect = Rect & {
startX: number; startX: number;
startY: number; startY: number;
}; };
export type OnError = (id: string, message: string) => void;
+4 -3
View File
@@ -13,6 +13,7 @@ import {
NodeInternals, NodeInternals,
NodeOrigin, NodeOrigin,
} from '../types'; } from '../types';
import { errorMessages } from '../contants';
export const isEdge = (element: Node | Connection | Edge): element is Edge => export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element; 'id' in element && 'source' in element && 'target' in element;
@@ -70,7 +71,7 @@ const connectionExists = (edge: Edge, edges: Edge[]) => {
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => { export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
devWarn("Can't create edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"); devWarn('006', errorMessages['006']());
return edges; return edges;
} }
@@ -94,7 +95,7 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] =>
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => { export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) { if (!newConnection.source || !newConnection.target) {
devWarn("Can't create a new edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"); devWarn('006', errorMessages['006']());
return edges; return edges;
} }
@@ -102,7 +103,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge; const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) { if (!foundEdge) {
devWarn(`The old edge with id=${oldEdge.id} does not exist. Help: https://reactflow.dev/error#700`); devWarn('007', errorMessages['007'](oldEdge.id));
return edges; return edges;
} }
+3 -3
View File
@@ -89,9 +89,9 @@ export const internalsSymbol = Symbol.for('internals');
// used for a11y key board controls for nodes and edges // used for a11y key board controls for nodes and edges
export const elementSelectionKeys = ['Enter', ' ', 'Escape']; export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
export const devWarn = (message: string) => { export const devWarn = (id: string, message: string) => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: ${message}`); console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
} }
}; };
@@ -118,7 +118,7 @@ export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boole
export const isMouseEvent = ( export const isMouseEvent = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent
): event is MouseEvent | ReactMouseEvent => 'button' in event; ): event is MouseEvent | ReactMouseEvent => 'clientX' in event;
export const getEventPosition = ( export const getEventPosition = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent, event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent,