mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 20:37:48 +00:00
feat: add workflow helperline (#2356)
* feat: add workflow helperline * del console * across zoom & adjust distance
This commit is contained in:
14
packages/global/core/workflow/type/index.d.ts
vendored
14
packages/global/core/workflow/type/index.d.ts
vendored
@@ -80,3 +80,17 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & {
|
||||
|
||||
workflow: WorkflowTemplateBasicType;
|
||||
};
|
||||
|
||||
export type THelperLine = {
|
||||
position: number;
|
||||
nodes: {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
}[];
|
||||
};
|
||||
|
@@ -0,0 +1,97 @@
|
||||
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
||||
import { CSSProperties, useEffect, useRef } from 'react';
|
||||
import { ReactFlowState, useStore, useViewport } from 'reactflow';
|
||||
|
||||
const canvasStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
pointerEvents: 'none'
|
||||
};
|
||||
|
||||
const storeSelector = (state: ReactFlowState) => ({
|
||||
width: state.width,
|
||||
height: state.height,
|
||||
transform: state.transform
|
||||
});
|
||||
|
||||
export type HelperLinesProps = {
|
||||
horizontal?: THelperLine;
|
||||
vertical?: THelperLine;
|
||||
};
|
||||
|
||||
function HelperLinesRenderer({ horizontal, vertical }: HelperLinesProps) {
|
||||
const { width, height, transform } = useStore(storeSelector);
|
||||
const { zoom } = useViewport();
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
|
||||
if (!ctx || !canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpi = window.devicePixelRatio;
|
||||
canvas.width = width * dpi;
|
||||
canvas.height = height * dpi;
|
||||
|
||||
ctx.scale(dpi, dpi);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.strokeStyle = '#D92D20';
|
||||
|
||||
const drawCross = (x: number, y: number, size: number) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - size, y - size);
|
||||
ctx.lineTo(x + size, y + size);
|
||||
ctx.moveTo(x + size, y - size);
|
||||
ctx.lineTo(x - size, y + size);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
if (vertical) {
|
||||
const x = vertical.position * transform[2] + transform[0];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
x,
|
||||
Math.min(...vertical.nodes.map((node) => node.top)) * transform[2] + transform[1]
|
||||
);
|
||||
ctx.lineTo(
|
||||
x,
|
||||
Math.max(...vertical.nodes.map((node) => node.bottom)) * transform[2] + transform[1]
|
||||
);
|
||||
ctx.stroke();
|
||||
|
||||
vertical.nodes.forEach((node) => {
|
||||
drawCross(x, node.top * transform[2] + transform[1], 5 * zoom);
|
||||
drawCross(x, node.bottom * transform[2] + transform[1], 5 * zoom);
|
||||
});
|
||||
}
|
||||
|
||||
if (horizontal) {
|
||||
const y = horizontal.position * transform[2] + transform[1];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(
|
||||
Math.min(...horizontal.nodes.map((node) => node.left)) * transform[2] + transform[0],
|
||||
y
|
||||
);
|
||||
ctx.lineTo(
|
||||
Math.max(...horizontal.nodes.map((node) => node.right)) * transform[2] + transform[0],
|
||||
y
|
||||
);
|
||||
ctx.stroke();
|
||||
|
||||
horizontal.nodes.forEach((node) => {
|
||||
drawCross(node.left * transform[2] + transform[0], y, 5 * zoom);
|
||||
drawCross(node.right * transform[2] + transform[0], y, 5 * zoom);
|
||||
});
|
||||
}
|
||||
}, [width, height, transform, horizontal, vertical]);
|
||||
|
||||
return <canvas ref={canvasRef} style={canvasStyle} />;
|
||||
}
|
||||
|
||||
export default HelperLinesRenderer;
|
@@ -3,6 +3,14 @@ import { WorkflowContext } from '../../context';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { Node, NodePositionChange, XYPosition } from 'reactflow';
|
||||
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
||||
|
||||
type GetHelperLinesResult = {
|
||||
horizontal?: THelperLine;
|
||||
vertical?: THelperLine;
|
||||
snapPosition: Partial<XYPosition>;
|
||||
};
|
||||
|
||||
export const useWorkflowUtils = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,8 +40,235 @@ export const useWorkflowUtils = () => {
|
||||
[nodeList]
|
||||
);
|
||||
|
||||
const getHelperLines = (
|
||||
change: NodePositionChange,
|
||||
nodes: Node[],
|
||||
distance = 8
|
||||
): GetHelperLinesResult => {
|
||||
const nodeA = nodes.find((node) => node.id === change.id);
|
||||
|
||||
if (!nodeA || !change.position) {
|
||||
return {
|
||||
horizontal: undefined,
|
||||
vertical: undefined,
|
||||
snapPosition: { x: undefined, y: undefined }
|
||||
};
|
||||
}
|
||||
|
||||
const nodeABounds = {
|
||||
left: change.position.x,
|
||||
right: change.position.x + (nodeA.width ?? 0),
|
||||
top: change.position.y,
|
||||
bottom: change.position.y + (nodeA.height ?? 0),
|
||||
width: nodeA.width ?? 0,
|
||||
height: nodeA.height ?? 0,
|
||||
centerX: change.position.x + (nodeA.width ?? 0) / 2,
|
||||
centerY: change.position.y + (nodeA.height ?? 0) / 2
|
||||
};
|
||||
|
||||
let horizontalDistance = distance;
|
||||
let verticalDistance = distance;
|
||||
|
||||
return nodes
|
||||
.filter((node) => node.id !== nodeA.id)
|
||||
.reduce<GetHelperLinesResult>(
|
||||
(result, nodeB) => {
|
||||
if (!result.vertical) {
|
||||
result.vertical = {
|
||||
position: nodeABounds.centerX,
|
||||
nodes: []
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.horizontal) {
|
||||
result.horizontal = {
|
||||
position: nodeABounds.centerY,
|
||||
nodes: []
|
||||
};
|
||||
}
|
||||
|
||||
const nodeBBounds = {
|
||||
left: nodeB.position.x,
|
||||
right: nodeB.position.x + (nodeB.width ?? 0),
|
||||
top: nodeB.position.y,
|
||||
bottom: nodeB.position.y + (nodeB.height ?? 0),
|
||||
width: nodeB.width ?? 0,
|
||||
height: nodeB.height ?? 0,
|
||||
centerX: nodeB.position.x + (nodeB.width ?? 0) / 2,
|
||||
centerY: nodeB.position.y + (nodeB.height ?? 0) / 2
|
||||
};
|
||||
|
||||
const distanceLeftLeft = Math.abs(nodeABounds.left - nodeBBounds.left);
|
||||
const distanceRightRight = Math.abs(nodeABounds.right - nodeBBounds.right);
|
||||
const distanceLeftRight = Math.abs(nodeABounds.left - nodeBBounds.right);
|
||||
const distanceRightLeft = Math.abs(nodeABounds.right - nodeBBounds.left);
|
||||
const distanceTopTop = Math.abs(nodeABounds.top - nodeBBounds.top);
|
||||
const distanceBottomTop = Math.abs(nodeABounds.bottom - nodeBBounds.top);
|
||||
const distanceBottomBottom = Math.abs(nodeABounds.bottom - nodeBBounds.bottom);
|
||||
const distanceTopBottom = Math.abs(nodeABounds.top - nodeBBounds.bottom);
|
||||
const distanceCenterXCenterX = Math.abs(nodeABounds.centerX - nodeBBounds.centerX);
|
||||
const distanceCenterYCenterY = Math.abs(nodeABounds.centerY - nodeBBounds.centerY);
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceLeftLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left;
|
||||
result.vertical.position = nodeBBounds.left;
|
||||
result.vertical.nodes = [nodeABounds, nodeBBounds];
|
||||
verticalDistance = distanceLeftLeft;
|
||||
} else if (distanceLeftLeft === verticalDistance) {
|
||||
result.vertical.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceRightRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right - nodeABounds.width;
|
||||
result.vertical.position = nodeBBounds.right;
|
||||
result.vertical.nodes = [nodeABounds, nodeBBounds];
|
||||
verticalDistance = distanceRightRight;
|
||||
} else if (distanceRightRight === verticalDistance) {
|
||||
result.vertical.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceLeftRight < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.right;
|
||||
result.vertical.position = nodeBBounds.right;
|
||||
result.vertical.nodes = [nodeABounds, nodeBBounds];
|
||||
verticalDistance = distanceLeftRight;
|
||||
} else if (distanceLeftRight === verticalDistance) {
|
||||
result.vertical.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceRightLeft < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.left - nodeABounds.width;
|
||||
result.vertical.position = nodeBBounds.left;
|
||||
result.vertical.nodes = [nodeABounds, nodeBBounds];
|
||||
verticalDistance = distanceRightLeft;
|
||||
} else if (distanceRightLeft === verticalDistance) {
|
||||
result.vertical.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________| |___________|
|
||||
if (distanceTopTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top;
|
||||
result.horizontal.position = nodeBBounds.top;
|
||||
result.horizontal.nodes = [nodeABounds, nodeBBounds];
|
||||
horizontalDistance = distanceTopTop;
|
||||
} else if (distanceTopTop === horizontalDistance) {
|
||||
result.horizontal.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|_________________
|
||||
// | |
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceBottomTop < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.top - nodeABounds.height;
|
||||
result.horizontal.position = nodeBBounds.top;
|
||||
result.horizontal.nodes = [nodeABounds, nodeBBounds];
|
||||
horizontalDistance = distanceBottomTop;
|
||||
} else if (distanceBottomTop === horizontalDistance) {
|
||||
result.horizontal.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A | | B |
|
||||
// |___________|_____|___________|
|
||||
if (distanceBottomBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom - nodeABounds.height;
|
||||
result.horizontal.position = nodeBBounds.bottom;
|
||||
result.horizontal.nodes = [nodeABounds, nodeBBounds];
|
||||
horizontalDistance = distanceBottomBottom;
|
||||
} else if (distanceBottomBottom === horizontalDistance) {
|
||||
result.horizontal.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// | |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// | A |
|
||||
// |___________|
|
||||
if (distanceTopBottom < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.bottom;
|
||||
result.horizontal.position = nodeBBounds.bottom;
|
||||
result.horizontal.nodes = [nodeABounds, nodeBBounds];
|
||||
horizontalDistance = distanceTopBottom;
|
||||
} else if (distanceTopBottom === horizontalDistance) {
|
||||
result.horizontal.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |
|
||||
// |___________|
|
||||
// |
|
||||
// |
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | B |
|
||||
// |___________|
|
||||
if (distanceCenterXCenterX < verticalDistance) {
|
||||
result.snapPosition.x = nodeBBounds.centerX - nodeABounds.width / 2;
|
||||
result.vertical.position = nodeBBounds.centerX;
|
||||
result.vertical.nodes = [nodeABounds, nodeBBounds];
|
||||
verticalDistance = distanceCenterXCenterX;
|
||||
} else if (distanceCenterXCenterX === verticalDistance) {
|
||||
result.vertical.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
// |‾‾‾‾‾‾‾‾‾‾‾| |‾‾‾‾‾‾‾‾‾‾‾|
|
||||
// | A |----| B |
|
||||
// |___________| |___________|
|
||||
if (distanceCenterYCenterY < horizontalDistance) {
|
||||
result.snapPosition.y = nodeBBounds.centerY - nodeABounds.height / 2;
|
||||
result.horizontal.position = nodeBBounds.centerY;
|
||||
result.horizontal.nodes = [nodeABounds, nodeBBounds];
|
||||
horizontalDistance = distanceCenterYCenterY;
|
||||
} else if (distanceCenterYCenterY === horizontalDistance) {
|
||||
result.horizontal.nodes.push(nodeBBounds);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
{ snapPosition: { x: undefined, y: undefined } } as GetHelperLinesResult
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
computedNewNodeName
|
||||
computedNewNodeName,
|
||||
getHelperLines
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,14 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Connection, NodeChange, OnConnectStartParams, addEdge, EdgeChange, Edge } from 'reactflow';
|
||||
import {
|
||||
Connection,
|
||||
NodeChange,
|
||||
OnConnectStartParams,
|
||||
addEdge,
|
||||
EdgeChange,
|
||||
Edge,
|
||||
applyNodeChanges,
|
||||
Node
|
||||
} from 'reactflow';
|
||||
import { EDGE_TYPE } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
@@ -8,6 +17,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useKeyboard } from './useKeyboard';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { useWorkflowUtils } from './useUtils';
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const { toast } = useToast();
|
||||
@@ -18,12 +28,47 @@ export const useWorkflow = () => {
|
||||
});
|
||||
|
||||
const { isDowningCtrl } = useKeyboard();
|
||||
const { setConnectingEdge, nodes, onNodesChange, setEdges, onEdgesChange, setHoverEdgeId } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
const {
|
||||
setConnectingEdge,
|
||||
nodes,
|
||||
setNodes,
|
||||
onNodesChange,
|
||||
setEdges,
|
||||
onEdgesChange,
|
||||
setHoverEdgeId,
|
||||
setHelperLineHorizontal,
|
||||
setHelperLineVertical
|
||||
} = useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const { getHelperLines } = useWorkflowUtils();
|
||||
|
||||
const customApplyNodeChanges = useCallback((changes: NodeChange[], nodes: Node[]): Node[] => {
|
||||
setHelperLineHorizontal(undefined);
|
||||
setHelperLineVertical(undefined);
|
||||
|
||||
if (
|
||||
changes.length === 1 &&
|
||||
changes[0].type === 'position' &&
|
||||
changes[0].dragging &&
|
||||
changes[0].position
|
||||
) {
|
||||
const helperLines = getHelperLines(changes[0], nodes);
|
||||
|
||||
changes[0].position.x = helperLines.snapPosition.x ?? changes[0].position.x;
|
||||
changes[0].position.y = helperLines.snapPosition.y ?? changes[0].position.y;
|
||||
|
||||
setHelperLineHorizontal(helperLines.horizontal);
|
||||
setHelperLineVertical(helperLines.vertical);
|
||||
}
|
||||
|
||||
return applyNodeChanges(changes, nodes);
|
||||
}, []);
|
||||
|
||||
/* node */
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
setNodes((nodes) => customApplyNodeChanges(changes, nodes));
|
||||
|
||||
for (const change of changes) {
|
||||
if (change.type === 'remove') {
|
||||
const node = nodes.find((n) => n.id === change.id);
|
||||
|
@@ -26,6 +26,7 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useWorkflow } from './hooks/useWorkflow';
|
||||
import { t } from 'i18next';
|
||||
import HelperLines from './components/HelperLines';
|
||||
|
||||
const NodeSimple = dynamic(() => import('./nodes/NodeSimple'));
|
||||
const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
@@ -62,7 +63,8 @@ const edgeTypes = {
|
||||
};
|
||||
|
||||
const Workflow = () => {
|
||||
const { nodes, edges, reactFlowWrapper } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const { nodes, edges, reactFlowWrapper, helperLineHorizontal, helperLineVertical } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
|
||||
const {
|
||||
ConfirmDeleteModal,
|
||||
@@ -135,6 +137,7 @@ const Workflow = () => {
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
>
|
||||
<FlowController />
|
||||
<HelperLines horizontal={helperLineHorizontal} vertical={helperLineVertical} />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
|
||||
|
@@ -48,6 +48,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { formatTime2HM, formatTime2YMDHMW } from '@fastgpt/global/common/string/time';
|
||||
import type { InitProps } from '@/pages/app/detail/components/PublishHistoriesSlider';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { THelperLine } from '@fastgpt/global/core/workflow/type';
|
||||
|
||||
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
|
||||
|
||||
@@ -135,6 +136,12 @@ type WorkflowContextType = {
|
||||
historiesDefaultData?: InitProps;
|
||||
setHistoriesDefaultData: React.Dispatch<React.SetStateAction<undefined | InitProps>>;
|
||||
|
||||
// helper line
|
||||
helperLineHorizontal?: THelperLine;
|
||||
setHelperLineHorizontal: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
|
||||
helperLineVertical?: THelperLine;
|
||||
setHelperLineVertical: React.Dispatch<React.SetStateAction<THelperLine | undefined>>;
|
||||
|
||||
// chat test
|
||||
setWorkflowTestData: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
@@ -260,6 +267,14 @@ export const WorkflowContext = createContext<WorkflowContextType>({
|
||||
setHistoriesDefaultData: function (value: React.SetStateAction<InitProps | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
helperLineHorizontal: undefined,
|
||||
setHelperLineHorizontal: function (value: React.SetStateAction<THelperLine | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
helperLineVertical: undefined,
|
||||
setHelperLineVertical: function (value: React.SetStateAction<THelperLine | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
getNodeDynamicInputs: function (nodeId: string): FlowNodeInputItemType[] {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
@@ -728,6 +743,11 @@ const WorkflowContextProvider = ({
|
||||
/* Version histories */
|
||||
const [historiesDefaultData, setHistoriesDefaultData] = useState<InitProps>();
|
||||
|
||||
/* helper line */
|
||||
const [helperLineHorizontal, setHelperLineHorizontal] = useState<THelperLine | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [helperLineVertical, setHelperLineVertical] = useState<THelperLine | undefined>(undefined);
|
||||
/* event bus */
|
||||
useEffect(() => {
|
||||
eventBus.on(EventNameEnum.requestWorkflowStore, () => {
|
||||
@@ -797,6 +817,12 @@ const WorkflowContextProvider = ({
|
||||
historiesDefaultData,
|
||||
setHistoriesDefaultData,
|
||||
|
||||
// helper line
|
||||
helperLineHorizontal,
|
||||
setHelperLineHorizontal,
|
||||
helperLineVertical,
|
||||
setHelperLineVertical,
|
||||
|
||||
// chat test
|
||||
setWorkflowTestData
|
||||
};
|
||||
|
Reference in New Issue
Block a user