v4.5.2 (#439)
This commit is contained in:
161
projects/app/src/pages/plugin/edit/Header.tsx
Normal file
161
projects/app/src/pages/plugin/edit/Header.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
|
||||
import { useRequest } from '@/web/common/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { flowNode2Modules, useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
|
||||
import { putUpdatePlugin } from '@/web/core/plugin/api';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { ModuleItemType } from '@fastgpt/global/core/module/type';
|
||||
|
||||
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
|
||||
const PreviewPlugin = dynamic(() => import('./Preview'));
|
||||
|
||||
type Props = { plugin: PluginItemSchema; onClose: () => void };
|
||||
|
||||
const Header = ({ plugin, onClose }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
const { nodes, edges, onFixView } = useFlowProviderStore();
|
||||
const [previewModules, setPreviewModules] = React.useState<ModuleItemType[]>();
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
const modules = flowNode2Modules({ nodes, edges });
|
||||
|
||||
// check required connect
|
||||
for (let i = 0; i < modules.length; i++) {
|
||||
const item = modules[i];
|
||||
|
||||
// update custom input connected
|
||||
if (item.flowType === FlowNodeTypeEnum.pluginInput) {
|
||||
item.inputs.forEach((item) => {
|
||||
item.connected = true;
|
||||
});
|
||||
if (item.outputs.find((output) => output.targets.length === 0)) {
|
||||
return Promise.reject(t('module.Plugin input must connect'));
|
||||
}
|
||||
}
|
||||
|
||||
if (item.inputs.find((input) => input.required && !input.connected)) {
|
||||
return Promise.reject(`【${item.name}】存在未连接的必填输入`);
|
||||
}
|
||||
if (item.inputs.find((input) => input.valueCheck && !input.valueCheck(input.value))) {
|
||||
return Promise.reject(`【${item.name}】存在为填写的必填项`);
|
||||
}
|
||||
}
|
||||
|
||||
// plugin must have input
|
||||
const pluginInputModule = modules.find(
|
||||
(item) => item.flowType === FlowNodeTypeEnum.pluginInput
|
||||
);
|
||||
|
||||
if (!pluginInputModule) {
|
||||
return Promise.reject(t('module.Plugin input is required'));
|
||||
}
|
||||
if (pluginInputModule.inputs.length < 1) {
|
||||
return Promise.reject(t('module.Plugin input is not value'));
|
||||
}
|
||||
|
||||
return putUpdatePlugin({
|
||||
id: plugin._id,
|
||||
modules
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
errorToast: '保存配置异常'
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
py={3}
|
||||
px={[2, 5, 8]}
|
||||
borderBottom={theme.borders.base}
|
||||
alignItems={'center'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<MyTooltip label={'返回'} offset={[10, 10]}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'back'} w={'14px'} />}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.300'}
|
||||
variant={'base'}
|
||||
aria-label={''}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onFixView();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Box ml={[3, 6]} fontSize={['md', '2xl']} flex={1}>
|
||||
{plugin.name}
|
||||
</Box>
|
||||
|
||||
<MyTooltip label={t('app.Import Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'importLight'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={onOpenImport}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('app.Export Configs')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'export'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
variant={'base'}
|
||||
aria-label={'save'}
|
||||
onClick={() =>
|
||||
copyData(
|
||||
JSON.stringify(flowNode2Modules({ nodes, edges }), null, 2),
|
||||
t('app.Export Config Successful')
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('module.Preview Plugin')}>
|
||||
<IconButton
|
||||
mr={[3, 6]}
|
||||
icon={<MyIcon name={'core/module/previewLight'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
setPreviewModules(flowNode2Modules({ nodes, edges }));
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('module.Save Config')}>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={['14px', '16px']} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
{isOpenImport && <ImportSettings onClose={onCloseImport} />}
|
||||
{!!previewModules && (
|
||||
<PreviewPlugin
|
||||
plugin={plugin}
|
||||
modules={previewModules}
|
||||
onClose={() => setPreviewModules(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Header);
|
||||
68
projects/app/src/pages/plugin/edit/Preview.tsx
Normal file
68
projects/app/src/pages/plugin/edit/Preview.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactFlow, { Background, ReactFlowProvider, useNodesState } from 'reactflow';
|
||||
import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/type';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { formatPluginIOModules } from '@fastgpt/global/core/module/utils';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';
|
||||
import { appModule2FlowNode } from '@/utils/adapt';
|
||||
|
||||
const nodeTypes = {
|
||||
[FlowNodeTypeEnum.pluginModule]: dynamic(
|
||||
() => import('@/components/core/module/Flow/components/nodes/NodePreviewPlugin')
|
||||
)
|
||||
};
|
||||
|
||||
const PreviewPlugin = ({
|
||||
plugin,
|
||||
modules,
|
||||
onClose
|
||||
}: {
|
||||
plugin: PluginItemSchema;
|
||||
modules: ModuleItemType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [nodes = [], setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes([
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
moduleId: 'plugin',
|
||||
flowType: FlowNodeTypeEnum.pluginModule,
|
||||
logo: plugin.avatar,
|
||||
name: plugin.name,
|
||||
description: plugin.intro,
|
||||
intro: plugin.intro,
|
||||
...formatPluginIOModules(plugin._id, modules)
|
||||
}
|
||||
})
|
||||
]);
|
||||
}, [modules, plugin, setNodes]);
|
||||
|
||||
return (
|
||||
<MyModal isOpen title={t('module.Preview Plugin')} onClose={onClose} isCentered>
|
||||
<Box h={'400px'} w={'400px'}>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
fitView
|
||||
nodes={nodes}
|
||||
edges={[]}
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
</Box>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PreviewPlugin);
|
||||
96
projects/app/src/pages/plugin/edit/index.tsx
Normal file
96
projects/app/src/pages/plugin/edit/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Header from './Header';
|
||||
import Flow from '@/components/core/module/Flow';
|
||||
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
|
||||
import { SystemModuleTemplateType } from '@fastgpt/global/core/module/type.d';
|
||||
import { PluginModuleTemplates } from '@/constants/flow/ModuleTemplate';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getOnePlugin, getUserPlugs2ModuleTemplates } from '@/web/core/plugin/api';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import Loading from '@/components/Loading';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePluginStore } from '@/web/core/plugin/store/plugin';
|
||||
|
||||
type Props = { pluginId: string };
|
||||
|
||||
const Render = ({ pluginId }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { nodes = [] } = useFlowProviderStore();
|
||||
const { pluginModuleTemplates, loadPluginModuleTemplates } = usePluginStore();
|
||||
|
||||
const filterTemplates = useMemo(() => {
|
||||
const copyTemplates: SystemModuleTemplateType = JSON.parse(
|
||||
JSON.stringify(PluginModuleTemplates)
|
||||
);
|
||||
const filterType: Record<string, 1> = {
|
||||
[FlowNodeTypeEnum.userGuide]: 1,
|
||||
[FlowNodeTypeEnum.pluginInput]: 1,
|
||||
[FlowNodeTypeEnum.pluginOutput]: 1
|
||||
};
|
||||
|
||||
// filter some template
|
||||
nodes.forEach((node) => {
|
||||
if (node.type && filterType[node.type]) {
|
||||
copyTemplates.forEach((item) => {
|
||||
item.list.forEach((module, index) => {
|
||||
if (module.flowType === node.type) {
|
||||
item.list.splice(index, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return copyTemplates;
|
||||
}, [nodes]);
|
||||
|
||||
const { data } = useQuery(['getOnePlugin', pluginId], () => getOnePlugin(pluginId), {
|
||||
onError: (error) => {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: getErrText(error, t('plugin.Load Plugin Failed'))
|
||||
});
|
||||
router.replace('/plugin/list');
|
||||
}
|
||||
});
|
||||
|
||||
useQuery(['getUserPlugs2ModuleTemplates'], () => loadPluginModuleTemplates());
|
||||
const filterPlugins = useMemo(
|
||||
() => pluginModuleTemplates.filter((item) => item.id !== pluginId),
|
||||
[pluginId, pluginModuleTemplates]
|
||||
);
|
||||
|
||||
return data ? (
|
||||
<Flow
|
||||
systemTemplates={filterTemplates}
|
||||
pluginTemplates={[{ label: '', list: filterPlugins }]}
|
||||
modules={data?.modules || []}
|
||||
Header={<Header plugin={data} onClose={() => router.back()} />}
|
||||
/>
|
||||
) : (
|
||||
<Loading />
|
||||
);
|
||||
};
|
||||
|
||||
export default function AdEdit(props: any) {
|
||||
return (
|
||||
<FlowProvider filterAppIds={[]}>
|
||||
<Render {...props} />
|
||||
</FlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
pluginId: context?.query?.pluginId || '',
|
||||
...(await serviceSideProps(context))
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user