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:
Archer
2024-06-11 10:16:24 +08:00
committed by GitHub
parent b20d075d35
commit bc6864c3dc
89 changed files with 2495 additions and 695 deletions

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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">

View File

@@ -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>
);
};

View File

@@ -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);