feat: add context menu & comment node (#2834)

* feat: add comment node

* useMemo
This commit is contained in:
heheer
2024-09-29 10:08:19 +08:00
committed by GitHub
parent 7bdff9ce9c
commit 7c829febec
14 changed files with 439 additions and 112 deletions

View File

@@ -151,7 +151,11 @@ export enum NodeInputKeyEnum {
loopEndInput = 'loopEndInput',
// form input
userInputForms = 'userInputForms'
userInputForms = 'userInputForms',
// comment
commentText = 'commentText',
commentSize = 'commentSize'
}
export enum NodeOutputKeyEnum {

View File

@@ -130,7 +130,8 @@ export enum FlowNodeTypeEnum {
loop = 'loop',
loopStart = 'loopStart',
loopEnd = 'loopEnd',
formInput = 'formInput'
formInput = 'formInput',
comment = 'comment'
}
// node IO value type

View File

@@ -0,0 +1,40 @@
import { FlowNodeTypeEnum } from '../../node/constant';
import { FlowNodeTemplateType } from '../../type/node.d';
import {
FlowNodeTemplateTypeEnum,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '../../constants';
import { getHandleConfig } from '../utils';
export const CommentNode: FlowNodeTemplateType = {
id: FlowNodeTypeEnum.comment,
templateType: FlowNodeTemplateTypeEnum.systemInput,
flowNodeType: FlowNodeTypeEnum.comment,
sourceHandle: getHandleConfig(false, false, false, false),
targetHandle: getHandleConfig(false, false, false, false),
avatar: '',
name: '',
intro: '',
version: '4811',
inputs: [
{
key: NodeInputKeyEnum.commentText,
renderTypeList: [],
valueType: WorkflowIOValueTypeEnum.string,
label: '',
value: ''
},
{
key: NodeInputKeyEnum.commentSize,
renderTypeList: [],
valueType: WorkflowIOValueTypeEnum.object,
label: '',
value: {
width: 240,
height: 140
}
}
],
outputs: []
};

View File

@@ -107,6 +107,7 @@ const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.pluginConfig]: () => Promise.resolve(),
[FlowNodeTypeEnum.emptyNode]: () => Promise.resolve(),
[FlowNodeTypeEnum.globalVariable]: () => Promise.resolve(),
[FlowNodeTypeEnum.comment]: () => Promise.resolve(),
[FlowNodeTypeEnum.runApp]: dispatchAppRequest // abandoned
};

View File

@@ -7,6 +7,7 @@ export const iconPaths = {
closeSolid: () => import('./icons/closeSolid.svg'),
collectionLight: () => import('./icons/collectionLight.svg'),
collectionSolid: () => import('./icons/collectionSolid.svg'),
comment: () => import('./icons/comment.svg'),
'common/add2': () => import('./icons/common/add2.svg'),
'common/addCircleLight': () => import('./icons/common/addCircleLight.svg'),
'common/addLight': () => import('./icons/common/addLight.svg'),

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" >
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7746 2.90224H5.22543C4.67152 2.90224 4.33762 2.90331 4.08927 2.92394C3.91796 2.93817 3.85352 2.95783 3.83939 2.96308C3.74318 3.01345 3.66463 3.09199 3.61426 3.18821C3.60902 3.20234 3.58935 3.26677 3.57512 3.43809C3.5545 3.68644 3.55343 4.02034 3.55343 4.57424V12.4544C3.55343 13.1464 3.55481 13.5712 3.58283 13.8723C3.58462 13.8915 3.58646 13.9093 3.58832 13.9258C3.60297 13.9181 3.61873 13.9096 3.63562 13.9002C3.90035 13.754 4.2523 13.5161 4.82438 13.1268L6.49832 11.9875C6.51469 11.9764 6.53278 11.9638 6.55248 11.9502C6.7415 11.8192 7.07919 11.5851 7.47593 11.4903C7.79844 11.4133 8.13446 11.4124 8.45734 11.4879C8.85455 11.5808 9.19338 11.8131 9.38304 11.9432C9.40281 11.9568 9.42096 11.9692 9.43739 11.9803L11.1789 13.153C11.7501 13.5377 12.1011 13.7724 12.365 13.9165C12.3817 13.9256 12.3974 13.934 12.4119 13.9415C12.4137 13.9252 12.4156 13.9076 12.4173 13.8886C12.4452 13.5892 12.4466 13.167 12.4466 12.4784V4.57424C12.4466 4.02034 12.4455 3.68644 12.4249 3.43809C12.4107 3.26677 12.391 3.20233 12.3857 3.18821C12.3354 3.09199 12.2568 3.01345 12.1606 2.96308C12.1465 2.95783 12.082 2.93816 11.9107 2.92394C11.6624 2.90331 11.3285 2.90224 10.7746 2.90224ZM2.43025 2.57509C2.22009 2.97967 2.22009 3.51119 2.22009 4.57424V12.4544C2.22009 13.7844 2.22009 14.4495 2.50121 14.8107C2.73556 15.1118 3.08767 15.2981 3.46845 15.3224C3.92523 15.3516 4.47501 14.9774 5.57457 14.229L7.24851 13.0898C7.51151 12.9108 7.64301 12.8213 7.78582 12.7871C7.90675 12.7582 8.03276 12.7579 8.15384 12.7862C8.29681 12.8197 8.42875 12.9085 8.69263 13.0862L10.4342 14.259C11.5318 14.9981 12.0805 15.3677 12.5361 15.337C12.9159 15.3115 13.2667 15.1248 13.5 14.824C13.7799 14.4633 13.7799 13.8017 13.7799 12.4784V4.57424C13.7799 3.51119 13.7799 2.97967 13.5697 2.57509C13.3926 2.23416 13.1147 1.95617 12.7737 1.77907C12.3691 1.56891 11.8376 1.56891 10.7746 1.56891H5.22543C4.16238 1.56891 3.63085 1.56891 3.22627 1.77907C2.88534 1.95617 2.60736 2.23416 2.43025 2.57509Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -24,6 +24,7 @@
"contains": "Contains",
"content_to_retrieve": "Content to Retrieve",
"content_to_search": "Content to Search",
"context_menu.add_comment": "Add comment",
"create_link_error": "Error creating link",
"custom_feedback": "Custom Feedback",
"custom_input": "Custom Input",
@@ -38,6 +39,7 @@
"edit_input": "Edit Input",
"edit_output": "Edit output",
"end_with": "Ends With",
"enter_comment": "Enter comment",
"error_info_returns_empty_on_success": "Error information of code execution, returns empty on success",
"execute_a_simple_script_code_usually_for_complex_data_processing": "Execute a simple script code, usually for complex data processing.",
"execute_different_branches_based_on_conditions": "Execute different branches based on conditions.",

View File

@@ -24,6 +24,8 @@
"contains": "包含",
"content_to_retrieve": "需要检索的内容",
"content_to_search": "需要检索的内容",
"contextMenu.addComment": "添加注释",
"context_menu.add_comment": "添加注释",
"create_link_error": "创建链接异常",
"custom_feedback": "自定义反馈",
"custom_input": "自定义输入",
@@ -38,6 +40,7 @@
"edit_input": "编辑输入",
"edit_output": "编辑输出",
"end_with": "结束为",
"enter_comment": "输入注释",
"error_info_returns_empty_on_success": "代码运行错误信息,成功时返回空",
"execute_a_simple_script_code_usually_for_complex_data_processing": "执行一段简单的脚本代码,通常用于进行复杂的数据处理。",
"execute_different_branches_based_on_conditions": "根据一定的条件,执行不同的分支。",

View File

@@ -0,0 +1,83 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comment';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { useReactFlow } from 'reactflow';
type ContextMenuProps = {
top: number;
left: number;
};
const ContextMenu = ({ top, left }: ContextMenuProps) => {
const { t } = useTranslation();
const setNodes = useContextSelector(WorkflowContext, (ctx) => ctx.setNodes);
const setMenu = useContextSelector(WorkflowContext, (ctx) => ctx.setMenu);
const { screenToFlowPosition } = useReactFlow();
const newNode = nodeTemplate2FlowNode({
template: CommentNode,
position: screenToFlowPosition({ x: left, y: top }),
t
});
return (
<Box position="relative">
<Box
position="absolute"
top={`${top - 6}px`}
left={`${left + 10}px`}
width={0}
height={0}
borderLeft="6px solid transparent"
borderRight="6px solid transparent"
borderBottom="6px solid white"
zIndex={2}
filter="drop-shadow(0px -1px 2px rgba(0, 0, 0, 0.1))"
/>
<Flex
position={'absolute'}
top={top}
left={left}
bg={'white'}
w={'120px'}
height={9}
p={1}
rounded={'md'}
boxShadow={'0px 2px 4px 0px #A1A7B340'}
className="context-menu"
alignItems={'center'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
setMenu(null);
setNodes((state) => {
const newState = state
.map((node) => ({
...node,
selected: false
}))
// @ts-ignore
.concat(newNode);
return newState;
});
}}
zIndex={1}
>
<MyIcon name="comment" w={'16px'} h={'16px'} ml={1} />
<Box fontSize={'12px'} fontWeight={'500'} ml={1.5}>
{t('workflow:context_menu.add_comment')}
</Box>
</Flex>
</Box>
);
};
export default React.memo(ContextMenu);

View File

@@ -282,7 +282,8 @@ export const useWorkflow = () => {
setEdges,
onChangeNode,
onEdgesChange,
setHoverEdgeId
setHoverEdgeId,
setMenu
} = useContextSelector(WorkflowContext, (v) => v);
const { getIntersectingNodes } = useReactFlow();
@@ -599,7 +600,7 @@ export const useWorkflow = () => {
connect
});
},
[onConnect, t, toast, nodes]
[onConnect, t, toast]
);
/* edge */
@@ -613,6 +614,24 @@ export const useWorkflow = () => {
setHoverEdgeId(undefined);
}, [setHoverEdgeId]);
// context menu
const onPaneContextMenu = useCallback(
(e: any) => {
// Prevent native context menu from showing
e.preventDefault();
setMenu({
top: e.clientY - 64,
left: e.clientX - 12
});
},
[setMenu]
);
const onPaneClick = useCallback(() => {
setMenu(null);
}, [setMenu]);
return {
handleNodesChange,
handleEdgeChange,
@@ -624,7 +643,9 @@ export const useWorkflow = () => {
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical,
onNodeDragStop
onNodeDragStop,
onPaneContextMenu,
onPaneClick
};
};

