streamlined connections
This commit is contained in:
@@ -6,9 +6,9 @@ import type { ZoomBehavior } from 'd3-zoom';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { Transition } from 'd3-transition';
|
||||
|
||||
import type { XYPosition, Rect } from './utils';
|
||||
import type { XYPosition, Rect, Position } from './utils';
|
||||
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
|
||||
import type { ConnectingHandle, HandleType } from './handles';
|
||||
import type { Handle, HandleType } from './handles';
|
||||
import { PanZoomInstance } from './panzoom';
|
||||
import { EdgeBase } from '..';
|
||||
|
||||
@@ -133,21 +133,50 @@ export type OnError = (id: string, message: string) => void;
|
||||
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
|
||||
export type PanBy = (delta: XYPosition) => boolean;
|
||||
|
||||
export type NoConnectionInProgress = {
|
||||
position: XYPosition;
|
||||
export const initialConnection: NoConnection = {
|
||||
inProgress: false,
|
||||
isValid: null,
|
||||
from: null,
|
||||
fromHandle: null,
|
||||
fromPosition: null,
|
||||
fromNode: null,
|
||||
to: null,
|
||||
toHandle: null,
|
||||
toPosition: null,
|
||||
toNode: null,
|
||||
};
|
||||
|
||||
export type NoConnection = {
|
||||
inProgress: false;
|
||||
isValid: null;
|
||||
|
||||
from: null;
|
||||
fromHandle: null;
|
||||
fromPosition: null;
|
||||
fromNode: null;
|
||||
|
||||
to: null;
|
||||
toHandle: null;
|
||||
toPosition: null;
|
||||
toNode: null;
|
||||
};
|
||||
|
||||
export type ConnectionInProgress = {
|
||||
position: XYPosition;
|
||||
inProgress: true;
|
||||
isValid: boolean | null;
|
||||
fromHandle: ConnectingHandle;
|
||||
toHandle: ConnectingHandle | null;
|
||||
|
||||
from: XYPosition;
|
||||
fromHandle: Handle;
|
||||
fromPosition: Position;
|
||||
fromNode: NodeBase;
|
||||
|
||||
to: XYPosition;
|
||||
toHandle: Handle | null;
|
||||
toPosition: Position;
|
||||
toNode: NodeBase | null;
|
||||
};
|
||||
|
||||
export type ConnectionState = ConnectionInProgress | NoConnectionInProgress;
|
||||
export type ConnectionState = ConnectionInProgress | NoConnection;
|
||||
|
||||
export type UpdateConnection = (params: ConnectionState) => void;
|
||||
|
||||
|
||||
@@ -2,29 +2,15 @@ import type { Position, IsValidConnection } from '.';
|
||||
|
||||
export type HandleType = 'source' | 'target';
|
||||
|
||||
export type HandleElement = {
|
||||
export type Handle = {
|
||||
id?: string | null;
|
||||
nodeId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
position: Position;
|
||||
type: HandleType;
|
||||
width: number;
|
||||
height: number;
|
||||
position: Position;
|
||||
type?: HandleType;
|
||||
};
|
||||
|
||||
export type ConnectingHandle = {
|
||||
nodeId: string;
|
||||
type: HandleType;
|
||||
handleId?: string | null;
|
||||
position?: Position | null;
|
||||
};
|
||||
|
||||
export type ConnectionHandle = {
|
||||
id: string | null;
|
||||
type: HandleType;
|
||||
nodeId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type HandleProps = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
|
||||
import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
|
||||
import { Optional } from '../utils/types';
|
||||
|
||||
/**
|
||||
@@ -110,8 +110,8 @@ export type NodeProps<NodeType extends NodeBase> = Pick<
|
||||
};
|
||||
|
||||
export type NodeHandleBounds = {
|
||||
source: HandleElement[] | null;
|
||||
target: HandleElement[] | null;
|
||||
source: Handle[] | null;
|
||||
target: Handle[] | null;
|
||||
};
|
||||
|
||||
export type InternalNodeUpdate = {
|
||||
@@ -148,7 +148,7 @@ export type NodeOrigin = [number, number];
|
||||
|
||||
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
|
||||
|
||||
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
|
||||
export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ export enum Position {
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export const oppositePosition = {
|
||||
[Position.Left]: Position.Right,
|
||||
[Position.Right]: Position.Left,
|
||||
[Position.Top]: Position.Bottom,
|
||||
[Position.Bottom]: Position.Top,
|
||||
};
|
||||
|
||||
export type XYPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
@@ -51,3 +51,7 @@ export function handleConnectionChange(
|
||||
cb(diff);
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnectionStatus(isValid: boolean | null) {
|
||||
return isValid === null ? null : isValid ? 'valid' : 'invalid';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types';
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, Handle, Position } from '../types';
|
||||
import { snapPosition, pointToRendererPoint } from './general';
|
||||
|
||||
export type GetPointerPositionParams = {
|
||||
@@ -59,13 +59,14 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
|
||||
// We store them in the internals object of the node in order to avoid
|
||||
// unnecessary recalculations.
|
||||
export const getHandleBounds = (
|
||||
selector: string,
|
||||
type: 'source' | 'target',
|
||||
nodeElement: HTMLDivElement,
|
||||
nodeBounds: DOMRect,
|
||||
zoom: number,
|
||||
nodeId: string,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
): Handle[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(`.${type}`);
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return null;
|
||||
@@ -78,11 +79,13 @@ export const getHandleBounds = (
|
||||
y: nodeBounds.top + nodeBounds.height * nodeOrigin[1],
|
||||
};
|
||||
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
return handlesArray.map((handle): Handle => {
|
||||
const handleBounds = handle.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
type,
|
||||
nodeId,
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
x: (handleBounds.left - nodeOffset.x) / zoom,
|
||||
y: (handleBounds.top - nodeOffset.y) / zoom,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position } from '../../types/utils';
|
||||
import { Position, XYPosition } from '../../types/utils';
|
||||
import { errorMessages } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
import { Handle } from '../../types';
|
||||
import { getNodeDimensions } from '../general';
|
||||
|
||||
export type GetEdgePositionParams = {
|
||||
@@ -58,14 +58,14 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
const [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
|
||||
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition);
|
||||
const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
|
||||
const target = getHandlePosition(targetNode, targetHandle, targetPosition);
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
};
|
||||
@@ -84,9 +84,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
handle.height = handle.height ?? 1;
|
||||
|
||||
if (handle.type === 'source') {
|
||||
source.push(handle as HandleElement);
|
||||
source.push(handle as Handle);
|
||||
} else if (handle.type === 'target') {
|
||||
target.push(handle as HandleElement);
|
||||
target.push(handle as Handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,27 +98,33 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
|
||||
export function getHandlePosition(
|
||||
node: InternalNodeBase,
|
||||
handle: HandleElement | null,
|
||||
fallbackPosition: Position = Position.Left
|
||||
): number[] {
|
||||
handle: Handle | null,
|
||||
fallbackPosition: Position = Position.Left,
|
||||
center = false
|
||||
): XYPosition {
|
||||
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
|
||||
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
|
||||
const { width, height } = handle ?? getNodeDimensions(node);
|
||||
|
||||
if (center) {
|
||||
return { x: x + width / 2, y: y + height / 2 };
|
||||
}
|
||||
|
||||
const position = handle?.position ?? fallbackPosition;
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return [x + width / 2, y];
|
||||
return { x: x + width / 2, y };
|
||||
case Position.Right:
|
||||
return [x + width, y + height / 2];
|
||||
return { x: x + width, y: y + height / 2 };
|
||||
case Position.Bottom:
|
||||
return [x + width / 2, y + height];
|
||||
return { x: x + width / 2, y: y + height };
|
||||
case Position.Left:
|
||||
return [x, y + height / 2];
|
||||
return { x, y: y + height / 2 };
|
||||
}
|
||||
}
|
||||
|
||||
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
|
||||
function getHandle(bounds: Handle[], handleId?: string | null): Handle | null {
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -274,8 +274,8 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
||||
node.internals = {
|
||||
...node.internals,
|
||||
handleBounds: {
|
||||
source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin),
|
||||
target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin),
|
||||
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id, node.origin || nodeOrigin),
|
||||
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id, node.origin || nodeOrigin),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils';
|
||||
import { pointToRendererPoint, getHostForElement, calcAutoPan, getEventPosition, getHandlePosition } from '../utils';
|
||||
import {
|
||||
ConnectionMode,
|
||||
type OnConnect,
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type Handle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
NodeLookup,
|
||||
Position,
|
||||
oppositePosition,
|
||||
} from '../types';
|
||||
|
||||
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
|
||||
@@ -39,11 +39,11 @@ export type OnPointerDownParams = {
|
||||
isValidConnection?: IsValidConnection;
|
||||
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
getFromHandle: () => ConnectingHandle | null;
|
||||
getFromHandle: () => Handle | null;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
|
||||
handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
|
||||
connectionMode: ConnectionMode;
|
||||
fromNodeId: string;
|
||||
fromHandleId: string | null;
|
||||
@@ -52,6 +52,7 @@ export type IsValidParams = {
|
||||
doc: Document | ShadowRoot;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
handleLookup?: Handle[];
|
||||
};
|
||||
|
||||
export type XYHandleInstance = {
|
||||
@@ -63,13 +64,11 @@ type Result = {
|
||||
handleDomNode: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection | null;
|
||||
toHandle: ConnectingHandle | null;
|
||||
toHandle: Handle | null;
|
||||
};
|
||||
|
||||
const alwaysValid = () => true;
|
||||
|
||||
let fromHandle: ConnectingHandle | null = null;
|
||||
|
||||
function onPointerDown(
|
||||
event: MouseEvent | TouchEvent,
|
||||
{
|
||||
@@ -99,7 +98,7 @@ function onPointerDown(
|
||||
// when xyflow is used inside a shadow root we can't use document
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
let autoPanId = 0;
|
||||
let closestHandle: ConnectionHandle | null;
|
||||
let closestHandle: Handle | null;
|
||||
|
||||
const { x, y } = getEventPosition(event);
|
||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||
@@ -113,10 +112,10 @@ function onPointerDown(
|
||||
let position = getEventPosition(event, containerBounds);
|
||||
let autoPanStarted = false;
|
||||
let connection: Connection | null = null;
|
||||
let isValid = false;
|
||||
let isValid: boolean | null = false;
|
||||
let handleDomNode: Element | null = null;
|
||||
|
||||
const handleLookup = getHandleLookup({
|
||||
const [handleLookup, fromHandleInternal] = getHandleLookup({
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
@@ -135,18 +134,30 @@ function onPointerDown(
|
||||
}
|
||||
|
||||
// Stays the same for all consecutive pointermove events
|
||||
fromHandle = {
|
||||
const fromHandle: Handle = {
|
||||
...fromHandleInternal,
|
||||
nodeId,
|
||||
handleId,
|
||||
type: handleType,
|
||||
position: (clickedHandle?.getAttribute('data-handlepos') as Position) ?? Position.Top,
|
||||
position: fromHandleInternal.position,
|
||||
};
|
||||
|
||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||
|
||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||
|
||||
updateConnection({
|
||||
position,
|
||||
inProgress: true,
|
||||
isValid: null,
|
||||
|
||||
from,
|
||||
fromHandle,
|
||||
fromPosition: fromHandle.position,
|
||||
fromNode: fromNodeInternal.internals.userNode,
|
||||
|
||||
to: pointToRendererPoint(position, getTransform()),
|
||||
toHandle: null,
|
||||
toPosition: oppositePosition[fromHandle.position],
|
||||
toNode: null,
|
||||
});
|
||||
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
@@ -180,26 +191,29 @@ function onPointerDown(
|
||||
doc,
|
||||
lib,
|
||||
flowId,
|
||||
handleLookup,
|
||||
});
|
||||
|
||||
handleDomNode = result.handleDomNode;
|
||||
connection = result.connection;
|
||||
isValid = result.isValid;
|
||||
isValid = isConnectionValid(!!closestHandle, result.isValid);
|
||||
|
||||
updateConnection({
|
||||
inProgress: true,
|
||||
isValid,
|
||||
|
||||
from,
|
||||
fromHandle,
|
||||
position:
|
||||
fromPosition: fromHandle.position,
|
||||
fromNode: fromNodeInternal.internals.userNode,
|
||||
|
||||
to:
|
||||
closestHandle && isValid
|
||||
? rendererPointToPoint(
|
||||
{
|
||||
x: closestHandle.x,
|
||||
y: closestHandle.y,
|
||||
},
|
||||
transform
|
||||
)
|
||||
: position,
|
||||
isValid: isConnectionValid(!!closestHandle, isValid),
|
||||
? { x: closestHandle.x, y: closestHandle.y }
|
||||
: pointToRendererPoint(position, transform),
|
||||
toHandle: result.toHandle,
|
||||
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -222,7 +236,6 @@ function onPointerDown(
|
||||
isValid = false;
|
||||
connection = null;
|
||||
handleDomNode = null;
|
||||
fromHandle = null;
|
||||
|
||||
doc.removeEventListener('mousemove', onPointerMove as EventListener);
|
||||
doc.removeEventListener('mouseup', onPointerUp as EventListener);
|
||||
@@ -251,6 +264,7 @@ function isValidHandle(
|
||||
lib,
|
||||
flowId,
|
||||
isValidConnection = alwaysValid,
|
||||
handleLookup,
|
||||
}: IsValidParams
|
||||
) {
|
||||
const isTarget = fromType === 'target';
|
||||
@@ -301,12 +315,17 @@ function isValidHandle(
|
||||
|
||||
result.isValid = isValid && isValidConnection(connection);
|
||||
|
||||
result.toHandle = {
|
||||
nodeId: handleNodeId as string,
|
||||
handleId,
|
||||
type: handleType as HandleType,
|
||||
position: handleToCheck.getAttribute('data-handlepos') as Position,
|
||||
};
|
||||
if (handleLookup) {
|
||||
const toHandle = handleLookup.find(
|
||||
(h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
|
||||
);
|
||||
|
||||
if (toHandle) {
|
||||
result.toHandle = {
|
||||
...toHandle,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -3,40 +3,34 @@ import {
|
||||
type HandleType,
|
||||
type NodeHandleBounds,
|
||||
type XYPosition,
|
||||
type ConnectionHandle,
|
||||
type Handle,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
|
||||
// this functions collects all handles and adds an absolute position
|
||||
// so that we can later find the closest handle to the mouse position
|
||||
export function getHandles(
|
||||
function getHandles(
|
||||
node: InternalNodeBase,
|
||||
handleBounds: NodeHandleBounds,
|
||||
type: HandleType,
|
||||
currentHandle: string
|
||||
): ConnectionHandle[] {
|
||||
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => {
|
||||
if (`${node.id}-${handle.id}-${type}` !== currentHandle) {
|
||||
const [x, y] = getHandlePosition(node, handle);
|
||||
res.push({
|
||||
id: handle.id || null,
|
||||
type,
|
||||
nodeId: node.id,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
|
||||
): [Handle[], Handle | null] {
|
||||
let excludedHandle = null;
|
||||
const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
|
||||
if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
|
||||
excludedHandle = handle;
|
||||
} else {
|
||||
const handleXY = getHandlePosition(node, handle);
|
||||
res.push({ ...handle, ...handleXY });
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
return [handles, excludedHandle];
|
||||
}
|
||||
|
||||
export function getClosestHandle(
|
||||
pos: XYPosition,
|
||||
connectionRadius: number,
|
||||
handles: ConnectionHandle[]
|
||||
): ConnectionHandle | null {
|
||||
let closestHandles: ConnectionHandle[] = [];
|
||||
export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
|
||||
let closestHandles: Handle[] = [];
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const handle of handles) {
|
||||
@@ -66,7 +60,7 @@ type GetHandleLookupParams = {
|
||||
nodeLookup: NodeLookup;
|
||||
nodeId: string;
|
||||
handleId: string | null;
|
||||
handleType: string;
|
||||
handleType: HandleType;
|
||||
};
|
||||
|
||||
export function getHandleLookup({
|
||||
@@ -74,19 +68,21 @@ export function getHandleLookup({
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
}: GetHandleLookupParams): ConnectionHandle[] {
|
||||
const connectionHandles: ConnectionHandle[] = [];
|
||||
}: GetHandleLookupParams): [Handle[], Handle] {
|
||||
const connectionHandles: Handle[] = [];
|
||||
const currentHandle = { nodeId, handleId, handleType };
|
||||
let excludedHandle: Handle | null = null;
|
||||
|
||||
for (const [, node] of nodeLookup) {
|
||||
for (const node of nodeLookup.values()) {
|
||||
if (node.internals.handleBounds) {
|
||||
const id = `${nodeId}-${handleId}-${handleType}`;
|
||||
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
|
||||
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
|
||||
const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
|
||||
const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
|
||||
excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
|
||||
connectionHandles.push(...sourceHandles, ...targetHandles);
|
||||
}
|
||||
}
|
||||
|
||||
return connectionHandles;
|
||||
return [connectionHandles, excludedHandle!];
|
||||
}
|
||||
|
||||
export function getHandleType(
|
||||
|
||||
Reference in New Issue
Block a user