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))
|
||||
}
|
||||
};
|
||||
}
|
||||
212
projects/app/src/pages/plugin/list/component/EditModal.tsx
Normal file
212
projects/app/src/pages/plugin/list/component/EditModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Input,
|
||||
Textarea,
|
||||
IconButton
|
||||
} from '@chakra-ui/react';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { compressImg } from '@/web/common/file/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useToast } from '@/web/common/hooks/useToast';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRequest } from '@/web/common/hooks/useRequest';
|
||||
import { delOnePlugin, postCreatePlugin, putUpdatePlugin } from '@/web/core/plugin/api';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfirm } from '@/web/common/hooks/useConfirm';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
export type FormType = {
|
||||
id?: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
intro: string;
|
||||
};
|
||||
export const defaultForm = {
|
||||
avatar: '/icon/logo.svg',
|
||||
name: '',
|
||||
intro: ''
|
||||
};
|
||||
|
||||
const CreateModal = ({
|
||||
defaultValue = defaultForm,
|
||||
onClose,
|
||||
onSuccess,
|
||||
onDelete
|
||||
}: {
|
||||
defaultValue?: FormType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
onDelete: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystemStore();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
title: t('common.Delete Tip'),
|
||||
content: t('plugin.Confirm Delete')
|
||||
});
|
||||
|
||||
const { register, setValue, getValues, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: defaultValue
|
||||
});
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png,.svg',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const src = await compressImg({
|
||||
file,
|
||||
maxW: 100,
|
||||
maxH: 100
|
||||
});
|
||||
setValue('avatar', src);
|
||||
setRefresh((state) => !state);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, t('common.Select File Failed')),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, t, toast]
|
||||
);
|
||||
|
||||
const { mutate: onclickCreate, isLoading: creating } = useRequest({
|
||||
mutationFn: async (data: FormType) => {
|
||||
return postCreatePlugin(data);
|
||||
},
|
||||
onSuccess(id: string) {
|
||||
router.push(`/plugin/edit?pluginId=${id}`);
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common.Create Success'),
|
||||
errorToast: t('common.Create Failed')
|
||||
});
|
||||
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
|
||||
mutationFn: async (data: FormType) => {
|
||||
if (!data.id) return Promise.resolve('');
|
||||
// @ts-ignore
|
||||
return putUpdatePlugin(data);
|
||||
},
|
||||
onSuccess() {
|
||||
onSuccess();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common.Update Success'),
|
||||
errorToast: t('common.Update Failed')
|
||||
});
|
||||
|
||||
const onclickDelApp = useCallback(async () => {
|
||||
if (!defaultValue.id) return;
|
||||
try {
|
||||
await delOnePlugin(defaultValue.id);
|
||||
toast({
|
||||
title: t('common.Delete Success'),
|
||||
status: 'success'
|
||||
});
|
||||
onDelete();
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, t('common.Delete Failed')),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}, [defaultValue.id, onClose, toast, t, onDelete]);
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} isCentered={!isPc}>
|
||||
<ModalHeader fontSize={'2xl'}>
|
||||
{defaultValue.id ? t('plugin.Update Your Plugin') : t('plugin.Create Your Plugin')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.800'} fontWeight={'bold'}>
|
||||
{t('plugin.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={3} alignItems={'center'}>
|
||||
<MyTooltip label={t('common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={getValues('avatar')}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
autoFocus={!defaultValue.id}
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t("common.Name Can't Be Empty")
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Box mt={3}>
|
||||
<Box mb={1}>{t('plugin.Intro')}</Box>
|
||||
<Textarea {...register('intro')} bg={'myWhite.600'} rows={5} />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<Flex px={5} py={4}>
|
||||
{!!defaultValue.id && (
|
||||
<IconButton
|
||||
className="delete"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
_hover={{
|
||||
bg: 'red.100'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfirm(onclickDelApp)();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
<Button variant={'base'} mr={3} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
{!!defaultValue.id ? (
|
||||
<Button isLoading={updating} onClick={handleSubmit((data) => onclickUpdate(data))}>
|
||||
{t('common.Confirm Update')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button isLoading={creating} onClick={handleSubmit((data) => onclickCreate(data))}>
|
||||
{t('common.Confirm Create')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
<ConfirmModal />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateModal;
|
||||
137
projects/app/src/pages/plugin/list/index.tsx
Normal file
137
projects/app/src/pages/plugin/list/index.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Grid, Card, useTheme, Flex, IconButton, Button, Image } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import EditModal, { defaultForm, FormType } from './component/EditModal';
|
||||
import { getUserPlugins } from '@/web/core/plugin/api';
|
||||
import EmptyTip from '@/components/EmptyTip';
|
||||
|
||||
const MyModules = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const [editModalData, setEditModalData] = useState<FormType>();
|
||||
|
||||
/* load plugins */
|
||||
const {
|
||||
data = [],
|
||||
isLoading,
|
||||
refetch
|
||||
} = useQuery(['loadModules'], () => getUserPlugins(), {
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer isLoading={isLoading}>
|
||||
<Flex pt={3} px={5} alignItems={'center'}>
|
||||
<Flex flex={1} alignItems={'center'}>
|
||||
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
|
||||
<Box className="textlg" letterSpacing={1} fontSize={['20px', '24px']} fontWeight={'bold'}>
|
||||
{t('plugin.My Plugins')}({t('common.Beta')})
|
||||
</Box>
|
||||
</Flex>
|
||||
<Button
|
||||
leftIcon={<AddIcon />}
|
||||
variant={'base'}
|
||||
onClick={() => setEditModalData(defaultForm)}
|
||||
>
|
||||
{t('common.New Create')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Grid
|
||||
p={5}
|
||||
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
|
||||
gridGap={5}
|
||||
>
|
||||
{data.map((plugin) => (
|
||||
<Card
|
||||
key={plugin._id}
|
||||
py={4}
|
||||
px={5}
|
||||
cursor={'pointer'}
|
||||
h={'140px'}
|
||||
border={theme.borders.md}
|
||||
boxShadow={'none'}
|
||||
userSelect={'none'}
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
|
||||
borderColor: 'transparent',
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .chat': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
onClick={() => router.push(`/plugin/edit?pluginId=${plugin._id}`)}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={plugin.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3}>{plugin.name}</Box>
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'edit'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
display={['', 'none']}
|
||||
_hover={{
|
||||
bg: 'myBlue.200'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditModalData({
|
||||
id: plugin._id,
|
||||
name: plugin.name,
|
||||
avatar: plugin.avatar,
|
||||
intro: plugin.intro
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
className={'textEllipsis3'}
|
||||
py={2}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{plugin.intro || t('plugin.No Intro')}
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
{data.length === 0 && <EmptyTip />}
|
||||
{!!editModalData && (
|
||||
<EditModal
|
||||
defaultValue={editModalData}
|
||||
onClose={() => setEditModalData(undefined)}
|
||||
onSuccess={refetch}
|
||||
onDelete={refetch}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MyModules;
|
||||
Reference in New Issue
Block a user