mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-20 02:34:52 +00:00
feat: copy module
This commit is contained in:
@@ -24,6 +24,10 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"Add": "Add",
|
"Add": "Add",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Collect": "Collect",
|
||||||
|
"Copy": "Copy",
|
||||||
|
"Delete": "Delete",
|
||||||
"Filed is repeat": "Filed is repeated",
|
"Filed is repeat": "Filed is repeated",
|
||||||
"Filed is repeated": "",
|
"Filed is repeated": "",
|
||||||
"Input": "Input",
|
"Input": "Input",
|
||||||
|
@@ -24,6 +24,10 @@
|
|||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"Add": "添加",
|
"Add": "添加",
|
||||||
|
"Cancel": "取消",
|
||||||
|
"Collect": "收藏",
|
||||||
|
"Copy": "复制",
|
||||||
|
"Delete": "删除",
|
||||||
"Filed is repeat": "",
|
"Filed is repeat": "",
|
||||||
"Filed is repeated": "字段重复了",
|
"Filed is repeated": "字段重复了",
|
||||||
"Input": "输入",
|
"Input": "输入",
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
import { Box, Flex, useTheme, Menu, MenuButton, MenuList, MenuItem } from '@chakra-ui/react';
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import type { FlowModuleItemType } from '@/types/flow';
|
import type { FlowModuleItemType } from '@/types/flow';
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: React.ReactNode | React.ReactNode[] | string;
|
children?: React.ReactNode | React.ReactNode[] | string;
|
||||||
@@ -15,6 +16,7 @@ type Props = {
|
|||||||
minW?: string | number;
|
minW?: string | number;
|
||||||
moduleId: string;
|
moduleId: string;
|
||||||
onDelNode: FlowModuleItemType['onDelNode'];
|
onDelNode: FlowModuleItemType['onDelNode'];
|
||||||
|
onCopyNode: FlowModuleItemType['onCopyNode'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeCard = ({
|
const NodeCard = ({
|
||||||
@@ -23,11 +25,39 @@ const NodeCard = ({
|
|||||||
name = '未知模块',
|
name = '未知模块',
|
||||||
description,
|
description,
|
||||||
minW = '300px',
|
minW = '300px',
|
||||||
|
onCopyNode,
|
||||||
onDelNode,
|
onDelNode,
|
||||||
moduleId
|
moduleId
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const menuList = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
icon: 'copy',
|
||||||
|
label: t('common.Copy'),
|
||||||
|
onClick: () => onCopyNode(moduleId)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'delete',
|
||||||
|
label: t('common.Delete'),
|
||||||
|
onClick: () => onDelNode(moduleId)
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// icon: 'collectionLight',
|
||||||
|
// label: t('common.Collect'),
|
||||||
|
// onClick: () => {}
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
icon: 'back',
|
||||||
|
label: t('common.Cancel'),
|
||||||
|
onClick: () => {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[moduleId, onCopyNode, onDelNode, t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
minW={minW}
|
minW={minW}
|
||||||
@@ -52,19 +82,27 @@ const NodeCard = ({
|
|||||||
</MyTooltip>
|
</MyTooltip>
|
||||||
)}
|
)}
|
||||||
<Box flex={1} />
|
<Box flex={1} />
|
||||||
<Flex
|
<Menu autoSelect={false} isLazy>
|
||||||
className={'nodrag'}
|
<MenuButton
|
||||||
w={'22px'}
|
className={'nodrag'}
|
||||||
h={'22px'}
|
_hover={{ bg: 'myWhite.600' }}
|
||||||
alignItems={'center'}
|
cursor={'pointer'}
|
||||||
justifyContent={'center'}
|
borderRadius={'md'}
|
||||||
color={'myGray.600'}
|
onClick={(e) => {
|
||||||
_hover={{ color: 'red.600' }}
|
e.stopPropagation();
|
||||||
cursor={'pointer'}
|
}}
|
||||||
onClick={() => onDelNode(moduleId)}
|
>
|
||||||
>
|
<MyIcon name={'more'} w={'14px'} p={2} />
|
||||||
<MyIcon name="delete" w={'16px'} />
|
</MenuButton>
|
||||||
</Flex>
|
<MenuList color={'myGray.700'} minW={`120px !important`} zIndex={10}>
|
||||||
|
{menuList.map((item) => (
|
||||||
|
<MenuItem key={item.label} onClick={item.onClick} py={[2, 3]}>
|
||||||
|
<MyIcon name={item.icon as any} w={['14px', '16px']} />
|
||||||
|
<Box ml={[1, 2]}>{item.label}</Box>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
</Flex>
|
</Flex>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
@@ -10,7 +10,7 @@ import ReactFlow, {
|
|||||||
Connection,
|
Connection,
|
||||||
useViewport
|
useViewport
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
import { Box, Flex, IconButton, useTheme, useDisclosure, position } from '@chakra-ui/react';
|
||||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||||
import {
|
import {
|
||||||
edgeOptions,
|
edgeOptions,
|
||||||
@@ -126,6 +126,32 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onAddNode = useCallback(
|
||||||
|
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
|
||||||
|
if (!reactFlowWrapper.current) return;
|
||||||
|
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||||
|
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||||
|
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||||
|
|
||||||
|
setNodes((state) =>
|
||||||
|
state.concat(
|
||||||
|
appModule2FlowNode({
|
||||||
|
item: {
|
||||||
|
...template,
|
||||||
|
moduleId: nanoid(),
|
||||||
|
position: { x: mouseX, y: mouseY }
|
||||||
|
},
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode,
|
||||||
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[x, zoom, y]
|
||||||
|
);
|
||||||
const onDelNode = useCallback(
|
const onDelNode = useCallback(
|
||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
setNodes((state) => state.filter((item) => item.id !== nodeId));
|
||||||
@@ -155,6 +181,45 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
|||||||
},
|
},
|
||||||
[setEdges]
|
[setEdges]
|
||||||
);
|
);
|
||||||
|
const onCopyNode = useCallback(
|
||||||
|
(nodeId: string) => {
|
||||||
|
setNodes((nodes) => {
|
||||||
|
const node = nodes.find((node) => node.id === nodeId);
|
||||||
|
if (!node) return nodes;
|
||||||
|
const template = {
|
||||||
|
logo: node.data.logo,
|
||||||
|
name: node.data.name,
|
||||||
|
intro: node.data.intro,
|
||||||
|
description: node.data.description,
|
||||||
|
flowType: node.data.flowType,
|
||||||
|
inputs: node.data.inputs,
|
||||||
|
outputs: node.data.outputs,
|
||||||
|
showStatus: node.data.showStatus
|
||||||
|
};
|
||||||
|
return nodes.concat(
|
||||||
|
appModule2FlowNode({
|
||||||
|
item: {
|
||||||
|
...template,
|
||||||
|
moduleId: nanoid(),
|
||||||
|
position: { x: node.position.x + 200, y: node.position.y + 50 }
|
||||||
|
},
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode,
|
||||||
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setNodes]
|
||||||
|
);
|
||||||
|
const onCollectionNode = useCallback(
|
||||||
|
(nodeId: string) => {
|
||||||
|
console.log(nodes.find((node) => node.id === nodeId));
|
||||||
|
},
|
||||||
|
[nodes]
|
||||||
|
);
|
||||||
|
|
||||||
const flow2AppModules = useCallback(() => {
|
const flow2AppModules = useCallback(() => {
|
||||||
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
||||||
@@ -264,39 +329,12 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setNodes]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAddNode = useCallback(
|
const onDelConnect = useCallback((id: string) => {
|
||||||
({ template, position }: { template: FlowModuleTemplateType; position: XYPosition }) => {
|
setEdges((state) => state.filter((item) => item.id !== id));
|
||||||
if (!reactFlowWrapper.current) return;
|
}, []);
|
||||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
|
||||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
|
||||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
|
||||||
|
|
||||||
setNodes((state) =>
|
|
||||||
state.concat(
|
|
||||||
appModule2FlowNode({
|
|
||||||
item: {
|
|
||||||
...template,
|
|
||||||
moduleId: nanoid(),
|
|
||||||
position: { x: mouseX, y: mouseY }
|
|
||||||
},
|
|
||||||
onChangeNode,
|
|
||||||
onDelNode,
|
|
||||||
onDelEdge
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onDelEdge, onChangeNode, onDelNode, setNodes, x, y, zoom]
|
|
||||||
);
|
|
||||||
const onDelConnect = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
setEdges((state) => state.filter((item) => item.id !== id));
|
|
||||||
},
|
|
||||||
[setEdges]
|
|
||||||
);
|
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
({ connect }: { connect: Connection }) => {
|
({ connect }: { connect: Connection }) => {
|
||||||
const source = nodes.find((node) => node.id === connect.source)?.data;
|
const source = nodes.find((node) => node.id === connect.source)?.data;
|
||||||
@@ -342,7 +380,7 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[onDelConnect, setEdges, nodes]
|
[nodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutate: onclickSave, isLoading } = useRequest({
|
const { mutate: onclickSave, isLoading } = useRequest({
|
||||||
@@ -373,19 +411,31 @@ const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => {
|
|||||||
item,
|
item,
|
||||||
onChangeNode,
|
onChangeNode,
|
||||||
onDelNode,
|
onDelNode,
|
||||||
onDelEdge
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
onFixView();
|
onFixView();
|
||||||
},
|
},
|
||||||
[onDelConnect, setEdges, setNodes, onFixView, onChangeNode, onDelNode, onDelEdge]
|
[
|
||||||
|
onDelConnect,
|
||||||
|
setEdges,
|
||||||
|
setNodes,
|
||||||
|
onFixView,
|
||||||
|
onChangeNode,
|
||||||
|
onDelNode,
|
||||||
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initData(JSON.parse(JSON.stringify(app)));
|
initData(JSON.parse(JSON.stringify(app)));
|
||||||
}, [app, initData]);
|
}, [app]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
2
client/src/types/flow.d.ts
vendored
2
client/src/types/flow.d.ts
vendored
@@ -61,6 +61,8 @@ export type FlowModuleItemType = FlowModuleTemplateType & {
|
|||||||
moduleId: string;
|
moduleId: string;
|
||||||
onChangeNode: (e: FlowModuleItemChangeProps) => void;
|
onChangeNode: (e: FlowModuleItemChangeProps) => void;
|
||||||
onDelNode: (id: string) => void;
|
onDelNode: (id: string) => void;
|
||||||
|
onCopyNode: (id: string) => void;
|
||||||
|
onCollectionNode: (id: string) => void;
|
||||||
onDelEdge: ({
|
onDelEdge: ({
|
||||||
moduleId,
|
moduleId,
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
|
@@ -64,12 +64,16 @@ export const appModule2FlowNode = ({
|
|||||||
item,
|
item,
|
||||||
onChangeNode,
|
onChangeNode,
|
||||||
onDelNode,
|
onDelNode,
|
||||||
onDelEdge
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
}: {
|
}: {
|
||||||
item: AppModuleItemType;
|
item: AppModuleItemType;
|
||||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||||
onDelNode: FlowModuleItemType['onDelNode'];
|
onDelNode: FlowModuleItemType['onDelNode'];
|
||||||
onDelEdge: FlowModuleItemType['onDelEdge'];
|
onDelEdge: FlowModuleItemType['onDelEdge'];
|
||||||
|
onCopyNode: FlowModuleItemType['onCopyNode'];
|
||||||
|
onCollectionNode: FlowModuleItemType['onCollectionNode'];
|
||||||
}): Node<FlowModuleItemType> => {
|
}): Node<FlowModuleItemType> => {
|
||||||
// init some static data
|
// init some static data
|
||||||
const template =
|
const template =
|
||||||
@@ -109,7 +113,9 @@ export const appModule2FlowNode = ({
|
|||||||
}),
|
}),
|
||||||
onChangeNode,
|
onChangeNode,
|
||||||
onDelNode,
|
onDelNode,
|
||||||
onDelEdge
|
onDelEdge,
|
||||||
|
onCopyNode,
|
||||||
|
onCollectionNode
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Reference in New Issue
Block a user