mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-18 17:51:24 +00:00
Feat: App folder and permission (#1726)
* app folder * feat: app foldere * fix: run app param error * perf: select app ux * perf: folder rerender * fix: ts * fix: parentId * fix: permission * perf: loading ux * perf: per select ux * perf: clb context * perf: query extension tip * fix: ts * perf: app detail per * perf: default per
This commit is contained in:
@@ -39,6 +39,7 @@ export default function InputGuideBox({
|
||||
);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [text],
|
||||
throttleWait: 300
|
||||
}
|
||||
|
@@ -155,12 +155,13 @@ ${JSON.stringify(questionGuides)}`;
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
_hover={{
|
||||
bg: 'auto',
|
||||
color: 'primary.600'
|
||||
bg: 'auto'
|
||||
}}
|
||||
>
|
||||
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'14px'} mr={2} />
|
||||
<Box mr={1}>{tool.toolName}</Box>
|
||||
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'1rem'} mr={2} />
|
||||
<Box mr={1} fontSize={'sm'}>
|
||||
{tool.toolName}
|
||||
</Box>
|
||||
{isChatting && !tool.response && (
|
||||
<MyIcon name={'common/loading'} w={'14px'} />
|
||||
)}
|
||||
@@ -219,7 +220,14 @@ ${toolResponse}`}
|
||||
<ChatAvatar src={avatar} type={type} />
|
||||
|
||||
{!!chatStatusMap && statusBoxData && isLastChild && (
|
||||
<Flex alignItems={'center'} px={3} py={'1.5px'} borderRadius="md" bg={chatStatusMap.bg}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={3}
|
||||
py={'1.5px'}
|
||||
borderRadius="md"
|
||||
bg={chatStatusMap.bg}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Box
|
||||
className={styles.statusAnimation}
|
||||
bg={chatStatusMap.color}
|
||||
|
116
projects/app/src/components/common/Modal/EditResourceModal.tsx
Normal file
116
projects/app/src/components/common/Modal/EditResourceModal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { ModalFooter, ModalBody, Input, Button, Box, Textarea, HStack } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal/index';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
|
||||
export type EditResourceInfoFormType = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
intro?: string;
|
||||
};
|
||||
|
||||
const EditResourceModal = ({
|
||||
onClose,
|
||||
onEdit,
|
||||
title,
|
||||
...defaultForm
|
||||
}: EditResourceInfoFormType & {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
onEdit: (data: EditResourceInfoFormType) => any;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
|
||||
defaultValues: defaultForm
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
|
||||
const { runAsync: onSave, loading } = useRequest2(
|
||||
(data: EditResourceInfoFormType) => onEdit(data),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const src = await compressImgFileAndUpload({
|
||||
type: MongoImageTypeEnum.appAvatar,
|
||||
file,
|
||||
maxW: 300,
|
||||
maxH: 300
|
||||
});
|
||||
setValue('avatar', src);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: getErrText(err, t('common.error.Select avatar failed')),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[setValue, t, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} iconSrc={avatar} title={title}>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel mb={1}>{t('core.app.Name and avatar')}</FormLabel>
|
||||
<HStack spacing={4}>
|
||||
<MyTooltip label={t('common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
{...register('name', { required: true })}
|
||||
bg={'myGray.50'}
|
||||
autoFocus
|
||||
maxLength={20}
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<FormLabel mb={1}>{t('common.Intro')}</FormLabel>
|
||||
<Textarea {...register('intro')} bg={'myGray.50'} maxLength={200} />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button isLoading={loading} onClick={handleSubmit(onSave)}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<File onSelect={onSelectFile} />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditResourceModal;
|
@@ -1,4 +1,3 @@
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -37,16 +36,19 @@ const ParentPaths = (props: {
|
||||
{concatPaths.map((item, i) => (
|
||||
<Flex key={item.parentId || i} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={['sm', fontSize || 'md']}
|
||||
py={1}
|
||||
px={[1, 2]}
|
||||
fontSize={['sm', fontSize || 'sm']}
|
||||
py={0.5}
|
||||
px={1.5}
|
||||
borderRadius={'md'}
|
||||
{...(i === concatPaths.length - 1
|
||||
? {
|
||||
cursor: 'default'
|
||||
cursor: 'default',
|
||||
color: 'myGray.700',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
color: 'myGray.600',
|
||||
_hover: {
|
||||
bg: 'myGray.100'
|
||||
},
|
||||
@@ -58,7 +60,9 @@ const ParentPaths = (props: {
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
|
||||
<Box mx={1.5} color={'myGray.500'}>
|
||||
/
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
|
184
projects/app/src/components/common/folder/MoveModal.tsx
Normal file
184
projects/app/src/components/common/folder/MoveModal.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
GetResourceFolderListItemResponse,
|
||||
ParentIdType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { useMemoizedFn, useMount } from 'ahooks';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
type FolderItemType = {
|
||||
id: string;
|
||||
name: string;
|
||||
open: boolean;
|
||||
children?: FolderItemType[];
|
||||
};
|
||||
|
||||
const rootId = 'root';
|
||||
|
||||
type Props = {
|
||||
moveResourceId: string;
|
||||
title: string;
|
||||
server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>;
|
||||
onConfirm: (id: ParentIdType) => Promise<any>;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedId, setSelectedId] = React.useState<string>();
|
||||
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
|
||||
const [folderList, setFolderList] = useState<FolderItemType[]>([]);
|
||||
|
||||
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
|
||||
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
|
||||
|
||||
setRequestingIdList((state) => [...state, e.parentId]);
|
||||
return server(e).finally(() =>
|
||||
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
|
||||
);
|
||||
}, {});
|
||||
|
||||
useMount(async () => {
|
||||
const data = await requestServer({ parentId: null });
|
||||
setFolderList([
|
||||
{
|
||||
id: rootId,
|
||||
name: t('common.folder.Root Path'),
|
||||
open: true,
|
||||
children: data.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
open: false
|
||||
}))
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
const RenderList = useMemoizedFn(
|
||||
({ list, index = 0 }: { list: FolderItemType[]; index?: number }) => {
|
||||
return (
|
||||
<>
|
||||
{list
|
||||
// can not move to itself
|
||||
.filter((item) => moveResourceId !== item.id)
|
||||
.map((item) => (
|
||||
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={1}
|
||||
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
|
||||
pr={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
{...(item.id === selectedId
|
||||
? {
|
||||
bg: 'primary.50 !important',
|
||||
onClick: () => setSelectedId(undefined)
|
||||
}
|
||||
: {
|
||||
onClick: () => setSelectedId(item.id)
|
||||
})}
|
||||
>
|
||||
{index !== 0 && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
visibility={!item.children || item.children.length > 0 ? 'visible' : 'hidden'}
|
||||
w={'1.25rem'}
|
||||
h={'1.25rem'}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'xs'}
|
||||
_hover={{
|
||||
bg: 'rgba(31, 35, 41, 0.08)'
|
||||
}}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (requestingIdList.includes(item.id)) return;
|
||||
|
||||
if (!item.children) {
|
||||
const data = await requestServer({ parentId: item.id });
|
||||
item.children = data.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
open: false
|
||||
}));
|
||||
}
|
||||
item.open = !item.open;
|
||||
setFolderList([...folderList]);
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={
|
||||
requestingIdList.includes(item.id)
|
||||
? 'common/loading'
|
||||
: 'common/rightArrowFill'
|
||||
}
|
||||
w={'1.25rem'}
|
||||
color={'myGray.500'}
|
||||
transform={item.open ? 'rotate(90deg)' : 'none'}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<MyIcon ml={index !== 0 ? '0.5rem' : 0} name={FolderIcon} w={'1.25rem'} />
|
||||
<Box fontSize={'sm'} ml={2}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
{item.children && item.open && (
|
||||
<Box mt={0.5}>
|
||||
<RenderList list={item.children} index={index + 1} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onConfirmSelect, loading: confirming } = useRequest2(
|
||||
() => {
|
||||
if (selectedId) {
|
||||
return onConfirm(selectedId === rootId ? null : selectedId);
|
||||
}
|
||||
return Promise.reject('');
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common.folder.Move Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isLoading={folderList.length === 0}
|
||||
iconSrc="/imgs/modal/move.svg"
|
||||
isOpen
|
||||
w={'30rem'}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}>
|
||||
<RenderList list={folderList} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button isLoading={confirming} isDisabled={!selectedId} onClick={onConfirmSelect}>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoveModal;
|
68
projects/app/src/components/common/folder/Path.tsx
Normal file
68
projects/app/src/components/common/folder/Path.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const FolderPath = (props: {
|
||||
paths: ParentTreePathItemType[];
|
||||
rootName?: string;
|
||||
FirstPathDom?: React.ReactNode;
|
||||
onClick: (parentId: string) => void;
|
||||
fontSize?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
|
||||
|
||||
const concatPaths = useMemo(
|
||||
() => [
|
||||
{
|
||||
parentId: '',
|
||||
parentName: rootName
|
||||
},
|
||||
...paths
|
||||
],
|
||||
[rootName, paths]
|
||||
);
|
||||
|
||||
return paths.length === 0 && !!FirstPathDom ? (
|
||||
<>{FirstPathDom}</>
|
||||
) : (
|
||||
<Flex flex={1} ml={-1.5}>
|
||||
{concatPaths.map((item, i) => (
|
||||
<Flex key={item.parentId || i} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={['sm', fontSize || 'sm']}
|
||||
py={0.5}
|
||||
px={1.5}
|
||||
borderRadius={'md'}
|
||||
{...(i === concatPaths.length - 1
|
||||
? {
|
||||
cursor: 'default',
|
||||
color: 'myGray.700',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
color: 'myGray.600',
|
||||
_hover: {
|
||||
bg: 'myGray.100'
|
||||
},
|
||||
onClick: () => {
|
||||
onClick(item.parentId);
|
||||
}
|
||||
})}
|
||||
>
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<Box mx={1.5} color={'myGray.500'}>
|
||||
/
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(FolderPath);
|
140
projects/app/src/components/common/folder/SelectOneResource.tsx
Normal file
140
projects/app/src/components/common/folder/SelectOneResource.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
GetResourceListItemResponse,
|
||||
ParentIdType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
type ResourceItemType = GetResourceListItemResponse & {
|
||||
open: boolean;
|
||||
children?: ResourceItemType[];
|
||||
};
|
||||
|
||||
const SelectOneResource = ({
|
||||
server,
|
||||
value,
|
||||
onSelect
|
||||
}: {
|
||||
server: (e: GetResourceFolderListProps) => Promise<GetResourceListItemResponse[]>;
|
||||
value?: ParentIdType;
|
||||
onSelect: (e?: string) => any;
|
||||
}) => {
|
||||
const [dataList, setDataList] = useState<ResourceItemType[]>([]);
|
||||
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
|
||||
|
||||
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
|
||||
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
|
||||
|
||||
setRequestingIdList((state) => [...state, e.parentId]);
|
||||
return server(e).finally(() =>
|
||||
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
|
||||
);
|
||||
}, {});
|
||||
|
||||
const { loading } = useRequest2(() => requestServer({ parentId: null }), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
setDataList(
|
||||
data.map((item) => ({
|
||||
...item,
|
||||
open: false
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const Render = useMemoizedFn(
|
||||
({ list, index = 0 }: { list: ResourceItemType[]; index?: number }) => {
|
||||
return (
|
||||
<>
|
||||
{list.map((item) => (
|
||||
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={1}
|
||||
pl={`${1.25 * index + 0.5}rem`}
|
||||
pr={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
{...(item.id === value
|
||||
? {
|
||||
bg: 'primary.50 !important',
|
||||
onClick: () => onSelect(undefined)
|
||||
}
|
||||
: {
|
||||
onClick: async () => {
|
||||
// folder => open(request children) or close
|
||||
if (item.isFolder) {
|
||||
if (!item.children) {
|
||||
const data = await requestServer({ parentId: item.id });
|
||||
item.children = data.map((item) => ({
|
||||
...item,
|
||||
open: false
|
||||
}));
|
||||
}
|
||||
|
||||
item.open = !item.open;
|
||||
setDataList([...dataList]);
|
||||
} else {
|
||||
onSelect(item.id);
|
||||
}
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
visibility={
|
||||
item.isFolder && (!item.children || item.children.length > 0)
|
||||
? 'visible'
|
||||
: 'hidden'
|
||||
}
|
||||
w={'1.25rem'}
|
||||
h={'1.25rem'}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'xs'}
|
||||
_hover={{
|
||||
bg: 'rgba(31, 35, 41, 0.08)'
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={
|
||||
requestingIdList.includes(item.id)
|
||||
? 'common/loading'
|
||||
: 'common/rightArrowFill'
|
||||
}
|
||||
w={'14px'}
|
||||
color={'myGray.500'}
|
||||
transform={item.open ? 'rotate(90deg)' : 'none'}
|
||||
/>
|
||||
</Flex>
|
||||
<Avatar ml={index !== 0 ? '0.5rem' : 0} src={item.avatar} w={'1.25rem'} />
|
||||
<Box fontSize={'sm'} ml={2}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
{item.children && item.open && (
|
||||
<Box mt={0.5}>
|
||||
<Render list={item.children} index={index + 1} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return loading ? <Loading fixed={false} /> : <Render list={dataList} />;
|
||||
};
|
||||
|
||||
export default SelectOneResource;
|
178
projects/app/src/components/common/folder/SlideCard.tsx
Normal file
178
projects/app/src/components/common/folder/SlideCard.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyDivider from '@fastgpt/web/components/common/MyDivider';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
|
||||
import {
|
||||
CollaboratorContextProvider,
|
||||
MemberManagerInputPropsType
|
||||
} from '../../support/permission/MemberManager/context';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
||||
const FolderSlideCard = ({
|
||||
name,
|
||||
intro,
|
||||
onEdit,
|
||||
onMove,
|
||||
deleteTip,
|
||||
onDelete,
|
||||
|
||||
defaultPer,
|
||||
managePer
|
||||
}: {
|
||||
name: string;
|
||||
intro?: string;
|
||||
onEdit: () => void;
|
||||
onMove: () => void;
|
||||
deleteTip: string;
|
||||
onDelete: () => void;
|
||||
|
||||
defaultPer: {
|
||||
value: PermissionValueType;
|
||||
defaultValue: PermissionValueType;
|
||||
onChange: (v: PermissionValueType) => Promise<any>;
|
||||
};
|
||||
managePer: MemberManagerInputPropsType;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
type: 'delete',
|
||||
content: deleteTip
|
||||
});
|
||||
|
||||
return (
|
||||
<Box w={'13rem'}>
|
||||
<Box>
|
||||
<HStack>
|
||||
<MyIcon name={FolderIcon} w={'1.5rem'} />
|
||||
<Box color={'myGray.900'}>{name}</Box>
|
||||
<MyIcon
|
||||
name={'edit'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
w={'0.875rem'}
|
||||
cursor={'pointer'}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</HStack>
|
||||
<Box mt={3} fontSize={'sm'} color={'myGray.500'} cursor={'pointer'} onClick={onEdit}>
|
||||
{intro || '暂无介绍'}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{managePer.permission.hasManagePer && (
|
||||
<>
|
||||
<MyDivider my={6} />
|
||||
|
||||
<Box>
|
||||
<FormLabel>{t('common.Operation')}</FormLabel>
|
||||
|
||||
<Button
|
||||
variant={'transparentBase'}
|
||||
pl={1}
|
||||
leftIcon={<MyIcon name={'common/file/move'} w={'1rem'} />}
|
||||
transform={'none !important'}
|
||||
w={'100%'}
|
||||
justifyContent={'flex-start'}
|
||||
size={'sm'}
|
||||
fontSize={'mini'}
|
||||
mt={4}
|
||||
onClick={onMove}
|
||||
>
|
||||
{t('common.Move')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'transparentDanger'}
|
||||
pl={1}
|
||||
leftIcon={<MyIcon name={'delete'} w={'1rem'} />}
|
||||
transform={'none !important'}
|
||||
w={'100%'}
|
||||
justifyContent={'flex-start'}
|
||||
size={'sm'}
|
||||
fontSize={'mini'}
|
||||
mt={3}
|
||||
onClick={() => {
|
||||
openConfirm(onDelete)();
|
||||
}}
|
||||
>
|
||||
{t('common.Delete folder')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<MyDivider my={6} />
|
||||
|
||||
<Box>
|
||||
<FormLabel>{t('support.permission.Permission')}</FormLabel>
|
||||
|
||||
{managePer.permission.hasManagePer && (
|
||||
<Box mt={5}>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('permission.Default permission')}
|
||||
</Box>
|
||||
<DefaultPermissionList
|
||||
mt="1"
|
||||
per={defaultPer.value}
|
||||
defaultPer={defaultPer.defaultValue}
|
||||
onChange={defaultPer.onChange}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box mt={6}>
|
||||
<CollaboratorContextProvider {...managePer}>
|
||||
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('permission.Collaborator')}
|
||||
</Box>
|
||||
{managePer.permission.hasManagePer && (
|
||||
<HStack spacing={3}>
|
||||
<MyTooltip label={t('permission.Manage')}>
|
||||
<MyIcon
|
||||
w="1rem"
|
||||
name="common/settingLight"
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={onOpenManageModal}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common.Add')}>
|
||||
<MyIcon
|
||||
w="1rem"
|
||||
name="support/permission/collaborator"
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={onOpenAddMember}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</HStack>
|
||||
)}
|
||||
</Flex>
|
||||
<MemberListCard
|
||||
mt={2}
|
||||
tagStyle={{
|
||||
type: 'borderSolid',
|
||||
colorSchema: 'gray'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</CollaboratorContextProvider>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ConfirmModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderSlideCard;
|
54
projects/app/src/components/common/folder/useFolderDrag.tsx
Normal file
54
projects/app/src/components/common/folder/useFolderDrag.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { useState, DragEvent, useCallback } from 'react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const useFolderDrag = ({
|
||||
onDrop,
|
||||
activeStyles
|
||||
}: {
|
||||
onDrop: (dragId: string, targetId: string) => any;
|
||||
activeStyles: BoxProps;
|
||||
}) => {
|
||||
const [dragId, setDragId] = useState<string>();
|
||||
const [targetId, setTargetId] = useState<string>();
|
||||
|
||||
const getBoxProps = useCallback(
|
||||
({ dataId, isFolder }: { dataId: string; isFolder: boolean }) => {
|
||||
return {
|
||||
draggable: true,
|
||||
'data-drag-id': isFolder ? dataId : undefined,
|
||||
onDragStart: (e: DragEvent<HTMLDivElement>) => {
|
||||
setDragId(dataId);
|
||||
},
|
||||
onDragOver: (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const targetId = e.currentTarget.getAttribute('data-drag-id');
|
||||
if (!targetId) return;
|
||||
setTargetId(targetId);
|
||||
},
|
||||
onDragLeave: (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setTargetId(undefined);
|
||||
},
|
||||
onDrop: (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (targetId && dragId && targetId !== dragId) {
|
||||
onDrop(dragId, targetId);
|
||||
}
|
||||
|
||||
setTargetId(undefined);
|
||||
setDragId(undefined);
|
||||
},
|
||||
...(activeStyles &&
|
||||
targetId === dataId && {
|
||||
...activeStyles
|
||||
})
|
||||
};
|
||||
},
|
||||
[activeStyles, dragId, onDrop, targetId]
|
||||
);
|
||||
|
||||
return {
|
||||
getBoxProps
|
||||
};
|
||||
};
|
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -68,6 +68,14 @@ const DatasetParamsModal = ({
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const [currentTabType, setCurrentTabType] = useState(SearchSettingTabEnum.searchMode);
|
||||
|
||||
const chatModelSelectList = (() =>
|
||||
llmModelList
|
||||
.filter((model) => model.usedInQueryExtension)
|
||||
.map((item) => ({
|
||||
value: item.model,
|
||||
label: item.name
|
||||
})))();
|
||||
|
||||
const { register, setValue, getValues, handleSubmit, watch } = useForm<DatasetParamsProps>({
|
||||
defaultValues: {
|
||||
limit,
|
||||
@@ -75,7 +83,7 @@ const DatasetParamsModal = ({
|
||||
searchMode,
|
||||
usingReRank: !!usingReRank && teamPlanStatus?.standardConstants?.permissionReRank !== false,
|
||||
datasetSearchUsingExtensionQuery,
|
||||
datasetSearchExtensionModel: datasetSearchExtensionModel ?? llmModelList[0]?.model,
|
||||
datasetSearchExtensionModel: datasetSearchExtensionModel || chatModelSelectList[0]?.value,
|
||||
datasetSearchExtensionBg
|
||||
}
|
||||
});
|
||||
@@ -85,14 +93,6 @@ const DatasetParamsModal = ({
|
||||
const usingReRankWatch = watch('usingReRank');
|
||||
const searchModeWatch = watch('searchMode');
|
||||
|
||||
const chatModelSelectList = (() =>
|
||||
llmModelList
|
||||
.filter((model) => model.usedInQueryExtension)
|
||||
.map((item) => ({
|
||||
value: item.model,
|
||||
label: item.name
|
||||
})))();
|
||||
|
||||
const searchModeList = useMemo(() => {
|
||||
const list = Object.values(DatasetSearchModeMap);
|
||||
return list;
|
||||
@@ -109,6 +109,15 @@ const DatasetParamsModal = ({
|
||||
return usingReRank !== undefined && reRankModelList.length > 0;
|
||||
}, [reRankModelList.length, usingReRank]);
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetSearchUsingCfrForm) {
|
||||
!queryExtensionModel &&
|
||||
setValue('datasetSearchExtensionModel', chatModelSelectList[0]?.value);
|
||||
} else {
|
||||
setValue('datasetSearchExtensionModel', '');
|
||||
}
|
||||
}, [chatModelSelectList, datasetSearchUsingCfrForm, queryExtensionModel, setValue]);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
@@ -270,7 +279,7 @@ const DatasetParamsModal = ({
|
||||
{t('core.dataset.Query extension intro')}
|
||||
</Box>
|
||||
<Flex mt={3} alignItems={'center'}>
|
||||
<Box flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</Box>
|
||||
<FormLabel flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</FormLabel>
|
||||
<Switch {...register('datasetSearchUsingExtensionQuery')} />
|
||||
</Flex>
|
||||
{datasetSearchUsingCfrForm === true && (
|
||||
|
@@ -237,7 +237,6 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
|
||||
});
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess() {
|
||||
setNewData(undefined);
|
||||
},
|
||||
|
@@ -14,22 +14,31 @@ const SearchParamsTip = ({
|
||||
limit = 1500,
|
||||
responseEmptyText,
|
||||
usingReRank = false,
|
||||
usingQueryExtension = false
|
||||
queryExtensionModel
|
||||
}: {
|
||||
searchMode: `${DatasetSearchModeEnum}`;
|
||||
similarity?: number;
|
||||
limit?: number;
|
||||
responseEmptyText?: string;
|
||||
usingReRank?: boolean;
|
||||
usingQueryExtension?: boolean;
|
||||
queryExtensionModel?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { reRankModelList } = useSystemStore();
|
||||
const { reRankModelList, llmModelList } = useSystemStore();
|
||||
|
||||
const hasReRankModel = reRankModelList.length > 0;
|
||||
const hasEmptyResponseMode = responseEmptyText !== undefined;
|
||||
const hasSimilarityMode = usingReRank || searchMode === DatasetSearchModeEnum.embedding;
|
||||
|
||||
const extensionModelName = useMemo(
|
||||
() =>
|
||||
queryExtensionModel
|
||||
? llmModelList.find((item) => item.model === queryExtensionModel)?.name ??
|
||||
llmModelList[0]?.name
|
||||
: undefined,
|
||||
[llmModelList, queryExtensionModel]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableContainer
|
||||
bg={'primary.50'}
|
||||
@@ -73,8 +82,8 @@ const SearchParamsTip = ({
|
||||
{usingReRank ? '✅' : '❌'}
|
||||
</Td>
|
||||
)}
|
||||
<Td pt={0} pb={2}>
|
||||
{usingQueryExtension ? '✅' : '❌'}
|
||||
<Td pt={0} pb={2} fontSize={'mini'}>
|
||||
{extensionModelName ? extensionModelName : '❌'}
|
||||
</Td>
|
||||
{hasEmptyResponseMode && <Th>{responseEmptyText !== '' ? '✅' : '❌'}</Th>}
|
||||
</Tr>
|
||||
|
@@ -1,108 +1,78 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { useAppStore } from '@/web/core/app/store/useAppStore';
|
||||
import SelectOneResource from '@/components/common/folder/SelectOneResource';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
GetResourceListItemResponse
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
const SelectAppModal = ({
|
||||
defaultApps = [],
|
||||
value,
|
||||
filterAppIds = [],
|
||||
max = 1,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
defaultApps: string[];
|
||||
value?: SelectAppItemType;
|
||||
filterAppIds?: string[];
|
||||
max?: number;
|
||||
onClose: () => void;
|
||||
onSuccess: (e: SelectAppItemType[]) => void;
|
||||
onSuccess: (e: SelectAppItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading } = useLoading();
|
||||
const theme = useTheme();
|
||||
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
|
||||
/* 加载模型 */
|
||||
const { myApps, loadMyApps } = useAppStore();
|
||||
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
|
||||
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
|
||||
|
||||
const apps = useMemo(
|
||||
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
|
||||
[myApps, filterAppIds]
|
||||
const getAppList = useCallback(
|
||||
async ({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({ parentId }).then((res) =>
|
||||
res
|
||||
.filter((item) => !filterAppIds.includes(item._id))
|
||||
.map<GetResourceListItemResponse>((item) => ({
|
||||
id: item._id,
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
isFolder: item.type === AppTypeEnum.folder
|
||||
}))
|
||||
);
|
||||
},
|
||||
[filterAppIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
|
||||
title={`选择应用`}
|
||||
iconSrc="/imgs/workflow/ai.svg"
|
||||
onClose={onClose}
|
||||
position={'relative'}
|
||||
w={'600px'}
|
||||
>
|
||||
<ModalBody
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
|
||||
gridGap={4}
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<Flex
|
||||
key={app._id}
|
||||
alignItems={'center'}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
p={2}
|
||||
cursor={'pointer'}
|
||||
{...(selectedApps.includes(app._id)
|
||||
? {
|
||||
bg: 'primary.100',
|
||||
onClick: () => {
|
||||
setSelectedApps(selectedApps.filter((e) => e !== app._id));
|
||||
}
|
||||
}
|
||||
: {
|
||||
onClick: () => {
|
||||
if (max === 1) {
|
||||
setSelectedApps([app._id]);
|
||||
} else if (selectedApps.length < max) {
|
||||
setSelectedApps([...selectedApps, app._id]);
|
||||
}
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Avatar src={app.avatar} w={['16px', '22px']} />
|
||||
<Box fontSize={'sm'} color={'myGray.900'} ml={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
|
||||
<SelectOneResource
|
||||
value={selectedApp?.id}
|
||||
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
|
||||
server={getAppList}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
{t('common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
ml={2}
|
||||
isDisabled={!selectedApp}
|
||||
onClick={() => {
|
||||
onSuccess(
|
||||
apps
|
||||
.filter((app) => selectedApps.includes(app._id))
|
||||
.map((app) => ({
|
||||
id: app._id,
|
||||
name: app.name,
|
||||
logo: app.avatar
|
||||
}))
|
||||
);
|
||||
if (!selectedApp) return;
|
||||
onSuccess(selectedApp);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { RenderInputProps } from '../type';
|
||||
import { Box, Button, Flex, useDisclosure, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import SelectAppModal from '../../../../SelectAppModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/components/core/workflow/context';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppDetailById } from '@/web/core/app/api';
|
||||
|
||||
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
@@ -21,8 +22,28 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
} = useDisclosure();
|
||||
|
||||
const value = item.value as SelectAppItemType | undefined;
|
||||
|
||||
const filterAppString = useMemo(() => filterAppIds?.join(',') || '', [filterAppIds]);
|
||||
const { data: appDetail, loading } = useRequest2(
|
||||
() => {
|
||||
if (value?.id) return getAppDetailById(value.id);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [value?.id],
|
||||
errorToast: 'Error',
|
||||
onError() {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key: 'app',
|
||||
value: {
|
||||
...item,
|
||||
value: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const Render = useMemo(() => {
|
||||
return (
|
||||
@@ -33,26 +54,22 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
{t('core.module.Select app')}
|
||||
</Button>
|
||||
) : (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
bg={'white'}
|
||||
px={3}
|
||||
py={2}
|
||||
<Button
|
||||
isLoading={loading}
|
||||
w={'100%'}
|
||||
justifyContent={loading ? 'center' : 'flex-start'}
|
||||
variant={'whiteFlow'}
|
||||
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
|
||||
>
|
||||
<Avatar src={value?.logo} w={6} />
|
||||
<Box fontWeight={'medium'} ml={2}>
|
||||
{value?.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
{appDetail?.name}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpenSelectApp && (
|
||||
<SelectAppModal
|
||||
defaultApps={item.value?.id ? [item.value.id] : []}
|
||||
filterAppIds={filterAppString.split(',')}
|
||||
value={item.value}
|
||||
filterAppIds={filterAppIds}
|
||||
onClose={onCloseSelectApp}
|
||||
onSuccess={(e) => {
|
||||
onChangeNode({
|
||||
@@ -61,7 +78,7 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
key: 'app',
|
||||
value: {
|
||||
...item,
|
||||
value: e[0]
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -70,15 +87,17 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
filterAppString,
|
||||
appDetail?.avatar,
|
||||
appDetail?.name,
|
||||
filterAppIds,
|
||||
isOpenSelectApp,
|
||||
item,
|
||||
loading,
|
||||
nodeId,
|
||||
onChangeNode,
|
||||
onCloseSelectApp,
|
||||
onOpenSelectApp,
|
||||
t,
|
||||
theme.borders.base,
|
||||
value
|
||||
]);
|
||||
|
||||
|
@@ -82,7 +82,7 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
|
||||
similarity={data.similarity}
|
||||
limit={data.limit}
|
||||
usingReRank={data.usingReRank}
|
||||
usingQueryExtension={data.datasetSearchUsingExtensionQuery}
|
||||
queryExtensionModel={data.datasetSearchExtensionModel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import { MemberManagerInputPropsType, CollaboratorContextProvider } from '../MemberManager/context';
|
||||
import { Box, Button, Flex, HStack, ModalBody } from '@chakra-ui/react';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import DefaultPermissionList from '../DefaultPerList';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
export type ConfigPerModalProps = {
|
||||
avatar?: string;
|
||||
name: string;
|
||||
|
||||
defaultPer: {
|
||||
value: PermissionValueType;
|
||||
defaultValue: PermissionValueType;
|
||||
onChange: (v: PermissionValueType) => Promise<any>;
|
||||
};
|
||||
managePer: MemberManagerInputPropsType;
|
||||
};
|
||||
|
||||
const ConfigPerModal = ({
|
||||
avatar,
|
||||
name,
|
||||
defaultPer,
|
||||
managePer,
|
||||
onClose
|
||||
}: ConfigPerModalProps & {
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="/imgs/modal/key.svg"
|
||||
onClose={onClose}
|
||||
title={t('permission.Permission config')}
|
||||
>
|
||||
<ModalBody>
|
||||
<HStack>
|
||||
<Avatar src={avatar} w={'1.75rem'} />
|
||||
<Box fontSize={'lg'}>{name}</Box>
|
||||
</HStack>
|
||||
<Box mt={6}>
|
||||
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
|
||||
<DefaultPermissionList
|
||||
mt="1"
|
||||
per={defaultPer.value}
|
||||
defaultPer={defaultPer.defaultValue}
|
||||
onChange={defaultPer.onChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<CollaboratorContextProvider {...managePer}>
|
||||
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
w="full"
|
||||
>
|
||||
<Box fontSize={'sm'}>{t('permission.Collaborator')}</Box>
|
||||
<Flex flexDirection="row" gap="2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="common/settingLight" />}
|
||||
onClick={onOpenManageModal}
|
||||
>
|
||||
{t('permission.Manage')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
|
||||
onClick={onOpenAddMember}
|
||||
>
|
||||
{t('common.Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</CollaboratorContextProvider>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigPerModal;
|
@@ -3,6 +3,8 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
export enum defaultPermissionEnum {
|
||||
private = 'private',
|
||||
@@ -13,16 +15,16 @@ export enum defaultPermissionEnum {
|
||||
type Props = Omit<BoxProps, 'onChange'> & {
|
||||
per: PermissionValueType;
|
||||
defaultPer: PermissionValueType;
|
||||
readPer: PermissionValueType;
|
||||
writePer: PermissionValueType;
|
||||
onChange: (v: PermissionValueType) => void;
|
||||
readPer?: PermissionValueType;
|
||||
writePer?: PermissionValueType;
|
||||
onChange: (v: PermissionValueType) => Promise<any> | any;
|
||||
};
|
||||
|
||||
const DefaultPermissionList = ({
|
||||
per,
|
||||
defaultPer,
|
||||
readPer,
|
||||
writePer,
|
||||
readPer = ReadPermissionVal,
|
||||
writePer = WritePermissionVal,
|
||||
onChange,
|
||||
...styles
|
||||
}: Props) => {
|
||||
@@ -33,14 +35,17 @@ const DefaultPermissionList = ({
|
||||
{ label: '团队可编辑', value: writePer }
|
||||
];
|
||||
|
||||
const { runAsync: onRequestChange, loading } = useRequest2(async (v: PermissionValueType) =>
|
||||
onChange(v)
|
||||
);
|
||||
|
||||
return (
|
||||
<Box {...styles}>
|
||||
<MySelect
|
||||
isLoading={loading}
|
||||
list={defaultPermissionSelectList}
|
||||
value={per}
|
||||
onchange={(v) => {
|
||||
onChange(v);
|
||||
}}
|
||||
onchange={onRequestChange}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -23,7 +23,6 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { Permission } from '@fastgpt/global/support/permission/controller';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
@@ -32,11 +31,10 @@ export type AddModalPropsType = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
const toast = useToast();
|
||||
function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const { permissionList, collaboratorList, onUpdateCollaborators, getPreLabelList } =
|
||||
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList } =
|
||||
useContextSelector(CollaboratorContext, (v) => v);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const {
|
||||
@@ -50,7 +48,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
});
|
||||
const filterMembers = useMemo(() => {
|
||||
return members.filter((item) => {
|
||||
if (item.permission.isOwner) return false;
|
||||
// if (item.permission.isOwner) return false;
|
||||
if (item.tmbId === userInfo?.team?.tmbId) return false;
|
||||
if (!searchText) return true;
|
||||
return item.memberName.includes(searchText);
|
||||
@@ -60,8 +58,8 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
|
||||
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
|
||||
const perLabel = useMemo(() => {
|
||||
return getPreLabelList(selectedPermission).join('、');
|
||||
}, [getPreLabelList, selectedPermission]);
|
||||
return getPerLabelList(selectedPermission).join('、');
|
||||
}, [getPerLabelList, selectedPermission]);
|
||||
|
||||
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
|
||||
mutationFn: () => {
|
||||
@@ -85,6 +83,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
borderColor="myGray.200"
|
||||
borderRadius="0.5rem"
|
||||
gridTemplateColumns="55% 45%"
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
@@ -141,7 +140,9 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
<MyAvatar src={member.avatar} w="32px" />
|
||||
<Box ml="2">{member.memberName}</Box>
|
||||
</Flex>
|
||||
{!!collaborator && <PermissionTags permission={collaborator.permission} />}
|
||||
{!!collaborator && (
|
||||
<PermissionTags permission={collaborator.permission.value} />
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
@@ -210,3 +211,5 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddMemberModal;
|
||||
|
@@ -18,10 +18,11 @@ import PermissionTags from './PermissionTags';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { CollaboratorContext } from './context';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import Loading from '@fastgpt/web/components/common/MyLoading';
|
||||
|
||||
export type ManageModalProps = {
|
||||
onClose: () => void;
|
||||
@@ -29,14 +30,12 @@ export type ManageModalProps = {
|
||||
|
||||
function ManageModal({ onClose }: ManageModalProps) {
|
||||
const { userInfo } = useUserStore();
|
||||
const { collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(v) => v
|
||||
);
|
||||
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
|
||||
useContextSelector(CollaboratorContext, (v) => v);
|
||||
|
||||
const { mutate: onDelete, isLoading: isDeleting } = useRequest({
|
||||
mutationFn: (tmbId: string) => onDelOneCollaborator(tmbId)
|
||||
});
|
||||
const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) =>
|
||||
onDelOneCollaborator(tmbId)
|
||||
);
|
||||
|
||||
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
|
||||
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
|
||||
@@ -49,14 +48,7 @@ function ManageModal({ onClose }: ManageModalProps) {
|
||||
const loading = isDeleting || isUpdating;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isLoading={loading}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
minW="600px"
|
||||
title="管理协作者"
|
||||
iconSrc="common/settingLight"
|
||||
>
|
||||
<MyModal isOpen onClose={onClose} minW="600px" title="管理协作者" iconSrc="common/settingLight">
|
||||
<ModalBody>
|
||||
<TableContainer borderRadius="md" minH="400px">
|
||||
<Table>
|
||||
@@ -86,26 +78,28 @@ function ManageModal({ onClose }: ManageModalProps) {
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td border="none">
|
||||
<PermissionTags permission={item.permission} />
|
||||
<PermissionTags permission={item.permission.value} />
|
||||
</Td>
|
||||
<Td border="none">
|
||||
{item.tmbId !== userInfo?.team?.tmbId && (
|
||||
<PermissionSelect
|
||||
Button={
|
||||
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
|
||||
}
|
||||
value={item.permission}
|
||||
onChange={(per) => {
|
||||
onUpdate({
|
||||
tmbId: item.tmbId,
|
||||
per
|
||||
});
|
||||
}}
|
||||
onDelete={() => {
|
||||
onDelete(item.tmbId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Not self; Not owner and other manager */}
|
||||
{item.tmbId !== userInfo?.team?.tmbId &&
|
||||
(permission.isOwner || !item.permission.hasManagePer) && (
|
||||
<PermissionSelect
|
||||
Button={
|
||||
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
|
||||
}
|
||||
value={item.permission.value}
|
||||
onChange={(per) => {
|
||||
onUpdate({
|
||||
tmbId: item.tmbId,
|
||||
per
|
||||
});
|
||||
}}
|
||||
onDelete={() => {
|
||||
onDelete(item.tmbId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
@@ -114,6 +108,7 @@ function ManageModal({ onClose }: ManageModalProps) {
|
||||
</Table>
|
||||
{collaboratorList?.length === 0 && <EmptyTip text={'暂无协作者'} />}
|
||||
</TableContainer>
|
||||
{loading && <Loading fixed={false} />}
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
|
@@ -0,0 +1,42 @@
|
||||
import { Box, BoxProps, Flex } from '@chakra-ui/react';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import React from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { CollaboratorContext } from './context';
|
||||
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
|
||||
|
||||
const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
return (
|
||||
<MyBox isLoading={isFetchingCollaborator} userSelect={'none'} {...props}>
|
||||
{collaboratorList?.length === 0 ? (
|
||||
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
|
||||
{t('permission.Not collaborator')}
|
||||
</Box>
|
||||
) : (
|
||||
<Flex gap="2" flexWrap={'wrap'}>
|
||||
{collaboratorList?.map((member) => {
|
||||
return (
|
||||
<Tag key={member.tmbId} type={'fill'} colorSchema="white" {...tagStyle}>
|
||||
<Avatar src={member.avatar} w="1.25rem" />
|
||||
<Box fontSize={'sm'}>{member.name}</Box>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemberListCard;
|
@@ -49,7 +49,7 @@ function PermissionSelect({
|
||||
...props
|
||||
}: PermissionSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const { permissionList } = useContextSelector(CollaboratorContext, (v) => v);
|
||||
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const closeTimer = useRef<any>();
|
||||
|
||||
@@ -66,10 +66,16 @@ function PermissionSelect({
|
||||
});
|
||||
|
||||
return {
|
||||
singleCheckBoxList: list.filter((item) => item.checkBoxType === 'single'),
|
||||
singleCheckBoxList: list
|
||||
.filter((item) => item.checkBoxType === 'single')
|
||||
.filter((item) => {
|
||||
if (permission.isOwner) return true;
|
||||
if (item.value === permissionList['manage'].value) return false;
|
||||
return true;
|
||||
}),
|
||||
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
|
||||
};
|
||||
}, [permissionList]);
|
||||
}, [permission.isOwner, permissionList]);
|
||||
const selectedSingleValue = useMemo(() => {
|
||||
const per = new Permission({ per: value });
|
||||
|
||||
@@ -88,6 +94,12 @@ function PermissionSelect({
|
||||
.map((item) => item.value);
|
||||
}, [permissionSelectList.multipleCheckBoxList, value]);
|
||||
|
||||
const onSelectPer = (per: PermissionValueType) => {
|
||||
if (per === value) return;
|
||||
onChange(per);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useOutsideClick({
|
||||
ref: ref,
|
||||
handler: () => {
|
||||
@@ -151,8 +163,7 @@ function PermissionSelect({
|
||||
const per = new Permission({ per: value });
|
||||
per.removePer(selectedSingleValue);
|
||||
per.addPer(item.value);
|
||||
onChange(per.value);
|
||||
setIsOpen(false);
|
||||
onSelectPer(per.value);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -10,9 +10,9 @@ export type PermissionTagsProp = {
|
||||
};
|
||||
|
||||
function PermissionTags({ permission }: PermissionTagsProp) {
|
||||
const { getPreLabelList } = useContextSelector(CollaboratorContext, (v) => v);
|
||||
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
|
||||
|
||||
const perTagList = getPreLabelList(permission);
|
||||
const perTagList = getPerLabelList(permission);
|
||||
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
|
||||
import { PermissionList } from '@fastgpt/global/support/permission/constant';
|
||||
import { Permission } from '@fastgpt/global/support/permission/controller';
|
||||
@@ -5,8 +6,14 @@ import { PermissionListType, PermissionValueType } from '@fastgpt/global/support
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import MemberListCard, { MemberListCardProps } from './MemberListCard';
|
||||
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
|
||||
const ManageModal = dynamic(() => import('./ManageModal'));
|
||||
|
||||
export type MemberManagerInputPropsType = {
|
||||
permission: Permission;
|
||||
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
|
||||
permissionList: PermissionListType;
|
||||
onUpdateCollaborators: (tmbIds: string[], permission: PermissionValueType) => any;
|
||||
@@ -16,7 +23,12 @@ export type MemberManagerPropsType = MemberManagerInputPropsType & {
|
||||
collaboratorList: CollaboratorItemType[];
|
||||
refetchCollaboratorList: () => void;
|
||||
isFetchingCollaborator: boolean;
|
||||
getPreLabelList: (per: PermissionValueType) => string[];
|
||||
getPerLabelList: (per: PermissionValueType) => string[];
|
||||
};
|
||||
export type ChildrenProps = {
|
||||
onOpenAddMember: () => void;
|
||||
onOpenManageModal: () => void;
|
||||
MemberListCard: (props: MemberListCardProps) => JSX.Element;
|
||||
};
|
||||
|
||||
type CollaboratorContextType = MemberManagerPropsType & {};
|
||||
@@ -30,7 +42,7 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
|
||||
onDelOneCollaborator: function () {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
getPreLabelList: function (): string[] {
|
||||
getPerLabelList: function (): string[] {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchCollaboratorList: function (): void {
|
||||
@@ -39,33 +51,36 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
|
||||
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isFetchingCollaborator: false
|
||||
isFetchingCollaborator: false,
|
||||
permission: new Permission()
|
||||
});
|
||||
|
||||
export const CollaboratorContextProvider = ({
|
||||
permission,
|
||||
onGetCollaboratorList,
|
||||
permissionList,
|
||||
onUpdateCollaborators,
|
||||
onDelOneCollaborator,
|
||||
children
|
||||
}: MemberManagerInputPropsType & {
|
||||
children: ReactNode;
|
||||
children: (props: ChildrenProps) => ReactNode;
|
||||
}) => {
|
||||
const {
|
||||
data: collaboratorList = [],
|
||||
refetch: refetchCollaboratorList,
|
||||
isLoading: isFetchingCollaborator
|
||||
} = useQuery(['collaboratorList'], onGetCollaboratorList);
|
||||
|
||||
const onUpdateCollaboratorsThen = async (tmbIds: string[], permission: PermissionValueType) => {
|
||||
await onUpdateCollaborators(tmbIds, permission);
|
||||
refetchCollaboratorList();
|
||||
};
|
||||
const onDelOneCollaboratorThem = async (tmbId: string) => {
|
||||
const onDelOneCollaboratorThen = async (tmbId: string) => {
|
||||
await onDelOneCollaborator(tmbId);
|
||||
refetchCollaboratorList();
|
||||
};
|
||||
|
||||
const getPreLabelList = useCallback(
|
||||
const getPerLabelList = useCallback(
|
||||
(per: PermissionValueType) => {
|
||||
const Per = new Permission({ per });
|
||||
const labels: string[] = [];
|
||||
@@ -91,17 +106,33 @@ export const CollaboratorContextProvider = ({
|
||||
[permissionList]
|
||||
);
|
||||
|
||||
const {
|
||||
isOpen: isOpenAddMember,
|
||||
onOpen: onOpenAddMember,
|
||||
onClose: onCloseAddMember
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenManageModal,
|
||||
onOpen: onOpenManageModal,
|
||||
onClose: onCloseManageModal
|
||||
} = useDisclosure();
|
||||
|
||||
const contextValue = {
|
||||
permission,
|
||||
onGetCollaboratorList,
|
||||
collaboratorList,
|
||||
refetchCollaboratorList,
|
||||
isFetchingCollaborator,
|
||||
permissionList,
|
||||
onUpdateCollaborators: onUpdateCollaboratorsThen,
|
||||
onDelOneCollaborator: onDelOneCollaboratorThem,
|
||||
getPreLabelList
|
||||
onDelOneCollaborator: onDelOneCollaboratorThen,
|
||||
getPerLabelList
|
||||
};
|
||||
return (
|
||||
<CollaboratorContext.Provider value={contextValue}>{children}</CollaboratorContext.Provider>
|
||||
<CollaboratorContext.Provider value={contextValue}>
|
||||
{children({ onOpenAddMember, onOpenManageModal, MemberListCard })}
|
||||
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
|
||||
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
|
||||
</CollaboratorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@@ -1,99 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Flex, Box, Button, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { AddMemberModal } from './AddMemberModal';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import ManageModal from './ManageModal';
|
||||
import {
|
||||
CollaboratorContext,
|
||||
CollaboratorContextProvider,
|
||||
MemberManagerInputPropsType
|
||||
} from './context';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
|
||||
function MemberManger() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isOpen: isOpenAddMember,
|
||||
onOpen: onOpenAddMember,
|
||||
onClose: onCloseAddMember
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenManageModal,
|
||||
onOpen: onOpenManageModal,
|
||||
onClose: onCloseManageModal
|
||||
} = useDisclosure();
|
||||
|
||||
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
|
||||
CollaboratorContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" flexDirection="row" justifyContent="space-between" w="full">
|
||||
<Box fontSize={'sm'}>协作者</Box>
|
||||
<Flex flexDirection="row" gap="2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="common/settingLight" />}
|
||||
onClick={onOpenManageModal}
|
||||
>
|
||||
{t('permission.Manage')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
|
||||
onClick={onOpenAddMember}
|
||||
>
|
||||
{t('common.Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
{/* member list */}
|
||||
<MyBox
|
||||
isLoading={isFetchingCollaborator}
|
||||
mt={2}
|
||||
bg="myGray.100"
|
||||
borderRadius="md"
|
||||
size={'md'}
|
||||
>
|
||||
{collaboratorList?.length === 0 ? (
|
||||
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
|
||||
暂无协作者
|
||||
</Box>
|
||||
) : (
|
||||
<Flex gap="2" p={1.5}>
|
||||
{collaboratorList?.map((member) => {
|
||||
return (
|
||||
<Tag px="4" py="1.5" bgColor="white" key={member.tmbId} width="fit-content">
|
||||
<Flex alignItems="center">
|
||||
<Avatar src={member.avatar} w="24px" />
|
||||
<TagLabel mx="2">{member.name}</TagLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</MyBox>
|
||||
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
|
||||
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Render(props: MemberManagerInputPropsType) {
|
||||
return (
|
||||
<CollaboratorContextProvider {...props}>
|
||||
<MemberManger />
|
||||
</CollaboratorContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Render);
|
@@ -6,6 +6,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
|
||||
|
||||
@@ -46,7 +47,7 @@ const TeamMenu = () => {
|
||||
<Flex w={'100%'} alignItems={'center'}>
|
||||
{userInfo?.team ? (
|
||||
<>
|
||||
<Image src={userInfo.team.avatar} alt={''} w={'16px'} />
|
||||
<Avatar src={userInfo.team.avatar} w={'1rem'} />
|
||||
<Box ml={2}>{userInfo.team.teamName}</Box>
|
||||
</>
|
||||
) : (
|
||||
|
14
projects/app/src/global/core/app/api.d.ts
vendored
14
projects/app/src/global/core/app/api.d.ts
vendored
@@ -1,17 +1,11 @@
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
|
||||
export type CreateAppParams = {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
type?: `${AppTypeEnum}`;
|
||||
modules: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
};
|
||||
|
||||
export type AppUpdateParams = {
|
||||
parentId?: ParentIdType;
|
||||
name?: string;
|
||||
type?: `${AppTypeEnum}`;
|
||||
type?: AppTypeEnum;
|
||||
avatar?: string;
|
||||
intro?: string;
|
||||
nodes?: AppSchema['modules'];
|
||||
@@ -23,7 +17,7 @@ export type AppUpdateParams = {
|
||||
};
|
||||
|
||||
export type PostPublishAppProps = {
|
||||
type: `${AppTypeEnum}`;
|
||||
type: AppTypeEnum;
|
||||
nodes: AppSchema['modules'];
|
||||
edges: AppSchema['edges'];
|
||||
chatConfig: AppSchema['chatConfig'];
|
||||
|
@@ -72,7 +72,7 @@ export type SearchTestResponse = {
|
||||
searchMode: `${DatasetSearchModeEnum}`;
|
||||
usingReRank: boolean;
|
||||
similarity: number;
|
||||
usingQueryExtension: boolean;
|
||||
queryExtensionModel?: string;
|
||||
};
|
||||
|
||||
/* =========== training =========== */
|
||||
|
@@ -17,7 +17,6 @@ import type { UsageItemType } from '@fastgpt/global/support/wallet/usage/type';
|
||||
import { usePagination } from '@fastgpt/web/hooks/usePagination';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import DateRangePicker, {
|
||||
type DateRangeType
|
||||
} from '@fastgpt/web/components/common/DateRangePicker';
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import type { CreateAppParams } from '@/global/core/app/api.d';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
@@ -9,17 +8,24 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import type { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const {
|
||||
name = 'APP',
|
||||
avatar,
|
||||
type = AppTypeEnum.advanced,
|
||||
modules,
|
||||
edges
|
||||
} = req.body as CreateAppParams;
|
||||
export type CreateAppBody = {
|
||||
parentId?: ParentIdType;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
type?: AppTypeEnum;
|
||||
modules: AppSchema['modules'];
|
||||
edges?: AppSchema['edges'];
|
||||
};
|
||||
|
||||
if (!name || !Array.isArray(modules)) {
|
||||
async function handler(req: ApiRequestProps<CreateAppBody>, res: NextApiResponse<any>) {
|
||||
const { parentId, name, avatar, type, modules, edges } = req.body;
|
||||
|
||||
if (!name || !type || !Array.isArray(modules)) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
@@ -34,6 +40,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const [{ _id: appId }] = await MongoApp.create(
|
||||
[
|
||||
{
|
||||
...parseParentIdInMongo(parentId),
|
||||
avatar,
|
||||
name,
|
||||
teamId,
|
||||
|
@@ -9,6 +9,7 @@ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
|
||||
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { findAppAndAllChildren } from '@fastgpt/service/core/app/controller';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { appId } = req.query as { appId: string };
|
||||
@@ -17,50 +18,61 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
|
||||
// Auth owner (folder owner, can delete all apps in the folder)
|
||||
const { teamId } = await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
|
||||
|
||||
const apps = await findAppAndAllChildren({
|
||||
teamId,
|
||||
appId,
|
||||
fields: '_id'
|
||||
});
|
||||
|
||||
console.log(apps);
|
||||
|
||||
// 删除对应的聊天
|
||||
await mongoSessionRun(async (session) => {
|
||||
await MongoChatItem.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChat.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// 删除分享链接
|
||||
await MongoOutLink.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// delete version
|
||||
await MongoAppVersion.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChatInputGuide.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// delete app
|
||||
await MongoApp.deleteOne(
|
||||
{
|
||||
_id: appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
for await (const app of apps) {
|
||||
const appId = app._id;
|
||||
// Chats
|
||||
await MongoChatItem.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChat.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// 删除分享链接
|
||||
await MongoOutLink.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// delete version
|
||||
await MongoAppVersion.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChatInputGuide.deleteMany(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
// delete app
|
||||
await MongoApp.deleteOne(
|
||||
{
|
||||
_id: appId
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
|
||||
/* 获取我的模型 */
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -11,7 +11,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
throw new Error('参数错误');
|
||||
}
|
||||
// 凭证校验
|
||||
const { app } = await authApp({ req, authToken: true, appId, per: WritePermissionVal });
|
||||
const { app } = await authApp({ req, authToken: true, appId, per: ReadPermissionVal });
|
||||
|
||||
if (!app.permission.hasWritePer) {
|
||||
app.modules = [];
|
||||
app.edges = [];
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
40
projects/app/src/pages/api/core/app/folder/create.ts
Normal file
40
projects/app/src/pages/api/core/app/folder/create.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
|
||||
export type CreateAppFolderBody = {
|
||||
parentId?: ParentIdType;
|
||||
name: string;
|
||||
intro?: string;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<CreateAppFolderBody>, res: NextApiResponse<any>) {
|
||||
const { name, intro, parentId } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
|
||||
|
||||
// Create app
|
||||
await MongoApp.create({
|
||||
...parseParentIdInMongo(parentId),
|
||||
avatar: FolderImgUrl,
|
||||
name,
|
||||
intro,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: AppTypeEnum.folder
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
41
projects/app/src/pages/api/core/app/folder/path.ts
Normal file
41
projects/app/src/pages/api/core/app/folder/path.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type {
|
||||
ParentIdType,
|
||||
ParentTreePathItemType
|
||||
} from '@fastgpt/global/common/parentFolder/type.d';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<ParentTreePathItemType[]> {
|
||||
const { parentId } = req.query as { parentId: string };
|
||||
|
||||
if (!parentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await authApp({ req, authToken: true, appId: parentId, per: ReadPermissionVal });
|
||||
|
||||
return await getParents(parentId);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
async function getParents(parentId: ParentIdType): Promise<ParentTreePathItemType[]> {
|
||||
if (!parentId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const parent = await MongoApp.findById(parentId, 'name parentId');
|
||||
|
||||
if (!parent) return [];
|
||||
|
||||
const paths = await getParents(parent.parentId);
|
||||
paths.push({ parentId, parentName: parent.name });
|
||||
|
||||
return paths;
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import type { NextApiResponse } from 'next';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
@@ -9,8 +9,21 @@ import {
|
||||
ReadPermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<AppListItemType[]> {
|
||||
export type ListAppBody = {
|
||||
parentId: ParentIdType;
|
||||
type?: AppTypeEnum;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<ListAppBody>,
|
||||
res: NextApiResponse<any>
|
||||
): Promise<AppListItemType[]> {
|
||||
// 凭证校验
|
||||
const {
|
||||
teamId,
|
||||
@@ -22,9 +35,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const { parentId, type } = req.body;
|
||||
|
||||
/* temp: get all apps and per */
|
||||
const [myApps, rpList] = await Promise.all([
|
||||
MongoApp.find({ teamId }, '_id avatar name intro tmbId defaultPermission')
|
||||
MongoApp.find(
|
||||
{ teamId, ...(type && { type }), ...parseParentIdInMongo(parentId) },
|
||||
'_id avatar type name intro tmbId defaultPermission'
|
||||
)
|
||||
.sort({
|
||||
updateTime: -1
|
||||
})
|
||||
@@ -54,10 +72,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
|
||||
return filterApps.map((app) => ({
|
||||
_id: app._id,
|
||||
avatar: app.avatar,
|
||||
type: app.type,
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
permission: app.permission,
|
||||
defaultPermission: app.defaultPermission
|
||||
defaultPermission: app.defaultPermission || AppDefaultPermissionVal
|
||||
}));
|
||||
}
|
||||
|
||||
|
@@ -9,10 +9,12 @@ import {
|
||||
WritePermissionVal,
|
||||
OwnerPermissionVal
|
||||
} from '@fastgpt/global/support/permission/constant';
|
||||
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
|
||||
|
||||
/* 获取我的模型 */
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const {
|
||||
parentId,
|
||||
name,
|
||||
avatar,
|
||||
type,
|
||||
@@ -49,6 +51,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
_id: appId
|
||||
},
|
||||
{
|
||||
...parseParentIdInMongo(parentId),
|
||||
name,
|
||||
type,
|
||||
avatar,
|
||||
|
@@ -92,7 +92,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
data: {
|
||||
list: searchRes,
|
||||
duration: `${((Date.now() - start) / 1000).toFixed(3)}s`,
|
||||
usingQueryExtension: !!aiExtensionResult,
|
||||
queryExtensionModel: aiExtensionResult?.model,
|
||||
...result
|
||||
}
|
||||
});
|
||||
|
@@ -20,7 +20,7 @@ import Avatar from '@/components/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import MemberManager from '@/components/support/permission/MemberManager';
|
||||
import { CollaboratorContextProvider } from '@/components/support/permission/MemberManager/context';
|
||||
import {
|
||||
postUpdateAppCollaborators,
|
||||
deleteAppCollaborators,
|
||||
@@ -29,12 +29,12 @@ import {
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '@/web/core/app/context/appContext';
|
||||
import {
|
||||
AppDefaultPermission,
|
||||
AppDefaultPermissionVal,
|
||||
AppPermissionList
|
||||
} from '@fastgpt/global/support/permission/app/constant';
|
||||
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -181,25 +181,57 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
|
||||
{/* role */}
|
||||
{appDetail.permission.hasManagePer && (
|
||||
<>
|
||||
{' '}
|
||||
<Box mt="4">
|
||||
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
|
||||
<DefaultPermissionList
|
||||
mt="2"
|
||||
per={defaultPermission}
|
||||
defaultPer={AppDefaultPermission}
|
||||
readPer={ReadPermissionVal}
|
||||
writePer={WritePermissionVal}
|
||||
defaultPer={AppDefaultPermissionVal}
|
||||
onChange={(v) => setValue('defaultPermission', v)}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={6}>
|
||||
<MemberManager
|
||||
<CollaboratorContextProvider
|
||||
permission={appDetail.permission}
|
||||
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
|
||||
permissionList={AppPermissionList}
|
||||
onUpdateCollaborators={onUpdateCollaborators}
|
||||
onDelOneCollaborator={onDelCollaborator}
|
||||
/>
|
||||
>
|
||||
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
w="full"
|
||||
>
|
||||
<Box fontSize={'sm'}>协作者</Box>
|
||||
<Flex flexDirection="row" gap="2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="common/settingLight" />}
|
||||
onClick={onOpenManageModal}
|
||||
>
|
||||
{t('permission.Manage')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="whitePrimary"
|
||||
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
|
||||
onClick={onOpenAddMember}
|
||||
>
|
||||
{t('common.Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</CollaboratorContextProvider>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
@@ -6,7 +6,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type.d';
|
||||
import { delModelById } from '@/web/core/app/api';
|
||||
import { delAppById } from '@/web/core/app/api';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -45,7 +45,7 @@ const AppCard = () => {
|
||||
const { mutate: handleDelModel, isLoading } = useRequest({
|
||||
mutationFn: async () => {
|
||||
if (!appDetail) return null;
|
||||
await delModelById(appDetail._id);
|
||||
await delAppById(appDetail._id);
|
||||
return 'success';
|
||||
},
|
||||
onSuccess(res) {
|
||||
|
@@ -297,7 +297,7 @@ const EditForm = ({
|
||||
similarity={getValues('dataset.similarity')}
|
||||
limit={getValues('dataset.limit')}
|
||||
usingReRank={getValues('dataset.usingReRank')}
|
||||
usingQueryExtension={getValues('dataset.datasetSearchUsingExtensionQuery')}
|
||||
queryExtensionModel={getValues('dataset.datasetSearchExtensionModel')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
@@ -25,6 +25,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
|
||||
type FormType = {
|
||||
avatar: string;
|
||||
@@ -32,12 +34,15 @@ type FormType = {
|
||||
templateId: string;
|
||||
};
|
||||
|
||||
const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => {
|
||||
const CreateModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
|
||||
const theme = useTheme();
|
||||
const { isPc, feConfigs } = useSystemStore();
|
||||
const { isPc } = useSystemStore();
|
||||
|
||||
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
avatar: '',
|
||||
@@ -82,6 +87,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
|
||||
return Promise.reject(t('core.dataset.error.Template does not exist'));
|
||||
}
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: data.avatar || template.avatar,
|
||||
name: data.name,
|
||||
type: template.type,
|
||||
@@ -91,7 +97,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
|
||||
},
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
onSuccess();
|
||||
loadMyApps();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common.Create Success'),
|
||||
|
294
projects/app/src/pages/app/list/component/List.tsx
Normal file
294
projects/app/src/pages/app/list/component/List.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { delAppById, putAppById } from '@/web/core/app/api';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useFolderDrag } from '@/components/common/folder/useFolderDrag';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import {
|
||||
AppDefaultPermissionVal,
|
||||
AppPermissionList
|
||||
} from '@fastgpt/global/support/permission/app/constant';
|
||||
import {
|
||||
deleteAppCollaborators,
|
||||
getCollaboratorList,
|
||||
postUpdateAppCollaborators
|
||||
} from '@/web/core/app/api/collaborator';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
||||
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
|
||||
const ConfigPerModal = dynamic(() => import('@/components/support/permission/ConfigPerModal'));
|
||||
|
||||
const ListItem = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const router = useRouter();
|
||||
const { myApps, loadMyApps, onUpdateApp, setMoveAppId } = useContextSelector(
|
||||
AppListContext,
|
||||
(v) => v
|
||||
);
|
||||
const [loadingAppId, setLoadingAppId] = useState<string>();
|
||||
|
||||
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
|
||||
const [editPerAppIndex, setEditPerAppIndex] = useState<number>();
|
||||
const editPerApp = useMemo(
|
||||
() => (editPerAppIndex !== undefined ? myApps[editPerAppIndex] : undefined),
|
||||
[editPerAppIndex, myApps]
|
||||
);
|
||||
|
||||
const { getBoxProps } = useFolderDrag({
|
||||
activeStyles: {
|
||||
borderColor: 'primary.600'
|
||||
},
|
||||
onDrop: async (dragId: string, targetId: string) => {
|
||||
setLoadingAppId(dragId);
|
||||
try {
|
||||
await putAppById(dragId, { parentId: targetId });
|
||||
loadMyApps();
|
||||
} catch (error) {}
|
||||
setLoadingAppId(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
type: 'delete'
|
||||
});
|
||||
|
||||
const { run: onclickDelApp } = useRequest2(
|
||||
(id: string) => {
|
||||
setLoadingAppId(id);
|
||||
return delAppById(id);
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
},
|
||||
onFinally() {
|
||||
setLoadingAppId(undefined);
|
||||
},
|
||||
successToast: t('common.Delete Success'),
|
||||
errorToast: t('common.Delete Failed')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
py={[4, 6]}
|
||||
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
|
||||
gridGap={5}
|
||||
>
|
||||
{myApps.map((app, index) => (
|
||||
<MyTooltip
|
||||
key={app._id}
|
||||
label={
|
||||
app.type === AppTypeEnum.folder
|
||||
? t('common.folder.Open folder')
|
||||
: app.permission.hasWritePer
|
||||
? appT('Edit app')
|
||||
: appT('Go to chat')
|
||||
}
|
||||
>
|
||||
<MyBox
|
||||
isLoading={loadingAppId === app._id}
|
||||
lineHeight={1.5}
|
||||
h={'100%'}
|
||||
py={3}
|
||||
px={5}
|
||||
cursor={'pointer'}
|
||||
borderWidth={'1.5px'}
|
||||
borderColor={'borderColor.low'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
userSelect={'none'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .more': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& .chat': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (app.type === AppTypeEnum.folder) {
|
||||
router.push({
|
||||
query: {
|
||||
parentId: app._id
|
||||
}
|
||||
});
|
||||
} else if (app.permission.hasWritePer) {
|
||||
router.push(`/app/detail?appId=${app._id}`);
|
||||
} else {
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}
|
||||
}}
|
||||
{...getBoxProps({
|
||||
dataId: app._id,
|
||||
isFolder: app.type === AppTypeEnum.folder
|
||||
})}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={app.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3}>{app.name}</Box>
|
||||
{app.permission.hasManagePer && (
|
||||
<Box
|
||||
className="more"
|
||||
position={'absolute'}
|
||||
top={3.5}
|
||||
right={4}
|
||||
display={['', 'none']}
|
||||
>
|
||||
<MyMenu
|
||||
Button={
|
||||
<IconButton
|
||||
size={'xsSquare'}
|
||||
variant={'transparentBase'}
|
||||
icon={<MyIcon name={'more'} w={'1rem'} />}
|
||||
aria-label={''}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'edit',
|
||||
label: '编辑信息',
|
||||
onClick: () =>
|
||||
setEditedApp({
|
||||
id: app._id,
|
||||
avatar: app.avatar,
|
||||
name: app.name,
|
||||
intro: app.intro
|
||||
})
|
||||
},
|
||||
{
|
||||
icon: 'common/file/move',
|
||||
label: t('common.folder.Move to'),
|
||||
onClick: () => setMoveAppId(app._id)
|
||||
},
|
||||
...(app.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/team/key',
|
||||
label: t('permission.Permission'),
|
||||
onClick: () => setEditPerAppIndex(index)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
...(app.permission.isOwner
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
type: 'danger' as 'danger',
|
||||
icon: 'delete',
|
||||
label: t('common.Delete'),
|
||||
onClick: () =>
|
||||
openConfirm(
|
||||
() => onclickDelApp(app._id),
|
||||
undefined,
|
||||
app.type === AppTypeEnum.folder
|
||||
? appT('Confirm delete folder tip')
|
||||
: appT('Confirm Del App Tip')
|
||||
)()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Box
|
||||
flex={1}
|
||||
className={'textEllipsis3'}
|
||||
py={2}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{app.intro || '还没写介绍~'}
|
||||
</Box>
|
||||
<Flex h={'34px'} alignItems={'flex-end'}>
|
||||
<Box flex={1}>
|
||||
<PermissionIconText
|
||||
defaultPermission={app.defaultPermission}
|
||||
color={'myGray.600'}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />}
|
||||
<ConfirmModal />
|
||||
{!!editedApp && (
|
||||
<EditResourceModal
|
||||
{...editedApp}
|
||||
title="应用信息编辑"
|
||||
onClose={() => {
|
||||
setEditedApp(undefined);
|
||||
}}
|
||||
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
|
||||
/>
|
||||
)}
|
||||
{!!editPerApp && (
|
||||
<ConfigPerModal
|
||||
avatar={editPerApp.avatar}
|
||||
name={editPerApp.name}
|
||||
defaultPer={{
|
||||
value: editPerApp.defaultPermission,
|
||||
defaultValue: AppDefaultPermissionVal,
|
||||
onChange: (e) => {
|
||||
return onUpdateApp(editPerApp._id, { defaultPermission: e });
|
||||
}
|
||||
}}
|
||||
managePer={{
|
||||
permission: editPerApp.permission,
|
||||
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
|
||||
permissionList: AppPermissionList,
|
||||
onUpdateCollaborators: (tmbIds: string[], permission: number) => {
|
||||
return postUpdateAppCollaborators({
|
||||
tmbIds,
|
||||
permission,
|
||||
appId: editPerApp._id
|
||||
});
|
||||
},
|
||||
onDelOneCollaborator: (tmbId: string) =>
|
||||
deleteAppCollaborators({
|
||||
appId: editPerApp._id,
|
||||
tmbId
|
||||
})
|
||||
}}
|
||||
onClose={() => setEditPerAppIndex(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
138
projects/app/src/pages/app/list/component/context.tsx
Normal file
138
projects/app/src/pages/app/list/component/context.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { ReactNode, useCallback, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppDetailById, getMyApps, putAppById } from '@/web/core/app/api';
|
||||
import { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import {
|
||||
GetResourceFolderListProps,
|
||||
ParentIdType,
|
||||
ParentTreePathItemType
|
||||
} from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppUpdateParams } from '@/global/core/app/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { delay } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
type AppListContextType = {
|
||||
parentId?: string | null;
|
||||
myApps: AppListItemType[];
|
||||
loadMyApps: () => void;
|
||||
isFetchingApps: boolean;
|
||||
folderDetail: AppDetailType | undefined | null;
|
||||
paths: ParentTreePathItemType[];
|
||||
onUpdateApp: (id: string, data: AppUpdateParams) => Promise<any>;
|
||||
setMoveAppId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
};
|
||||
|
||||
export const AppListContext = createContext<AppListContextType>({
|
||||
parentId: undefined,
|
||||
myApps: [],
|
||||
loadMyApps: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
isFetchingApps: false,
|
||||
folderDetail: undefined,
|
||||
paths: [],
|
||||
onUpdateApp: function (id: string, data: AppUpdateParams): Promise<any> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
|
||||
|
||||
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { appT } = useI18n();
|
||||
const router = useRouter();
|
||||
const { parentId = null } = router.query as { parentId?: string | null };
|
||||
|
||||
const {
|
||||
data = [],
|
||||
runAsync: loadMyApps,
|
||||
loading: isFetchingApps
|
||||
} = useRequest2(() => getMyApps({ parentId }), {
|
||||
manual: false,
|
||||
refreshOnWindowFocus: true,
|
||||
refreshDeps: [parentId]
|
||||
});
|
||||
|
||||
const { data: paths = [], runAsync: refetchPaths } = useRequest2(
|
||||
() => getAppFolderPath(parentId),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const { data: folderDetail, runAsync: refetchFolderDetail } = useRequest2(
|
||||
() => {
|
||||
if (parentId) return getAppDetailById(parentId);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onUpdateApp } = useRequest2((id: string, data: AppUpdateParams) =>
|
||||
putAppById(id, data).then(async (res) => {
|
||||
await Promise.all([refetchFolderDetail(), refetchPaths(), loadMyApps()]);
|
||||
return res;
|
||||
})
|
||||
);
|
||||
|
||||
const [moveAppId, setMoveAppId] = useState<string>();
|
||||
const onMoveApp = useCallback(
|
||||
async (parentId: ParentIdType) => {
|
||||
if (!moveAppId) return;
|
||||
await onUpdateApp(moveAppId, { parentId });
|
||||
},
|
||||
[moveAppId, onUpdateApp]
|
||||
);
|
||||
|
||||
const getAppFolderList = useCallback(({ parentId }: GetResourceFolderListProps) => {
|
||||
return getMyApps({
|
||||
parentId,
|
||||
type: AppTypeEnum.folder
|
||||
}).then((res) =>
|
||||
res.map((item) => ({
|
||||
id: item._id,
|
||||
name: item.name
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const contextValue: AppListContextType = {
|
||||
parentId,
|
||||
myApps: data,
|
||||
loadMyApps,
|
||||
isFetchingApps,
|
||||
folderDetail,
|
||||
paths,
|
||||
onUpdateApp,
|
||||
setMoveAppId
|
||||
};
|
||||
return (
|
||||
<AppListContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{!!moveAppId && (
|
||||
<MoveModal
|
||||
moveResourceId={moveAppId}
|
||||
server={getAppFolderList}
|
||||
title={appT('Move app')}
|
||||
onClose={() => setMoveAppId(undefined)}
|
||||
onConfirm={onMoveApp}
|
||||
/>
|
||||
)}
|
||||
</AppListContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppListContextProvider;
|
@@ -1,192 +1,209 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box, Grid, Flex, IconButton, Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, Flex, Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { delModelById } from '@/web/core/app/api';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { serviceSideProps } from '@/web/common/utils/i18n';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import CreateModal from './component/CreateModal';
|
||||
import { useAppStore } from '@/web/core/app/store/useAppStore';
|
||||
import PermissionIconText from '@/components/support/permission/IconText';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import List from './component/List';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postCreateAppFolder } from '@/web/core/app/api/app';
|
||||
import type { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditFolderModal';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import AppListContextProvider, { AppListContext } from './component/context';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { useRouter } from 'next/router';
|
||||
import FolderSlideCard from '@/components/common/folder/SlideCard';
|
||||
import { delAppById } from '@/web/core/app/api';
|
||||
import {
|
||||
AppDefaultPermissionVal,
|
||||
AppPermissionList
|
||||
} from '@fastgpt/global/support/permission/app/constant';
|
||||
import {
|
||||
deleteAppCollaborators,
|
||||
getCollaboratorList,
|
||||
postUpdateAppCollaborators
|
||||
} from '@/web/core/app/api/collaborator';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
|
||||
const EditFolderModal = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
|
||||
);
|
||||
|
||||
const MyApps = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { appT, commonT } = useI18n();
|
||||
|
||||
const { appT } = useI18n();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystemStore();
|
||||
const {
|
||||
paths,
|
||||
parentId,
|
||||
myApps,
|
||||
loadMyApps,
|
||||
onUpdateApp,
|
||||
setMoveAppId,
|
||||
isFetchingApps,
|
||||
folderDetail
|
||||
} = useContextSelector(AppListContext, (v) => v);
|
||||
const { userInfo } = useUserStore();
|
||||
const { myApps, loadMyApps } = useAppStore();
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
type: 'delete',
|
||||
content: '确认删除该应用所有信息?'
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenCreateModal,
|
||||
onOpen: onOpenCreateModal,
|
||||
onClose: onCloseCreateModal
|
||||
} = useDisclosure();
|
||||
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
|
||||
|
||||
/* 点击删除 */
|
||||
const onclickDelApp = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await delModelById(id);
|
||||
toast({
|
||||
title: '删除成功',
|
||||
status: 'success'
|
||||
});
|
||||
loadMyApps();
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || t('common.Delete Failed'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
|
||||
onSuccess() {
|
||||
loadMyApps();
|
||||
},
|
||||
[toast, loadMyApps, t]
|
||||
);
|
||||
|
||||
/* 加载模型 */
|
||||
const { isFetching } = useQuery(['loadApps'], () => loadMyApps(), {
|
||||
refetchOnMount: true
|
||||
errorToast: 'Error'
|
||||
});
|
||||
const { runAsync: onDeleFolder } = useRequest2(delAppById, {
|
||||
onSuccess() {
|
||||
router.replace({
|
||||
query: {
|
||||
parentId: folderDetail?.parentId
|
||||
}
|
||||
});
|
||||
},
|
||||
errorToast: 'Error'
|
||||
});
|
||||
|
||||
return (
|
||||
<PageContainer isLoading={isFetching} insertProps={{ px: [5, '48px'] }}>
|
||||
<Flex pt={[4, '30px']} alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box letterSpacing={1} fontSize={['20px', '24px']} color={'myGray.900'}>
|
||||
{appT('My Apps')}
|
||||
<PageContainer
|
||||
isLoading={myApps.length === 0 && isFetchingApps}
|
||||
insertProps={{ px: folderDetail ? [4, 6] : [4, 10] }}
|
||||
>
|
||||
<Flex gap={5}>
|
||||
<Box flex={'1 0 0'}>
|
||||
<Flex pt={[4, 6]} alignItems={'center'} justifyContent={'space-between'}>
|
||||
<FolderPath
|
||||
paths={paths}
|
||||
FirstPathDom={
|
||||
<Box letterSpacing={1} fontSize={['md', 'lg']} color={'myGray.900'}>
|
||||
{appT('My Apps')}
|
||||
</Box>
|
||||
}
|
||||
onClick={(parentId) => {
|
||||
router.push({
|
||||
query: {
|
||||
parentId
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{userInfo?.team.permission.hasWritePer && (
|
||||
<MyMenu
|
||||
width={150}
|
||||
iconSize="1.5rem"
|
||||
Button={
|
||||
<Button variant={'primary'} leftIcon={<AddIcon />}>
|
||||
<Box>{t('common.Create New')}</Box>
|
||||
</Button>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/simpleBot',
|
||||
label: appT('Create bot'),
|
||||
description: appT('Create one ai app'),
|
||||
onClick: onOpenCreateModal
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: FolderIcon,
|
||||
label: t('Folder'),
|
||||
onClick: () => setEditFolder({})
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<List />
|
||||
</Box>
|
||||
{userInfo?.team.permission.hasWritePer && (
|
||||
<Button leftIcon={<AddIcon />} variant={'primaryOutline'} onClick={onOpenCreateModal}>
|
||||
{commonT('New Create')}
|
||||
</Button>
|
||||
{!!folderDetail && isPc && (
|
||||
<Box pt={[4, 6]}>
|
||||
<FolderSlideCard
|
||||
name={folderDetail.name}
|
||||
intro={folderDetail.intro}
|
||||
onEdit={() => {
|
||||
setEditFolder({
|
||||
id: folderDetail._id,
|
||||
name: folderDetail.name,
|
||||
intro: folderDetail.intro
|
||||
});
|
||||
}}
|
||||
onMove={() => setMoveAppId(folderDetail._id)}
|
||||
deleteTip={appT('Confirm delete folder tip')}
|
||||
onDelete={() => onDeleFolder(folderDetail._id)}
|
||||
defaultPer={{
|
||||
value: folderDetail.defaultPermission,
|
||||
defaultValue: AppDefaultPermissionVal,
|
||||
onChange: (e) => {
|
||||
return onUpdateApp(folderDetail._id, { defaultPermission: e });
|
||||
}
|
||||
}}
|
||||
managePer={{
|
||||
permission: folderDetail.permission,
|
||||
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
|
||||
permissionList: AppPermissionList,
|
||||
onUpdateCollaborators: (tmbIds: string[], permission: number) => {
|
||||
return postUpdateAppCollaborators({
|
||||
tmbIds,
|
||||
permission,
|
||||
appId: folderDetail._id
|
||||
});
|
||||
},
|
||||
onDelOneCollaborator: (tmbId: string) =>
|
||||
deleteAppCollaborators({
|
||||
appId: folderDetail._id,
|
||||
tmbId
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
<Grid
|
||||
py={[4, 6]}
|
||||
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
|
||||
gridGap={5}
|
||||
>
|
||||
{myApps.map((app) => (
|
||||
<MyTooltip
|
||||
key={app._id}
|
||||
label={app.permission.hasWritePer ? appT('To Settings') : appT('To Chat')}
|
||||
>
|
||||
<Box
|
||||
lineHeight={1.5}
|
||||
h={'100%'}
|
||||
py={3}
|
||||
px={5}
|
||||
cursor={'pointer'}
|
||||
borderWidth={'1.5px'}
|
||||
borderColor={'borderColor.low'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
userSelect={'none'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .delete': {
|
||||
display: 'flex'
|
||||
},
|
||||
'& .chat': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (app.permission.hasWritePer) {
|
||||
router.push(`/app/detail?appId=${app._id}`);
|
||||
} else {
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={app.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3}>{app.name}</Box>
|
||||
{app.permission.isOwner && (
|
||||
<IconButton
|
||||
className="delete"
|
||||
position={'absolute'}
|
||||
top={4}
|
||||
right={4}
|
||||
size={'xsSquare'}
|
||||
variant={'whiteDanger'}
|
||||
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||
aria-label={'delete'}
|
||||
display={['', 'none']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openConfirm(() => onclickDelApp(app._id))();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Box
|
||||
flex={1}
|
||||
className={'textEllipsis3'}
|
||||
py={2}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{app.intro || '这个应用还没写介绍~'}
|
||||
</Box>
|
||||
<Flex h={'34px'} alignItems={'flex-end'}>
|
||||
<Box flex={1}>
|
||||
<PermissionIconText
|
||||
defaultPermission={app.defaultPermission}
|
||||
color={'myGray.600'}
|
||||
/>
|
||||
</Box>
|
||||
{app.permission.hasWritePer && (
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'xsSquare'}
|
||||
variant={'whitePrimary'}
|
||||
icon={
|
||||
<MyTooltip label={'去聊天'}>
|
||||
<MyIcon name={'core/chat/chatLight'} w={'14px'} />
|
||||
</MyTooltip>
|
||||
}
|
||||
aria-label={'chat'}
|
||||
display={['', 'none']}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/chat?appId=${app._id}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{myApps.length === 0 && <EmptyTip text={'还没有应用,快去创建一个吧!'} pt={'30vh'} />}
|
||||
<ConfirmModal />
|
||||
{isOpenCreateModal && (
|
||||
<CreateModal onClose={onCloseCreateModal} onSuccess={() => loadMyApps()} />
|
||||
{!!editFolder && (
|
||||
<EditFolderModal
|
||||
{...editFolder}
|
||||
onClose={() => setEditFolder(undefined)}
|
||||
onCreate={(data) => onCreateFolder({ ...data, parentId })}
|
||||
onEdit={({ id, ...data }) => onUpdateApp(id, data)}
|
||||
/>
|
||||
)}
|
||||
{isOpenCreateModal && <CreateModal onClose={onCloseCreateModal} />}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
function ContextRender() {
|
||||
return (
|
||||
<AppListContextProvider>
|
||||
<MyApps />
|
||||
</AppListContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextRender;
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
@@ -194,5 +211,3 @@ export async function getServerSideProps(content: any) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MyApps;
|
||||
|
@@ -108,7 +108,7 @@ const Test = ({ datasetId }: { datasetId: string }) => {
|
||||
usingReRank: res.usingReRank,
|
||||
limit: res.limit,
|
||||
similarity: res.similarity,
|
||||
usingQueryExtension: res.usingQueryExtension
|
||||
queryExtensionModel: res.queryExtensionModel
|
||||
};
|
||||
pushDatasetTestItem(testItem);
|
||||
setDatasetTestItem(testItem);
|
||||
@@ -430,7 +430,7 @@ const TestResults = React.memo(function TestResults({
|
||||
similarity={datasetTestItem.similarity}
|
||||
limit={datasetTestItem.limit}
|
||||
usingReRank={datasetTestItem.usingReRank}
|
||||
usingQueryExtension={datasetTestItem.usingQueryExtension}
|
||||
queryExtensionModel={datasetTestItem.queryExtensionModel}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
@@ -143,7 +143,7 @@ function request(
|
||||
): any {
|
||||
/* 去空 */
|
||||
for (const key in data) {
|
||||
if (data[key] === null || data[key] === undefined) {
|
||||
if (data[key] === undefined) {
|
||||
delete data[key];
|
||||
}
|
||||
}
|
||||
|
@@ -1,28 +1,25 @@
|
||||
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
|
||||
import type {
|
||||
AppDetailType,
|
||||
AppListItemType,
|
||||
ChatInputGuideConfigType
|
||||
} from '@fastgpt/global/core/app/type.d';
|
||||
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
|
||||
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
|
||||
import { AppUpdateParams, CreateAppParams } from '@/global/core/app/api';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { AppUpdateParams } from '@/global/core/app/api';
|
||||
import type { CreateAppBody } from '@/pages/api/core/app/create';
|
||||
import type { ListAppBody } from '@/pages/api/core/app/list';
|
||||
|
||||
/**
|
||||
* 获取模型列表
|
||||
*/
|
||||
export const getMyApps = () => GET<AppListItemType[]>('/core/app/list');
|
||||
export const getMyApps = (data?: ListAppBody) => POST<AppListItemType[]>('/core/app/list', data);
|
||||
|
||||
/**
|
||||
* 创建一个模型
|
||||
*/
|
||||
export const postCreateApp = (data: CreateAppParams) => POST<string>('/core/app/create', data);
|
||||
export const postCreateApp = (data: CreateAppBody) => POST<string>('/core/app/create', data);
|
||||
|
||||
export const getMyAppsByTags = (data: {}) => POST(`/proApi/core/chat/team/getApps`, data);
|
||||
/**
|
||||
* 根据 ID 删除模型
|
||||
*/
|
||||
export const delModelById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
|
||||
export const delAppById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
|
||||
|
||||
/**
|
||||
* 根据 ID 获取模型
|
||||
|
11
projects/app/src/web/core/app/api/app.ts
Normal file
11
projects/app/src/web/core/app/api/app.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DELETE, GET, POST } from '@/web/common/api/request';
|
||||
import type { CreateAppFolderBody } from '@/pages/api/core/app/folder/create';
|
||||
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
|
||||
/* folder */
|
||||
export const postCreateAppFolder = (data: CreateAppFolderBody) =>
|
||||
POST('/core/app/folder/create', data);
|
||||
|
||||
export const getAppFolderPath = (parentId: ParentIdType) =>
|
||||
GET<ParentTreePathItemType[]>(`/core/app/folder/path`, { parentId });
|
@@ -1,3 +1,4 @@
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
|
||||
import type { FeishuType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
|
||||
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
|
||||
@@ -6,7 +7,7 @@ import { NullPermission } from '@fastgpt/global/support/permission/constant';
|
||||
export const defaultApp: AppDetailType = {
|
||||
_id: '',
|
||||
name: '应用加载中',
|
||||
type: 'simple',
|
||||
type: AppTypeEnum.simple,
|
||||
avatar: '/icon/logo.svg',
|
||||
intro: '',
|
||||
updateTime: Date.now(),
|
||||
|
@@ -1,31 +1,25 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { getMyApps } from '@/web/core/app/api';
|
||||
import { AppListItemType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
export type State = {
|
||||
myApps: AppListItemType[];
|
||||
loadMyApps: () => Promise<AppListItemType[]>;
|
||||
loadMyApps: (...arg: Parameters<typeof getMyApps>) => Promise<AppListItemType[]>;
|
||||
};
|
||||
|
||||
export const useAppStore = create<State>()(
|
||||
devtools(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
myApps: [],
|
||||
async loadMyApps() {
|
||||
const res = await getMyApps();
|
||||
set((state) => {
|
||||
state.myApps = res;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'appStore',
|
||||
partialize: (state) => ({})
|
||||
immer((set, get) => ({
|
||||
myApps: [],
|
||||
async loadMyApps(data) {
|
||||
const res = await getMyApps(data);
|
||||
set((state) => {
|
||||
state.myApps = res;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
)
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
export const appTemplates: (AppItemType & {
|
||||
avatar: string;
|
||||
intro: string;
|
||||
type: `${AppTypeEnum}`;
|
||||
type: AppTypeEnum;
|
||||
})[] = [
|
||||
{
|
||||
id: 'simpleChat',
|
||||
|
@@ -15,7 +15,7 @@ export type SearchTestStoreItemType = {
|
||||
limit: number;
|
||||
usingReRank: boolean;
|
||||
similarity: number;
|
||||
usingQueryExtension: boolean;
|
||||
queryExtensionModel?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
Reference in New Issue
Block a user