Merge pull request #2877 from wbkd/feat/isValidConnection

feat(reactflow): add isValidConnection prop
This commit is contained in:
Moritz Klack
2023-03-06 15:00:16 +01:00
committed by GitHub
10 changed files with 49 additions and 13 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@reactflow/core': minor
---
add isValidConnection prop for ReactFlow component
@@ -12,6 +12,8 @@ import ReactFlow, {
OnConnectStart, OnConnectStart,
OnConnectEnd, OnConnectEnd,
OnConnect, OnConnect,
updateEdge,
Edge,
} from 'reactflow'; } from 'reactflow';
import styles from './validation.module.css'; import styles from './validation.module.css';
@@ -28,15 +30,15 @@ const isValidConnection = (connection: Connection) => connection.target === 'B';
const CustomInput: FC<NodeProps> = () => ( const CustomInput: FC<NodeProps> = () => (
<> <>
<div>Only connectable with B</div> <div>Only connectable with B</div>
<Handle type="source" position={Position.Right} isValidConnection={isValidConnection} /> <Handle type="source" position={Position.Right} />
</> </>
); );
const CustomNode: FC<NodeProps> = ({ id }) => ( const CustomNode: FC<NodeProps> = ({ id }) => (
<> <>
<Handle type="target" position={Position.Left} isValidConnection={isValidConnection} /> <Handle type="target" position={Position.Left} />
<div>{id}</div> <div>{id}</div>
<Handle type="source" position={Position.Right} isValidConnection={isValidConnection} /> <Handle type="source" position={Position.Right} />
</> </>
); );
@@ -74,6 +76,11 @@ const ValidationFlow = () => {
[value] [value]
); );
const onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => setEdges((els) => updateEdge(oldEdge, newConnection, els)),
[setEdges]
);
return ( return (
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
@@ -86,6 +93,8 @@ const ValidationFlow = () => {
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
onEdgeUpdate={onEdgeUpdate}
isValidConnection={isValidConnection}
fitView fitView
/> />
); );
@@ -11,6 +11,8 @@ import { getMouseHandler } from './utils';
import { elementSelectionKeys } from '../../utils'; import { elementSelectionKeys } from '../../utils';
import type { EdgeProps, WrapEdgeProps, Connection } from '../../types'; import type { EdgeProps, WrapEdgeProps, Connection } from '../../types';
const alwaysValidConnection = () => true;
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
id, id,
@@ -94,12 +96,14 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
return; return;
} }
const { edges, isValidConnection: isValidConnectionStore } = store.getState();
const nodeId = isSourceHandle ? target : source; const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null; const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source'; const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = () => true; const isValidConnection = isValidConnectionStore || alwaysValidConnection;
const isTarget = isSourceHandle; const isTarget = isSourceHandle;
const edge = store.getState().edges.find((e) => e.id === id)!; const edge = edges.find((e) => e.id === id)!;
setUpdating(true); setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType); onEdgeUpdateStart?.(event, edge, handleType);
+12 -6
View File
@@ -27,7 +27,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
{ {
type = 'source', type = 'source',
position = Position.Top, position = Position.Top,
isValidConnection = alwaysValid, isValidConnection,
isConnectable = true, isConnectable = true,
id, id,
onConnect, onConnect,
@@ -61,8 +61,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
...params, ...params,
}; };
if (hasDefaultEdges) { if (hasDefaultEdges) {
const { edges } = store.getState(); const { edges, setEdges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) }); setEdges(addEdge(edgeParams, edges));
} }
onConnectAction?.(edgeParams); onConnectAction?.(edgeParams);
@@ -81,7 +81,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isTarget, isTarget,
getState: store.getState, getState: store.getState,
setState: store.setState, setState: store.setState,
isValidConnection, isValidConnection: isValidConnection || store.getState().isValidConnection || alwaysValid,
}); });
} }
@@ -93,7 +93,12 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}; };
const onClick = (event: ReactMouseEvent) => { const onClick = (event: ReactMouseEvent) => {
const { onClickConnectStart, onClickConnectEnd, connectionMode } = store.getState(); const {
onClickConnectStart,
onClickConnectEnd,
connectionMode,
isValidConnection: isValidConnectionStore,
} = store.getState();
if (!connectionStartHandle) { if (!connectionStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } }); store.setState({ connectionStartHandle: { nodeId, type, handleId } });
@@ -101,6 +106,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
} }
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore || alwaysValid;
const { connection, isValid } = isValidHandle( const { connection, isValid } = isValidHandle(
event, event,
{ {
@@ -112,7 +118,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionStartHandle.nodeId, connectionStartHandle.nodeId,
connectionStartHandle.handleId || null, connectionStartHandle.handleId || null,
connectionStartHandle.type, connectionStartHandle.type,
isValidConnection, isValidConnectionHandler,
doc doc
); );
@@ -49,6 +49,7 @@ type StoreUpdaterProps = Pick<
| 'autoPanOnNodeDrag' | 'autoPanOnNodeDrag'
| 'onError' | 'onError'
| 'connectionRadius' | 'connectionRadius'
| 'isValidConnection'
> & { rfId: string }; > & { rfId: string };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -127,6 +128,7 @@ const StoreUpdater = ({
autoPanOnNodeDrag, autoPanOnNodeDrag,
onError, onError,
connectionRadius, connectionRadius,
isValidConnection,
}: StoreUpdaterProps) => { }: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
@@ -184,6 +186,7 @@ const StoreUpdater = ({
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState); useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError, store.setState); useDirectStoreUpdater('onError', onError, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState); useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges); useStoreUpdater<Edge[]>(edges, setEdges);
@@ -163,6 +163,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
autoPanOnConnect = true, autoPanOnConnect = true,
autoPanOnNodeDrag = true, autoPanOnNodeDrag = true,
connectionRadius = 20, connectionRadius = 20,
isValidConnection,
onError, onError,
style, style,
id, id,
@@ -295,6 +296,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
autoPanOnNodeDrag={autoPanOnNodeDrag} autoPanOnNodeDrag={autoPanOnNodeDrag}
onError={onError} onError={onError}
connectionRadius={connectionRadius} connectionRadius={connectionRadius}
isValidConnection={isValidConnection}
/> />
<SelectionListener onSelectionChange={onSelectionChange} /> <SelectionListener onSelectionChange={onSelectionChange} />
{children} {children}
+2 -2
View File
@@ -33,8 +33,8 @@ const createRFStore = () =>
return Array.from(get().nodeInternals.values()); return Array.from(get().nodeInternals.values());
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {} } = get(); const { defaultEdgeOptions = null } = get();
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) }); set({ edges: defaultEdgeOptions ? edges.map((e) => ({ ...defaultEdgeOptions, ...e })) : edges });
}, },
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => { setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
const hasDefaultNodes = typeof nodes !== 'undefined'; const hasDefaultNodes = typeof nodes !== 'undefined';
+1
View File
@@ -62,6 +62,7 @@ const initialState: ReactFlowStore = {
autoPanOnNodeDrag: true, autoPanOnNodeDrag: true,
connectionRadius: 20, connectionRadius: 20,
onError: devWarn, onError: devWarn,
isValidConnection: undefined,
}; };
export default initialState; export default initialState;
@@ -38,6 +38,7 @@ import type {
SelectionMode, SelectionMode,
OnError, OnError,
} from '.'; } from '.';
import { ValidConnectionFunc } from '../components/Handle/utils';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & { export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
nodes?: Node[]; nodes?: Node[];
@@ -144,6 +145,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
autoPanOnConnect?: boolean; autoPanOnConnect?: boolean;
connectionRadius?: number; connectionRadius?: number;
onError?: OnError; onError?: OnError;
isValidConnection?: ValidConnectionFunc;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+4
View File
@@ -61,6 +61,8 @@ export interface Connection {
targetHandle: string | null; targetHandle: string | null;
} }
export type IsValidConnection = (edge: Edge | Connection) => boolean;
export type ConnectionStatus = 'valid' | 'invalid'; export type ConnectionStatus = 'valid' | 'invalid';
export enum ConnectionMode { export enum ConnectionMode {
@@ -223,6 +225,8 @@ export type ReactFlowStore = {
autoPanOnConnect: boolean; autoPanOnConnect: boolean;
autoPanOnNodeDrag: boolean; autoPanOnNodeDrag: boolean;
connectionRadius: number; connectionRadius: number;
isValidConnection?: IsValidConnection;
}; };
export type ReactFlowActions = { export type ReactFlowActions = {