feat(onError): add onError prop #2772

This commit is contained in:
moklick
2023-01-25 17:47:42 +01:00
parent 3d6937b3e4
commit 9527261719
20 changed files with 68 additions and 42 deletions
@@ -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,7 +5,7 @@ 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';
@@ -42,7 +42,10 @@ 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', 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.');
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,8 @@
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';
type SymbolProps = Omit<EdgeMarker, 'type'>; type SymbolProps = Omit<EdgeMarker, 'type'>;
@@ -38,11 +38,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', `Marker type "${type}" doesn't exist.`);
return null; return null;
} }
@@ -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,7 +4,7 @@ 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';
@@ -36,13 +36,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 +77,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', `Node type "${nodeType}" not found. Using fallback type "default".`);
`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}
@@ -17,7 +17,8 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
const typeKeys = Object.keys(nodeOrEdgeTypes); const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) { if (shallow(typesKeysRef.current, typeKeys)) {
devWarn( 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" '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."
); );
} }
+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;
+5 -4
View File
@@ -1,7 +1,7 @@
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';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
@@ -62,7 +62,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 +82,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', 'Only child nodes can use a parent extent.');
currentExtent = nodeExtent; currentExtent = nodeExtent;
} }
+4 -4
View File
@@ -2,7 +2,7 @@ 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';
function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void { function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>): void {
const store = useStoreApi(); const store = useStoreApi();
@@ -18,9 +18,9 @@ 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
'The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400' .getState()
); .onError?.('004', 'The React Flow parent container needs a width and a height to render the graph.');
} }
store.setState({ width: size.width || 500, height: size.height || 500 }); store.setState({ width: size.width || 500, height: size.height || 500 });
@@ -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;
+3 -3
View File
@@ -70,7 +70,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', "Can't create edge. An edge needs a source and a target.");
return edges; return edges;
} }
@@ -94,7 +94,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', "Can't create a new edge. An edge needs a source and a target.");
return edges; return edges;
} }
@@ -102,7 +102,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', `The old edge with id=${oldEdge.id} does not exist.`);
return edges; return edges;
} }
+2 -2
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}`);
} }
}; };