View File

@@ -17,6 +17,7 @@ import { WorkflowContext } from '../context';
import { useWorkflow } from './hooks/useWorkflow';
import HelperLines from './components/HelperLines';
import FlowController from './components/FlowController';
import ContextMenu from './components/ContextMenu';
export const minZoom = 0.1;
export const maxZoom = 1.5;
@@ -57,14 +58,15 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
[FlowNodeTypeEnum.loop]: dynamic(() => import('./nodes/Loop/NodeLoop')),
[FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/Loop/NodeLoopStart')),
[FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd')),
[FlowNodeTypeEnum.formInput]: dynamic(() => import('./nodes/NodeFormInput'))
[FlowNodeTypeEnum.formInput]: dynamic(() => import('./nodes/NodeFormInput')),
[FlowNodeTypeEnum.comment]: dynamic(() => import('./nodes/NodeComment'))
};
const edgeTypes = {
[EDGE_TYPE]: ButtonEdge
};
const Workflow = () => {
const { nodes, edges, reactFlowWrapper, workflowControlMode } = useContextSelector(
const { nodes, edges, menu, reactFlowWrapper, workflowControlMode } = useContextSelector(
WorkflowContext,
(v) => v
);
@@ -79,7 +81,9 @@ const Workflow = () => {
onEdgeMouseLeave,
helperLineHorizontal,
helperLineVertical,
onNodeDragStop
onNodeDragStop,
onPaneContextMenu,
onPaneClick
} = useWorkflow();
const {
@@ -121,6 +125,7 @@ const Workflow = () => {
<NodeTemplatesModal isOpen={isOpenTemplate} onClose={onCloseTemplate} />
</>
{menu && <ContextMenu {...menu} />}
<ReactFlow
ref={reactFlowWrapper}
fitView
@@ -142,6 +147,8 @@ const Workflow = () => {
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseLeave={onEdgeMouseLeave}
panOnScrollSpeed={2}
onPaneContextMenu={onPaneContextMenu}
onPaneClick={onPaneClick}
{...(workflowControlMode === 'select'
? {
selectionMode: SelectionMode.Full,

View File

@@ -0,0 +1,128 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { Box, Textarea } from '@chakra-ui/react';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
const NodeComment = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs } = data;
const { commentText, commentSize } = useMemo(
() => ({
commentText: inputs.find((item) => item.key === NodeInputKeyEnum.commentText),
commentSize: inputs.find((item) => item.key === NodeInputKeyEnum.commentSize)
}),
[inputs]
);
const onChangeNode = useContextSelector(WorkflowContext, (ctx) => ctx.onChangeNode);
const { t } = useTranslation();
const [size, setSize] = useState<{
width: number;
height: number;
}>(commentSize?.value);
const initialY = useRef(0);
const initialX = useRef(0);
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
initialY.current = e.clientY;
initialX.current = e.clientX;
const handleMouseMove = (e: MouseEvent) => {
const deltaY = e.clientY - initialY.current;
const deltaX = e.clientX - initialX.current;
setSize((prevSize) => ({
width: prevSize.width + deltaX < 240 ? 240 : prevSize.width + deltaX,
height: prevSize.height + deltaY < 140 ? 140 : prevSize.height + deltaY
}));
initialY.current = e.clientY;
initialX.current = e.clientX;
commentSize &&
onChangeNode({
nodeId: nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.commentSize,
value: {
...commentSize,
value: {
width: size.width + deltaX,
height: size.height + deltaY
}
}
});
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[commentSize, nodeId, onChangeNode, size.height, size.width]
);
return (
<NodeCard
selected={false}
{...data}
minW={`${size.width}px`}
minH={`${size.height}px`}
menuForbid={{
debug: true
}}
border={'none'}
rounded={'none'}
bg={'#D8E9FF'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)'
}
>
<Box w={'full'} h={'full'} position={'relative'}>
<Box
position={'absolute'}
right={'0'}
bottom={'-2'}
zIndex={9}
cursor={'nwse-resize'}
px={'2px'}
className="nodrag"
onMouseDown={handleMouseDown}
>
<MyIcon name={'common/editor/resizer'} width={'14px'} height={'14px'} />
</Box>
<Textarea
value={commentText?.value}
border={'none'}
rounded={'none'}
minH={`${size.height}px`}
minW={`${size.width}px`}
resize={'none'}
placeholder={t('workflow:enter_comment')}
onChange={(e) => {
commentText &&
onChangeNode({
nodeId: nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.commentText,
value: {
...commentText,
value: e.target.value
}
});
}}
/>
</Box>
</NodeCard>
);
};
export default React.memo(NodeComment);

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { Box, Button, Card, Flex, Image } from '@chakra-ui/react';
import { Box, Button, Card, Flex, FlexProps, Image } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
@@ -39,7 +39,7 @@ type Props = FlowNodeItemType & {
copy?: boolean;
delete?: boolean;
};
};
} & Omit<FlexProps, 'children'>;
const NodeCard = (props: Props) => {
const { t } = useTranslation();
@@ -62,7 +62,8 @@ const NodeCard = (props: Props) => {
isTool = false,
isError = false,
debugResult,
isFolded
isFolded,
...customStyle
} = props;
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
@@ -156,120 +157,136 @@ const NodeCard = (props: Props) => {
/* Node header */
const Header = useMemo(() => {
const showHeader = node?.flowNodeType !== FlowNodeTypeEnum.comment;
return (
<Box position={'relative'}>
{/* debug */}
<Box px={4} py={3}>
{/* tool target handle */}
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{showHeader && (
<Box px={4} py={3}>
{/* tool target handle */}
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{/* avatar and name */}
<Flex alignItems={'center'}>
{node?.flowNodeType !== FlowNodeTypeEnum.stopTool && (
<Box
mr={2}
cursor={'pointer'}
rounded={'sm'}
_hover={{ bg: 'myGray.200' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'attr',
key: 'isFolded',
value: !isFolded
});
}}
>
<MyIcon
name={!isFolded ? 'core/chat/chevronDown' : 'core/chat/chevronRight'}
w={'24px'}
h={'24px'}
color={'myGray.500'}
/>
</Box>
)}
<Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'30px'} h={'30px'} />
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
{t(name as any)}
</Box>
<MyIcon
className="controller-rename"
display={'none'}
name={'edit'}
w={'14px'}
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app:modules.Title is required'),
status: 'warning'
});
}
{/* avatar and name */}
<Flex alignItems={'center'}>
{node?.flowNodeType !== FlowNodeTypeEnum.stopTool && (
<Box
mr={2}
cursor={'pointer'}
rounded={'sm'}
_hover={{ bg: 'myGray.200' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
key: 'isFolded',
value: !isFolded
});
}
});
}}
/>
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={t('app:app.modules.click to update')}>
<Button
bg={'yellow.50'}
color={'yellow.600'}
variant={'ghost'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
cursor={'pointer'}
_hover={{ bg: 'yellow.100' }}
onClick={onOpenConfirmSync(onClickSyncVersion)}
}}
>
<Box>{t('app:app.modules.has new version')}</Box>
<QuestionOutlineIcon ml={1} />
</Button>
</MyTooltip>
)}
{!!nodeTemplate?.diagram && !hasNewVersion && (
<MyTooltip
label={
<Image src={nodeTemplate?.diagram} w={'100%'} minH={['auto', '200px']} alt={''} />
}
>
<Box
fontSize={'sm'}
color={'primary.700'}
p={1}
rounded={'sm'}
cursor={'default'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
>
{t('common:core.module.Diagram')}
<MyIcon
name={!isFolded ? 'core/chat/chevronDown' : 'core/chat/chevronRight'}
w={'24px'}
h={'24px'}
color={'myGray.500'}
/>
</Box>
</MyTooltip>
)}
</Flex>
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
)}
<Avatar
src={avatar}
borderRadius={'sm'}
objectFit={'contain'}
w={'30px'}
h={'30px'}
/>
<Box ml={3} fontSize={'md'} fontWeight={'medium'}>
{t(name as any)}
</Box>
<MyIcon
className="controller-rename"
display={'none'}
name={'edit'}
w={'14px'}
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
onSuccess: (e) => {
if (!e) {
return toast({
title: t('app:modules.Title is required'),
status: 'warning'
});
}
onChangeNode({
nodeId,
type: 'attr',
key: 'name',
value: e
});
}
});
}}
/>
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={t('app:app.modules.click to update')}>
<Button
bg={'yellow.50'}
color={'yellow.600'}
variant={'ghost'}
h={8}
px={2}
rounded={'6px'}
fontSize={'xs'}
fontWeight={'medium'}
cursor={'pointer'}
_hover={{ bg: 'yellow.100' }}
onClick={onOpenConfirmSync(onClickSyncVersion)}
>
<Box>{t('app:app.modules.has new version')}</Box>
<QuestionOutlineIcon ml={1} />
</Button>
</MyTooltip>
)}
{!!nodeTemplate?.diagram && !hasNewVersion && (
<MyTooltip
label={
<Image
src={nodeTemplate?.diagram}
w={'100%'}
minH={['auto', '200px']}
alt={''}
/>
}
>
<Box
fontSize={'sm'}
color={'primary.700'}
p={1}
rounded={'sm'}
cursor={'default'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
>
{t('common:core.module.Diagram')}
</Box>
</MyTooltip>
)}
</Flex>
<NodeIntro nodeId={nodeId} intro={intro} />
</Box>
)}
<MenuRender nodeId={nodeId} menuForbid={menuForbid} nodeList={nodeList} />
<ConfirmSyncModal />
</Box>
);
}, [
node?.flowNodeType,
showToolHandle,
nodeId,
node?.flowNodeType,
isFolded,
avatar,
t,
@@ -278,9 +295,10 @@ const NodeCard = (props: Props) => {
onOpenConfirmSync,
onClickSyncVersion,
nodeTemplate?.diagram,
intro,
menuForbid,
nodeList,
intro,
ConfirmSyncModal,
onChangeNode,
onOpenCustomTitleModal,
toast
@@ -334,6 +352,7 @@ const NodeCard = (props: Props) => {
: {
borderColor: selected ? 'primary.600' : 'borderColor.base'
})}
{...customStyle}
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}

View File

@@ -174,6 +174,11 @@ type WorkflowContextType = {
//
workflowControlMode?: 'drag' | 'select';
setWorkflowControlMode: (value?: SetState<'drag' | 'select'> | undefined) => void;
menu: {
top: number;
left: number;
} | null;
setMenu: (value: React.SetStateAction<{ top: number; left: number } | null>) => void;
};
type DebugDataType = {
@@ -313,6 +318,10 @@ export const WorkflowContext = createContext<WorkflowContextType>({
},
onSwitchCloudVersion: function (appVersion: AppVersionSchemaType): boolean {
throw new Error('Function not implemented.');
},
menu: null,
setMenu: function (value: React.SetStateAction<{ top: number; left: number } | null>): void {
throw new Error('Function not implemented.');
}
});
@@ -973,6 +982,8 @@ const WorkflowContextProvider = ({
};
}, [edges, nodes]);
const [menu, setMenu] = useState<{ top: number; left: number } | null>(null);
const value = {
appId,
reactFlowWrapper,
@@ -1032,7 +1043,10 @@ const WorkflowContextProvider = ({
setShowHistoryModal,
// chat test
setWorkflowTestData
setWorkflowTestData,
menu,
setMenu
};
return (