diff --git a/examples/vite-app/cypress/e2e/basic.cy.ts b/examples/vite-app/cypress/e2e/basic.cy.ts index fd4c107a..c6f027c9 100644 --- a/examples/vite-app/cypress/e2e/basic.cy.ts +++ b/examples/vite-app/cypress/e2e/basic.cy.ts @@ -51,16 +51,16 @@ describe('Basic Flow Rendering', () => { .type('{shift}', { release: false }) .wait(50) .get('.react-flow__pane') - .trigger('mousedown', 1000, 50, { button: 0, force: true }) - .trigger('mousemove', 1, 400, { button: 0 }) + .trigger('mousedown', 1, 10, { button: 0, force: true }) + .trigger('mousemove', 1000, 450, { button: 0 }) .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(0).should('have.not.class', 'selected'); + cy.get('.react-flow__node').eq(3).should('have.not.class', 'selected'); cy.get('.react-flow__nodesselection-rect'); @@ -74,11 +74,13 @@ describe('Basic Flow Rendering', () => { .trigger('mousedown', 'topRight', { button: 0, force: true }) .trigger('mousemove', 'bottomLeft', { button: 0 }) .wait(50) - .trigger('mouseup', 'bottomLeft', { force: true }) - .wait(50) + .trigger('mouseup', 'bottomLeft', { button: 0, force: true }) + .wait(400) .get('.react-flow__node') - .should('have.class', 'selected') - .get('.react-flow__nodesselection-rect'); + .should('have.class', 'selected'); + + cy.wait(200); + cy.get('.react-flow__nodesselection-rect'); cy.get('body').type('{shift}', { release: true }); }); @@ -113,14 +115,14 @@ describe('Basic Flow Rendering', () => { cy.get('.react-flow__node') .contains('Node 3') .find('.react-flow__handle.source') - .trigger('mousedown', { button: 0 }); + .trigger('mousedown', { force: true, button: 0 }); cy.get('.react-flow__node') .contains('Node 4') .find('.react-flow__handle.target') - .trigger('mousemove', { force: true }) - .wait(50) - .trigger('mouseup', { force: true }); + .trigger('mousemove', { force: true, button: 0 }) + .wait(200) + .trigger('mouseup', { force: true, button: 0 }); cy.get('.react-flow__edge').should('have.length', 3); }); diff --git a/examples/vite-app/src/examples/Basic/index.tsx b/examples/vite-app/src/examples/Basic/index.tsx index 1197ef06..52d22025 100644 --- a/examples/vite-app/src/examples/Basic/index.tsx +++ b/examples/vite-app/src/examples/Basic/index.tsx @@ -48,8 +48,6 @@ const initialEdges: Edge[] = [ { id: 'e1-3', source: '1', target: '3' }, ]; -const nodeOrigin: NodeOrigin = [0.5, 0.5]; - const defaultEdgeOptions = {}; const BasicFlow = () => { @@ -96,7 +94,6 @@ const BasicFlow = () => { selectNodesOnDrag={false} elevateEdgesOnSelect elevateNodesOnSelect={false} - // nodeOrigin={nodeOrigin} > diff --git a/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx b/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx index 4fe25917..b27306dd 100644 --- a/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx +++ b/examples/vite-app/src/examples/CustomConnectionLine/ConnectionLine.tsx @@ -1,9 +1,8 @@ -import React, { FC } from 'react'; import { ConnectionLineComponentProps } from 'reactflow'; -const ConnectionLine: FC = ({ fromX, fromY, toX, toY }) => { +function ConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) { return ( - + <> = ({ fromX, fromY, toX, t d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`} /> - + ); -}; +} export default ConnectionLine; diff --git a/examples/vite-app/src/examples/Switch/index.tsx b/examples/vite-app/src/examples/Switch/index.tsx index 658082ed..b6ae2278 100644 --- a/examples/vite-app/src/examples/Switch/index.tsx +++ b/examples/vite-app/src/examples/Switch/index.tsx @@ -1,5 +1,5 @@ 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 onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); @@ -100,7 +100,6 @@ const BasicFlow = () => { onNodeClick={onNodeClick} onConnect={onConnect} onNodeDragStop={onNodeDragStop} - disableKeyboardA11y onNodeDrag={onNodeDrag} >
diff --git a/examples/vite-app/src/examples/Undirectional/index.tsx b/examples/vite-app/src/examples/Undirectional/index.tsx index 4ea676a5..4f25f4a5 100644 --- a/examples/vite-app/src/examples/Undirectional/index.tsx +++ b/examples/vite-app/src/examples/Undirectional/index.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useCallback } from 'react'; +import { MouseEvent, useCallback } from 'react'; import ReactFlow, { useReactFlow, NodeTypes, @@ -184,6 +184,7 @@ const UpdateNodeInternalsFlow = () => { const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els)); + const { project } = useReactFlow(); const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)); diff --git a/packages/core/src/components/ConnectionLine/index.tsx b/packages/core/src/components/ConnectionLine/index.tsx index 3693f769..fae0c3b1 100644 --- a/packages/core/src/components/ConnectionLine/index.tsx +++ b/packages/core/src/components/ConnectionLine/index.tsx @@ -6,16 +6,15 @@ import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; 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'; type ConnectionLineProps = { - connectionNodeId: string; - connectionHandleType: HandleType; - connectionLineType: ConnectionLineType; - isConnectable: boolean; - connectionLineStyle?: CSSProperties; - CustomConnectionLineComponent?: ConnectionLineComponent; + nodeId: string; + handleType: HandleType; + type: ConnectionLineType; + style?: CSSProperties; + CustomComponent?: ConnectionLineComponent; }; const oppositePosition = { @@ -26,69 +25,62 @@ const oppositePosition = { }; const ConnectionLine = ({ - connectionNodeId, - connectionHandleType, - connectionLineStyle, - connectionLineType = ConnectionLineType.Bezier, - isConnectable, - CustomConnectionLineComponent, + nodeId, + handleType, + style, + type = ConnectionLineType.Bezier, + CustomComponent, }: ConnectionLineProps) => { const { fromNode, handleId, toX, toY, connectionMode } = useStore( useCallback( (s: ReactFlowStore) => ({ - fromNode: s.nodeInternals.get(connectionNodeId), + fromNode: s.nodeInternals.get(nodeId), handleId: s.connectionHandleId, toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], connectionMode: s.connectionMode, }), - [connectionNodeId] + [nodeId] ), shallow ); const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; - let handleBounds = fromHandleBounds?.[connectionHandleType]; + let handleBounds = fromHandleBounds?.[handleType]; if (connectionMode === ConnectionMode.Loose) { - handleBounds = handleBounds - ? handleBounds - : fromHandleBounds?.[connectionHandleType === 'source' ? 'target' : 'source']; + handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source']; } - if (!fromNode || !isConnectable || !handleBounds) { + if (!fromNode || !handleBounds) { return null; } const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; - const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode?.width ?? 0) / 2; - const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0; - const fromX = (fromNode?.positionAbsolute?.x || 0) + fromHandleX; - const fromY = (fromNode?.positionAbsolute?.y || 0) + fromHandleY; - + const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.width ?? 0) / 2; + const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.height ?? 0; + const fromX = (fromNode.positionAbsolute?.x ?? 0) + fromHandleX; + const fromY = (fromNode.positionAbsolute?.y ?? 0) + fromHandleY; const fromPosition = fromHandle?.position; + const toPosition = fromPosition ? oppositePosition[fromPosition] : null; - if (!fromPosition) { + if (!fromPosition || !toPosition) { return null; } - const toPosition: Position = oppositePosition[fromPosition]; - - if (CustomConnectionLineComponent) { + if (CustomComponent) { return ( - - - + ); } @@ -103,29 +95,62 @@ const ConnectionLine = ({ targetPosition: toPosition, }; - if (connectionLineType === ConnectionLineType.Bezier) { + if (type === ConnectionLineType.Bezier) { // we assume the destination position is opposite to the source position [dAttr] = getBezierPath(pathParams); - } else if (connectionLineType === ConnectionLineType.Step) { + } else if (type === ConnectionLineType.Step) { [dAttr] = getSmoothStepPath({ ...pathParams, borderRadius: 0, }); - } else if (connectionLineType === ConnectionLineType.SmoothStep) { + } else if (type === ConnectionLineType.SmoothStep) { [dAttr] = getSmoothStepPath(pathParams); - } else if (connectionLineType === ConnectionLineType.SimpleBezier) { + } else if (type === ConnectionLineType.SimpleBezier) { [dAttr] = getSimpleBezierPath(pathParams); } else { dAttr = `M${fromX},${fromY} ${toX},${toY}`; } - return ( - - - - ); + return ; }; 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 ( + + + + + + ); +} + +export default ConnectionLineWrapper; diff --git a/packages/core/src/components/Edges/wrapEdge.tsx b/packages/core/src/components/Edges/wrapEdge.tsx index 36c49615..9d10f944 100644 --- a/packages/core/src/components/Edges/wrapEdge.tsx +++ b/packages/core/src/components/Edges/wrapEdge.tsx @@ -9,7 +9,7 @@ import { EdgeAnchor } from './EdgeAnchor'; import { getMarkerId } from '../../utils/graph'; import { getMouseHandler } 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) => { const EdgeWrapper = ({ @@ -115,7 +115,7 @@ export default (EdgeComponent: ComponentType) => { getState: store.getState, setState: store.setState, isValidConnection, - elementEdgeUpdaterType: handleType, + edgeUpdaterType: handleType, onEdgeUpdateEnd: _onEdgeUpdateEnd, }); }; diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index aa887d2f..59d2bd7f 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -5,11 +5,12 @@ import { shallow } from 'zustand/shallow'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; import { handlePointerDown } from './handler'; -import { devWarn, getHostForElement, isMouseEvent } from '../../utils'; +import { getHostForElement, isMouseEvent } from '../../utils'; import { addEdge } from '../../utils/graph'; import { Position } from '../../types'; import type { HandleProps, Connection, ReactFlowState } from '../../types'; import { isValidHandle } from './utils'; +import { errorMessages } from '../../contants'; const alwaysValid = () => true; @@ -42,7 +43,8 @@ const Handle = forwardRef( const nodeId = useNodeId(); 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; } diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index 74c0ef5a..a049a2cc 100644 --- a/packages/core/src/components/Handle/utils.ts +++ b/packages/core/src/components/Handle/utils.ts @@ -98,7 +98,7 @@ export function isValidHandle( const isValid = connectionMode === ConnectionMode.Strict ? (isTarget && handleIsSource) || (!isTarget && handleIsTarget) - : handleNodeId !== handle.nodeId || handleId !== handle.id; + : handleNodeId !== fromNodeId || handleId !== fromHandleId; if (isValid) { result.isValid = isValidConnection(connection); diff --git a/packages/core/src/components/StoreUpdater/index.tsx b/packages/core/src/components/StoreUpdater/index.tsx index 55af8fb4..c5645944 100644 --- a/packages/core/src/components/StoreUpdater/index.tsx +++ b/packages/core/src/components/StoreUpdater/index.tsx @@ -47,6 +47,7 @@ type StoreUpdaterProps = Pick< | 'elevateNodesOnSelect' | 'autoPanOnConnect' | 'autoPanOnNodeDrag' + | 'onError' > & { rfId: string }; const selector = (s: ReactFlowState) => ({ @@ -123,6 +124,7 @@ const StoreUpdater = ({ rfId, autoPanOnConnect, autoPanOnNodeDrag, + onError, }: StoreUpdaterProps) => { const { setNodes, @@ -178,6 +180,7 @@ const StoreUpdater = ({ useDirectStoreUpdater('rfId', rfId, store.setState); useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState); useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState); + useDirectStoreUpdater('onError', onError, store.setState); useStoreUpdater(nodes, setNodes); useStoreUpdater(edges, setEdges); diff --git a/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx b/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx index fbdd4972..7429f635 100644 --- a/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx +++ b/packages/core/src/container/EdgeRenderer/MarkerSymbols.tsx @@ -1,8 +1,9 @@ import { useMemo } from 'react'; -import { devWarn } from '../../utils'; import { MarkerType } from '../../types'; import type { EdgeMarker } from '../../types'; +import { useStoreApi } from '../../hooks/useStore'; +import { errorMessages } from '../../contants'; type SymbolProps = Omit; @@ -38,11 +39,12 @@ export const MarkerSymbols = { }; export function useMarkerSymbol(type: MarkerType) { + const store = useStoreApi(); const symbol = useMemo(() => { const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type); if (!symbolExists) { - devWarn(`Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`); + store.getState().onError?.('009', errorMessages['009'](type)); return null; } diff --git a/packages/core/src/container/EdgeRenderer/index.tsx b/packages/core/src/container/EdgeRenderer/index.tsx index c687a8d8..ddc4aa4d 100644 --- a/packages/core/src/container/EdgeRenderer/index.tsx +++ b/packages/core/src/container/EdgeRenderer/index.tsx @@ -1,27 +1,20 @@ -import { memo } from 'react'; +import { memo, ReactNode } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; import { useStore } from '../../hooks/useStore'; import useVisibleEdges from '../../hooks/useVisibleEdges'; -import ConnectionLine from '../../components/ConnectionLine/index'; import MarkerDefinitions from './MarkerDefinitions'; import { getEdgePositions, getHandle, getNodeData } from './utils'; import { GraphViewProps } from '../GraphView'; -import { devWarn } from '../../utils'; import { ConnectionMode, Position } from '../../types'; import type { Edge, ReactFlowState } from '../../types'; +import { errorMessages } from '../../contants'; type EdgeRendererProps = Pick< GraphViewProps, | 'edgeTypes' - | 'connectionLineType' - | 'connectionLineType' - | 'connectionLineStyle' - | 'connectionLineComponent' - | 'connectionLineContainerStyle' - | 'connectionLineContainerStyle' | 'onEdgeClick' | 'onEdgeDoubleClick' | 'defaultMarkerColor' @@ -40,11 +33,10 @@ type EdgeRendererProps = Pick< | 'disableKeyboardA11y' > & { elevateEdgesOnSelect: boolean; + children: ReactNode; }; const selector = (s: ReactFlowState) => ({ - connectionNodeId: s.connectionNodeId, - connectionHandleType: s.connectionHandleType, nodesConnectable: s.nodesConnectable, edgesFocusable: s.edgesFocusable, elementsSelectable: s.elementsSelectable, @@ -52,35 +44,38 @@ const selector = (s: ReactFlowState) => ({ height: s.height, connectionMode: s.connectionMode, nodeInternals: s.nodeInternals, + onError: s.onError, }); -const EdgeRenderer = (props: EdgeRendererProps) => { - const { - connectionNodeId, - connectionHandleType, - nodesConnectable, - edgesFocusable, - elementsSelectable, - width, - height, - connectionMode, - nodeInternals, - } = useStore(selector, shallow); - const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect); +const EdgeRenderer = ({ + defaultMarkerColor, + onlyRenderVisibleElements, + elevateEdgesOnSelect, + rfId, + edgeTypes, + noPanClassName, + onEdgeUpdate, + onEdgeContextMenu, + onEdgeMouseEnter, + onEdgeMouseMove, + onEdgeMouseLeave, + 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) { return null; } - const { - connectionLineType, - defaultMarkerColor, - connectionLineStyle, - connectionLineComponent, - connectionLineContainerStyle, - } = props; - const renderConnectionLine = connectionNodeId && connectionHandleType; - return ( <> {edgeTree.map(({ level, edges, isMaxLevel }) => ( @@ -91,11 +86,11 @@ const EdgeRenderer = (props: EdgeRendererProps) => { height={height} className="react-flow__edges react-flow__container" > - {isMaxLevel && } + {isMaxLevel && } {edges.map((edge: Edge) => { - const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)!); - const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)!); + const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source)); + const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target)); if (!sourceIsValid || !targetIsValid) { return null; @@ -103,32 +98,25 @@ const EdgeRenderer = (props: EdgeRendererProps) => { let edgeType = edge.type || 'default'; - if (!props.edgeTypes[edgeType]) { - devWarn( - `Edge type "${edgeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300` - ); - + if (!edgeTypes[edgeType]) { + onError?.('011', errorMessages['011'](edgeType)); edgeType = 'default'; } - const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; - // when connection type is loose we can define all handles as sources + const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; + // when connection type is loose we can define all handles as sources and connect source -> source const targetNodeHandles = connectionMode === ConnectionMode.Strict ? targetHandleBounds!.target : (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []); - const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null); - const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null); + const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle); + const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle); const sourcePosition = sourceHandle?.position || Position.Bottom; const targetPosition = targetHandle?.position || Position.Top; const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); if (!sourceHandle || !targetHandle) { - devWarn( - `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` - ); + onError?.('008', errorMessages['008'](sourceHandle, edge)); return null; } @@ -146,7 +134,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { { sourcePosition={sourcePosition} targetPosition={targetPosition} elementsSelectable={elementsSelectable} - onEdgeUpdate={props.onEdgeUpdate} - onContextMenu={props.onEdgeContextMenu} - onMouseEnter={props.onEdgeMouseEnter} - onMouseMove={props.onEdgeMouseMove} - onMouseLeave={props.onEdgeMouseLeave} - onClick={props.onEdgeClick} - edgeUpdaterRadius={props.edgeUpdaterRadius} - onEdgeDoubleClick={props.onEdgeDoubleClick} - onEdgeUpdateStart={props.onEdgeUpdateStart} - onEdgeUpdateEnd={props.onEdgeUpdateEnd} - rfId={props.rfId} + onEdgeUpdate={onEdgeUpdate} + onContextMenu={onEdgeContextMenu} + onMouseEnter={onEdgeMouseEnter} + onMouseMove={onEdgeMouseMove} + onMouseLeave={onEdgeMouseLeave} + onClick={onEdgeClick} + edgeUpdaterRadius={edgeUpdaterRadius} + onEdgeDoubleClick={onEdgeDoubleClick} + onEdgeUpdateStart={onEdgeUpdateStart} + onEdgeUpdateEnd={onEdgeUpdateEnd} + rfId={rfId} ariaLabel={edge.ariaLabel} isFocusable={isFocusable} pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined} @@ -193,23 +181,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => { ))} - {renderConnectionLine && ( - - - - )} + {children} ); }; diff --git a/packages/core/src/container/EdgeRenderer/utils.ts b/packages/core/src/container/EdgeRenderer/utils.ts index eee01706..edd74543 100644 --- a/packages/core/src/container/EdgeRenderer/utils.ts +++ b/packages/core/src/container/EdgeRenderer/utils.ts @@ -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) { return null; } - // there is no handleId when there are no multiple handles/ handles with ids - // so we just pick the first one - let handle: HandleElement | null = null; - if (bounds.length === 1 || !handleId) { - handle = bounds[0]; - } else if (handleId) { - handle = bounds.find((d) => d.id === handleId)!; + if (handleId) { + return bounds.find((d) => d.id === handleId)!; + } else if (bounds.length === 1) { + return bounds[0]; } - return typeof handle === 'undefined' ? null : handle; + return null; } interface EdgePositions { @@ -167,16 +164,15 @@ export function isEdgeVisible({ 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 isInvalid = - !node || - !handleBounds || - !node.width || - !node.height || - typeof node.positionAbsolute?.x === 'undefined' || - typeof node.positionAbsolute?.y === 'undefined'; + const isValid = + handleBounds && + node?.width && + node?.height && + typeof node?.positionAbsolute?.x !== 'undefined' && + typeof node?.positionAbsolute?.y !== 'undefined'; return [ { @@ -186,6 +182,6 @@ export function getNodeData(node: Node): [Rect, NodeHandleBounds | null, boolean height: node?.height || 0, }, handleBounds, - !isInvalid, + !!isValid, ]; } diff --git a/packages/core/src/container/GraphView/index.tsx b/packages/core/src/container/GraphView/index.tsx index 425d9180..3de2550c 100644 --- a/packages/core/src/container/GraphView/index.tsx +++ b/packages/core/src/container/GraphView/index.tsx @@ -6,6 +6,7 @@ import EdgeRenderer from '../EdgeRenderer'; import ViewportWrapper from '../Viewport'; import useOnInitHandler from '../../hooks/useOnInitHandler'; import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types'; +import ConnectionLine from '../../components/ConnectionLine'; export type GraphViewProps = Omit & Required< @@ -149,10 +150,6 @@ const GraphView = ({ edgeTypes={edgeTypes} onEdgeClick={onEdgeClick} onEdgeDoubleClick={onEdgeDoubleClick} - connectionLineType={connectionLineType} - connectionLineStyle={connectionLineStyle} - connectionLineComponent={connectionLineComponent} - connectionLineContainerStyle={connectionLineContainerStyle} onEdgeUpdate={onEdgeUpdate} onlyRenderVisibleElements={onlyRenderVisibleElements} onEdgeContextMenu={onEdgeContextMenu} @@ -167,7 +164,14 @@ const GraphView = ({ elevateEdgesOnSelect={!!elevateEdgesOnSelect} disableKeyboardA11y={disableKeyboardA11y} rfId={rfId} - /> + > + +
({ nodesFocusable: s.nodesFocusable, elementsSelectable: s.elementsSelectable, updateNodeDimensions: s.updateNodeDimensions, + onError: s.onError, }); const NodeRenderer = (props: NodeRendererProps) => { - const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore( - selector, - shallow - ); + const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } = + useStore(selector, shallow); const nodes = useVisibleNodes(props.onlyRenderVisibleElements); const resizeObserverRef = useRef(); @@ -78,9 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => { let nodeType = node.type || 'default'; if (!props.nodeTypes[nodeType]) { - devWarn( - `Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300` - ); + onError?.('003', errorMessages['003'](nodeType)); nodeType = 'default'; } diff --git a/packages/core/src/container/NodeRenderer/utils.ts b/packages/core/src/container/NodeRenderer/utils.ts index ebac7413..6abc695d 100644 --- a/packages/core/src/container/NodeRenderer/utils.ts +++ b/packages/core/src/container/NodeRenderer/utils.ts @@ -5,7 +5,6 @@ import InputNode from '../../components/Nodes/InputNode'; import OutputNode from '../../components/Nodes/OutputNode'; import GroupNode from '../../components/Nodes/GroupNode'; import wrapNode from '../../components/Nodes/wrapNode'; -import { devWarn } from '../../utils'; import type { NodeTypes, NodeProps, NodeTypesWrapped, NodeOrigin, XYPosition } from '../../types'; 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) { - devWarn('nodeOrigin must be between 0 and 1'); return { x, y }; } diff --git a/packages/core/src/container/ReactFlow/index.tsx b/packages/core/src/container/ReactFlow/index.tsx index 67964f0f..120a5efa 100644 --- a/packages/core/src/container/ReactFlow/index.tsx +++ b/packages/core/src/container/ReactFlow/index.tsx @@ -163,6 +163,7 @@ const ReactFlow = forwardRef( autoPanOnConnect = true, autoPanOnNodeDrag = true, connectionRadius = 20, + onError, style, id, ...rest @@ -292,6 +293,7 @@ const ReactFlow = forwardRef( rfId={rfId} autoPanOnConnect={autoPanOnConnect} autoPanOnNodeDrag={autoPanOnNodeDrag} + onError={onError} /> {children} diff --git a/packages/core/src/container/ReactFlow/utils.ts b/packages/core/src/container/ReactFlow/utils.ts index 2f8a9239..cc5ab36d 100644 --- a/packages/core/src/container/ReactFlow/utils.ts +++ b/packages/core/src/container/ReactFlow/utils.ts @@ -5,6 +5,7 @@ import { devWarn } from '../../utils'; import { CreateEdgeTypes } from '../EdgeRenderer/utils'; import { CreateNodeTypes } from '../NodeRenderer/utils'; import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types'; +import { errorMessages } from '../../contants'; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped; 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') { const typeKeys = Object.keys(nodeOrEdgeTypes); if (shallow(typesKeysRef.current, typeKeys)) { - devWarn( - "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" - ); + devWarn('002', errorMessages['002']()); } typesKeysRef.current = typeKeys; diff --git a/packages/core/src/contants.ts b/packages/core/src/contants.ts new file mode 100644 index 00000000..815ac6f0 --- /dev/null +++ b/packages/core/src/contants.ts @@ -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".`, +}; diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts index 868b6387..c6f3d269 100644 --- a/packages/core/src/hooks/useDrag/index.ts +++ b/packages/core/src/hooks/useDrag/index.ts @@ -61,6 +61,7 @@ function useDrag({ snapGrid, snapToGrid, nodeOrigin, + onError, } = store.getState(); lastPos.current = { x, y }; @@ -75,7 +76,7 @@ function useDrag({ 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 hasChange = hasChange || n.position.x !== updatedPos.position.x || n.position.y !== updatedPos.position.y; diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts index 74484baf..ed40053b 100644 --- a/packages/core/src/hooks/useDrag/utils.ts +++ b/packages/core/src/hooks/useDrag/utils.ts @@ -1,8 +1,9 @@ import type { RefObject } from 'react'; -import { clampPosition, devWarn, isNumeric } from '../../utils'; -import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; +import { clampPosition, isNumeric } from '../../utils'; +import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, OnError, XYPosition } from '../../types'; import { getNodePositionWithOrigin } from '../../utils/graph'; +import { errorMessages } from '../../contants'; export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { if (!node.parentNode) { @@ -62,7 +63,8 @@ export function calcNextPosition( nextPosition: XYPosition, nodeInternals: NodeInternals, nodeExtent?: CoordinateExtent, - nodeOrigin: NodeOrigin = [0, 0] + nodeOrigin: NodeOrigin = [0, 0], + onError?: OnError ): { position: XYPosition; positionAbsolute: XYPosition } { let currentExtent = node.extent || nodeExtent; @@ -81,7 +83,7 @@ export function calcNextPosition( ] : currentExtent; } else { - devWarn('Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500'); + onError?.('005', errorMessages['005']()); currentExtent = nodeExtent; } diff --git a/packages/core/src/hooks/useResizeHandler.ts b/packages/core/src/hooks/useResizeHandler.ts index d2ca3e63..a6d6d921 100644 --- a/packages/core/src/hooks/useResizeHandler.ts +++ b/packages/core/src/hooks/useResizeHandler.ts @@ -2,7 +2,8 @@ import { useEffect } from 'react'; import type { MutableRefObject } from 'react'; import { useStoreApi } from '../hooks/useStore'; -import { devWarn, getDimensions } from '../utils'; +import { getDimensions } from '../utils'; +import { errorMessages } from '../contants'; function useResizeHandler(rendererNode: MutableRefObject): void { const store = useStoreApi(); @@ -18,9 +19,7 @@ function useResizeHandler(rendererNode: MutableRefObject) const size = getDimensions(rendererNode.current); if (size.height === 0 || size.width === 0) { - devWarn( - 'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400' - ); + store.getState().onError?.('004', errorMessages['004']()); } store.setState({ width: size.width || 500, height: size.height || 500 }); diff --git a/packages/core/src/hooks/useStore.ts b/packages/core/src/hooks/useStore.ts index bc0e605e..3983579d 100644 --- a/packages/core/src/hooks/useStore.ts +++ b/packages/core/src/hooks/useStore.ts @@ -3,10 +3,10 @@ import { useStore as useZustandStore } from 'zustand'; import type { StoreApi } from 'zustand'; import StoreContext from '../contexts/RFStoreContext'; +import { errorMessages } from '../contants'; import type { ReactFlowState } from '../types'; -const errorMessage = - '[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#100'; +const zustandErrorMessage = errorMessages['001'](); type ExtractState = StoreApi extends { getState: () => infer T } ? T : never; @@ -17,7 +17,7 @@ function useStore( const store = useContext(StoreContext); if (store === null) { - throw new Error(errorMessage); + throw new Error(zustandErrorMessage); } return useZustandStore(store, selector, equalityFn); @@ -27,7 +27,7 @@ const useStoreApi = () => { const store = useContext(StoreContext); if (store === null) { - throw new Error(errorMessage); + throw new Error(zustandErrorMessage); } return useMemo( diff --git a/packages/core/src/hooks/useUpdateNodePositions.ts b/packages/core/src/hooks/useUpdateNodePositions.ts index b20ea4d1..131f85cd 100644 --- a/packages/core/src/hooks/useUpdateNodePositions.ts +++ b/packages/core/src/hooks/useUpdateNodePositions.ts @@ -7,7 +7,8 @@ function useUpdateNodePositions() { const store = useStoreApi(); 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); // 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. @@ -27,10 +28,17 @@ function useUpdateNodePositions() { 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.positionAbsolute = updatedPos.positionAbsolute; + n.position = position; + n.positionAbsolute = positionAbsolute; } return n; diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts index 6c475b5b..afccec2b 100644 --- a/packages/core/src/store/initialState.ts +++ b/packages/core/src/store/initialState.ts @@ -1,3 +1,4 @@ +import { devWarn } from '../utils'; import { ConnectionMode } from '../types'; import type { CoordinateExtent, ReactFlowStore } from '../types'; @@ -59,6 +60,7 @@ const initialState: ReactFlowStore = { autoPanOnConnect: true, autoPanOnNodeDrag: true, connectionRadius: 20, + onError: devWarn, }; export default initialState; diff --git a/packages/core/src/styles/init.css b/packages/core/src/styles/init.css index a621794f..9cecc294 100644 --- a/packages/core/src/styles/init.css +++ b/packages/core/src/styles/init.css @@ -98,7 +98,7 @@ .react-flow__connection { pointer-events: none; - &.animated { + .animated { stroke-dasharray: 5; animation: dashdraw 0.5s linear infinite; } diff --git a/packages/core/src/types/component-props.ts b/packages/core/src/types/component-props.ts index bafb8d5e..1614805a 100644 --- a/packages/core/src/types/component-props.ts +++ b/packages/core/src/types/component-props.ts @@ -36,6 +36,7 @@ import type { EdgeMouseHandler, HandleType, SelectionMode, + OnError, } from '.'; export type ReactFlowProps = HTMLAttributes & { @@ -142,6 +143,7 @@ export type ReactFlowProps = HTMLAttributes & { autoPanOnNodeDrag?: boolean; autoPanOnConnect?: boolean; connectionRadius?: number; + onError?: OnError; }; export type ReactFlowRefType = HTMLDivElement; diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index ba0de157..9dbd3dce 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -206,6 +206,7 @@ export type ReactFlowStore = { onNodesDelete?: OnNodesDelete; onEdgesDelete?: OnEdgesDelete; + onError?: OnError; // event handlers onViewportChangeStart?: OnViewportChange; @@ -270,3 +271,5 @@ export type SelectionRect = Rect & { startX: number; startY: number; }; + +export type OnError = (id: string, message: string) => void; diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index 7e94f766..c7772a1c 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -13,6 +13,7 @@ import { NodeInternals, NodeOrigin, } from '../types'; +import { errorMessages } from '../contants'; export const isEdge = (element: Node | Connection | Edge): element is Edge => '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[] => { 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; } @@ -94,7 +95,7 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => { 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; } @@ -102,7 +103,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge; 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; } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 5bfd52c2..f1021bfb 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -89,9 +89,9 @@ export const internalsSymbol = Symbol.for('internals'); // used for a11y key board controls for nodes and edges export const elementSelectionKeys = ['Enter', ' ', 'Escape']; -export const devWarn = (message: string) => { +export const devWarn = (id: string, message: string) => { 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 = ( event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent -): event is MouseEvent | ReactMouseEvent => 'button' in event; +): event is MouseEvent | ReactMouseEvent => 'clientX' in event; export const getEventPosition = ( event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent,