Group role (#2993)

* feat: app/dataset support group (#2898)

* pref: member-group (#2862)

* feat: group list ordered by updateTime

* fix: transfer ownership of group when deleting member

* fix: i18n fix

* feat: can not set member as admin/owner when user is not active

* fix: GroupInfoModal hover input do not change color

* fix(fe): searchinput do not scroll

* feat: app collaborator with group, remove default permission

* feat: dataset collaborator with group, remove default permission

* chore(test): pref mock

* chore: remove useless code

* chore: adjust

* fix: add self as collaborator when creating folder

* fix(fe): folder manage menu do not show when user has write permission
only

* fix: dataset folder create

* feat: Add code comment

* Pref: app move (#2952)

* perf: app schema

* doc

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-10-25 19:39:11 +08:00
committed by GitHub
parent 74d58d562b
commit f89452acdd
60 changed files with 1142 additions and 1094 deletions

View File

@@ -1,7 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { Box, Flex, Input } from '@chakra-ui/react';
import { delDatasetById } from '@/web/core/dataset/api';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useForm } from 'react-hook-form';
@@ -10,7 +8,7 @@ import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { postRebuildEmbedding } from '@/web/core/dataset/api';
@@ -21,12 +19,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider/index';
import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import MemberManager from '../../component/MemberManager';
import {
getCollaboratorList,
@@ -39,7 +33,6 @@ import { EditResourceInfoFormType } from '@/components/common/Modal/EditResource
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
const Info = ({ datasetId }: { datasetId: string }) => {
const router = useRouter();
const [openBaseConfig, setOpenBaseConfig] = useState(true);
const [openPermissionConfig, setOpenPermissionConfig] = useState(true);
const { t } = useTranslation();
@@ -56,10 +49,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
const vectorModel = watch('vectorModel');
const agentModel = watch('agentModel');
const defaultPermission = watch('defaultPermission');
const { datasetModelList, vectorModelList } = useSystemStore();
const { openConfirm: onOpenConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
const { ConfirmModal: ConfirmDelModal } = useConfirm({
content: t('common:core.dataset.Delete Confirm'),
type: 'delete'
});
@@ -69,30 +61,17 @@ const Info = ({ datasetId }: { datasetId: string }) => {
type: 'delete'
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const { File } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
/* 点击删除 */
const { mutate: onclickDelete, isLoading: isDeleting } = useRequest({
mutationFn: () => {
return delDatasetById(datasetId);
},
onSuccess() {
router.replace(`/dataset/list`);
},
successToast: t('common:common.Delete Success'),
errorToast: t('common:common.Delete Failed')
});
const { runAsync: onSave, loading: isSaving } = useRequest2(
const { runAsync: onSave } = useRequest2(
(data: DatasetItemType) => {
return updateDataset({
id: datasetId,
agentModel: data.agentModel,
externalReadUrl: data.externalReadUrl,
defaultPermission: data.defaultPermission
externalReadUrl: data.externalReadUrl
});
},
{
@@ -101,7 +80,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
}
);
const { runAsync: onSelectFile, loading: isSelecting } = useRequest2(
const { runAsync: onSelectFile } = useRequest2(
(e: File[]) => {
const file = e[0];
if (!file) return Promise.resolve(null);
@@ -122,7 +101,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
}
);
const { runAsync: onRebuilding, loading: isRebuilding } = useRequest2(
const { runAsync: onRebuilding } = useRequest2(
(vectorModel: VectorModelItemType) => {
return postRebuildEmbedding({
datasetId,
@@ -242,10 +221,9 @@ const Info = ({ datasetId }: { datasetId: string }) => {
onchange={(e) => {
const vectorModel = vectorModelList.find((item) => item.model === e);
if (!vectorModel) return;
return onOpenConfirmRebuild(() => {
return onRebuilding(vectorModel).then(() => {
setValue('vectorModel', vectorModel);
});
return onOpenConfirmRebuild(async () => {
await onRebuilding(vectorModel);
setValue('vectorModel', vectorModel);
})();
}}
/>
@@ -326,20 +304,12 @@ const Info = ({ datasetId }: { datasetId: string }) => {
<FormLabel fontWeight={'500'} fontSize={'mini'} pb={3} userSelect={'none'}>
{t('common:permission.Default permission')}
</FormLabel>
<DefaultPermissionList
fontSize={'mini'}
per={defaultPermission}
defaultPer={DatasetDefaultPermissionVal}
onChange={(v) => {
setValue('defaultPermission', v);
return handleSubmit((data) => onSave({ ...data, defaultPermission: v }))();
}}
/>
</Box>
<Box py={4}>
<MemberManager
managePer={{
mode: 'all',
permission: datasetDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(datasetId),
permissionList: DatasetPermissionList,
@@ -348,11 +318,19 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body,
datasetId
}),
onDelOneCollaborator: (tmbId) =>
deleteDatasetCollaborators({
datasetId,
tmbId
})
onDelOneCollaborator: async ({ groupId, tmbId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId,
tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId,
groupId
});
}
}
}}
/>
</Box>

View File

@@ -18,10 +18,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { DatasetsContext } from '../context';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import ConfigPerModal from '@/components/support/permission/ConfigPerModal';
import {
deleteDatasetCollaborators,
@@ -34,7 +31,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { useI18n } from '@/web/context/I18n';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag';
@@ -42,7 +38,6 @@ const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditRe
function List() {
const { setLoading } = useSystemStore();
const { toast } = useToast();
const { isPc } = useSystem();
const { t } = useTranslation();
const { commonT } = useI18n();
@@ -59,21 +54,32 @@ function List() {
folderDetail
} = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetIndex, setEditPerDatasetIndex] = useState<number>();
const [loadingDatasetId, setLoadingDatasetId] = useState<string>();
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const parentDataset = useMemo(
() => myDatasets.find((item) => String(item._id) === parentId),
[parentId, myDatasets]
);
const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({
type: 'common',
title: t('common:move.confirm'),
content: t('dataset:move.hint')
});
const { runAsync: updateDataset } = useRequest2(onUpdateDataset);
const { getBoxProps } = useFolderDrag({
activeStyles: {
borderColor: 'primary.600'
},
onDrop: async (dragId: string, targetId: string) => {
setLoadingDatasetId(dragId);
try {
await onUpdateDataset({
onDrop: (dragId: string, targetId: string) => {
openMoveConfirm(() =>
updateDataset({
id: dragId,
parentId: targetId
});
} catch (error) {}
setLoadingDatasetId(undefined);
})
)();
}
});
@@ -86,10 +92,6 @@ function List() {
[editPerDatasetIndex, myDatasets]
);
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const { mutate: exportDataset } = useRequest({
mutationFn: async (dataset: DatasetItemType) => {
setLoading(true);
@@ -100,15 +102,10 @@ function List() {
filename: `${dataset.name}.csv`
});
},
onSuccess() {
toast({
status: 'success',
title: t('common:core.dataset.Start export')
});
},
onSettled() {
setLoading(false);
},
successToast: t('common:core.dataset.Start export'),
errorToast: t('common:dataset.Export Dataset Limit Error')
});
@@ -176,7 +173,6 @@ function List() {
}
>
<MyBox
isLoading={loadingDatasetId === dataset._id}
display={'flex'}
flexDirection={'column'}
lineHeight={1.5}
@@ -278,8 +274,8 @@ function List() {
</HStack>
)}
<PermissionIconText
private={dataset.private}
iconColor="myGray.400"
defaultPermission={dataset.defaultPermission}
color={'myGray.500'}
/>
</HStack>
@@ -293,7 +289,9 @@ function List() {
</Box>
</HStack>
)}
{dataset.permission.hasWritePer && (
{(dataset.type === DatasetTypeEnum.folder
? dataset.permission.hasManagePer
: dataset.permission.hasWritePer) && (
<Box
className="more"
display={['', 'none']}
@@ -336,11 +334,18 @@ function List() {
avatar: dataset.avatar
})
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMoveDatasetId(dataset._id)
},
...((parentDataset ? parentDataset : dataset)?.permission
.hasManagePer
? [
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => {
setMoveDatasetId(dataset._id);
}
}
]
: []),
...(dataset.permission.hasManagePer
? [
{
@@ -427,36 +432,20 @@ function List() {
}
avatar={editPerDataset.avatar}
name={editPerDataset.name}
defaultPer={{
value: editPerDataset.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) =>
onUpdateDataset({
id: editPerDataset._id,
defaultPermission: e
})
}}
managePer={{
mode: 'all',
permission: editPerDataset.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove default value after group is ready
permission
}: {
members?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
members,
permission,
onUpdateCollaborators: (props) =>
postUpdateDatasetCollaborators({
...props,
datasetId: editPerDataset._id
});
},
onDelOneCollaborator: (tmbId: string) =>
}),
onDelOneCollaborator: async (props) =>
deleteDatasetCollaborators({
datasetId: editPerDataset._id,
tmbId
...props,
datasetId: editPerDataset._id
}),
refreshDeps: [editPerDataset._id, editPerDataset.inheritPermission]
}}
@@ -464,6 +453,7 @@ function List() {
/>
)}
<ConfirmModal />
<MoveConfirmModal />
</>
);
}

View File

@@ -1,186 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Card,
Flex,
Box,
Button,
ModalBody,
ModalHeader,
ModalFooter,
useTheme,
Grid
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { getDatasets, putDatasetById, getDatasetPaths } from '@/web/core/dataset/api';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
const MoveModal = ({
onClose,
onSuccess,
moveDataId
}: {
onClose: () => void;
onSuccess: () => void;
moveDataId: string;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [parentId, setParentId] = useState<string>('');
const { data } = useQuery(['getDatasets', parentId], () => {
return Promise.all([
getDatasets({ parentId, type: DatasetTypeEnum.folder }),
getDatasetPaths(parentId)
]);
});
const paths = useMemo(
() => [
{
parentId: '',
parentName: t('common:core.dataset.My Dataset')
},
...(data?.[1] || [])
],
[data, t]
);
const folderList = useMemo(
() => (data?.[0] || []).filter((item) => item._id !== moveDataId),
[moveDataId, data]
);
const { mutate, isLoading } = useRequest({
mutationFn: () => putDatasetById({ id: moveDataId, parentId }),
onSuccess,
errorToast: t('common:dataset.Move Failed')
});
return (
<MyModal
isOpen={true}
maxW={['90vw', '800px']}
w={'800px'}
iconSrc="/imgs/modal/move.svg"
title={
<>
{!!parentId ? (
<Flex flex={1} userSelect={'none'} fontSize={['sm', 'md']} fontWeight={'normal'}>
{paths.map((item, i) => (
<Flex key={item.parentId} mr={2} alignItems={'center'}>
<Box
borderRadius={'md'}
{...(i === paths.length - 1
? {
cursor: 'default'
}
: {
cursor: 'pointer',
_hover: {
color: 'primary.500'
},
onClick: () => {
setParentId(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== paths.length - 1 && (
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
)}
</Flex>
))}
</Flex>
) : (
<Box>{t('common:core.dataset.My Dataset')}</Box>
)}
</>
}
onClose={onClose}
>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
userSelect={'none'}
>
<Grid
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
>
{folderList.map((item) =>
(() => {
return (
<MyTooltip
key={item._id}
label={
item.type === DatasetTypeEnum.dataset
? t('common:dataset.Select Dataset')
: t('common:dataset.Select Folder')
}
>
<Card
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
onClick={() => {
setParentId(item._id);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>
<Box
className="textEllipsis"
ml={3}
fontWeight={'bold'}
fontSize={['md', 'md']}
>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
{item.type === DatasetTypeEnum.folder ? (
<Box color={'myGray.500'}>{t('common:Folder')}</Box>
) : (
<>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</>
)}
</Flex>
</Card>
</MyTooltip>
);
})()
)}
</Grid>
{folderList.length === 0 && (
<EmptyTip text={t('common:common.folder.No Folder')}></EmptyTip>
)}
</ModalBody>
<ModalFooter>
<Button isLoading={isLoading} onClick={mutate}>
{t('common:dataset.Confirm move the folder')}
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default MoveModal;

View File

@@ -13,7 +13,6 @@ import {
import { useRouter } from 'next/router';
import React, { useCallback, useState } from 'react';
import { createContext } from 'use-context-selector';
import { useI18n } from '@/web/context/I18n';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api';
import dynamic from 'next/dynamic';
@@ -68,7 +67,6 @@ export const DatasetsContext = createContext<DatasetContextType>({
function DatasetContextProvider({ children }: { children: React.ReactNode }) {
const router = useRouter();
const { commonT } = useI18n();
const { t } = useTranslation();
const [moveDatasetId, setMoveDatasetId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
@@ -127,10 +125,12 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
parentId,
type: DatasetTypeEnum.folder
})
).map((item) => ({
id: item._id,
name: item.name
}));
)
.filter((item) => item.permission.hasManagePer)
.map((item) => ({
id: item._id,
name: item.name
}));
}, []);
const [editedDataset, setEditedDataset] = useState<EditResourceInfoFormType>();
@@ -164,9 +164,10 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
<MoveModal
moveResourceId={moveDatasetId}
server={getDatasetFolderList}
title={commonT('Move')}
title={t('common:Move')}
onClose={() => setMoveDatasetId(undefined)}
onConfirm={onMoveDataset}
onConfirm={(parentId) => onMoveDataset(parentId)}
moveHint={t('dataset:move.hint')}
/>
)}
</DatasetsContext.Provider>

View File

@@ -17,10 +17,7 @@ import { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditF
import dynamic from 'next/dynamic';
import { postCreateDatasetFolder, resumeInheritPer } from '@/web/core/dataset/api';
import FolderSlideCard from '@/components/common/folder/SlideCard';
import {
DatasetDefaultPermissionVal,
DatasetPermissionList
} from '@fastgpt/global/support/permission/dataset/constant';
import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant';
import {
postUpdateDatasetCollaborators,
deleteDatasetCollaborators,
@@ -52,7 +49,6 @@ const Dataset = () => {
loadMyDatasets,
refetchFolderDetail,
folderDetail,
setEditedDataset,
setMoveDatasetId,
onDelDataset,
onUpdateDataset,
@@ -228,38 +224,39 @@ const Dataset = () => {
});
})
}
defaultPer={{
value: folderDetail.defaultPermission,
defaultValue: DatasetDefaultPermissionVal,
onChange: (e) => {
return onUpdateDataset({
id: folderDetail._id,
defaultPermission: e
});
}
}}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList,
onUpdateCollaborators: ({
members = [], // TODO: remove the default value after group is ready
members,
groups,
permission
}: {
members?: string[];
groups?: string[];
permission: number;
}) => {
return postUpdateDatasetCollaborators({
}) =>
postUpdateDatasetCollaborators({
members,
groups,
permission,
datasetId: folderDetail._id
});
},
onDelOneCollaborator: (tmbId: string) =>
deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
}),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
tmbId
});
} else if (groupId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
groupId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission]
}}
/>