Permission (#1687)

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-06-04 17:52:00 +08:00
committed by GitHub
parent fcb915c988
commit 19c8a06d51
109 changed files with 2291 additions and 1091 deletions

View File

@@ -0,0 +1,49 @@
import { Box, BoxProps } from '@chakra-ui/react';
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';
export enum defaultPermissionEnum {
private = 'private',
read = 'read',
edit = 'edit'
}
type Props = Omit<BoxProps, 'onChange'> & {
per: PermissionValueType;
defaultPer: PermissionValueType;
readPer: PermissionValueType;
writePer: PermissionValueType;
onChange: (v: PermissionValueType) => void;
};
const DefaultPermissionList = ({
per,
defaultPer,
readPer,
writePer,
onChange,
...styles
}: Props) => {
const { t } = useTranslation();
const defaultPermissionSelectList = [
{ label: '仅协作者访问', value: defaultPer },
{ label: '团队可访问', value: readPer },
{ label: '团队可编辑', value: writePer }
];
return (
<Box {...styles}>
<MySelect
list={defaultPermissionSelectList}
value={per}
onchange={(v) => {
onChange(v);
}}
/>
</Box>
);
};
export default DefaultPermissionList;

View File

@@ -1,19 +1,34 @@
import React from 'react';
import React, { useMemo } from 'react';
import { PermissionTypeEnum, PermissionTypeMap } from '@fastgpt/global/support/permission/constant';
import { Box, Flex, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { Permission } from '@fastgpt/global/support/permission/controller';
const PermissionIconText = ({
permission,
defaultPermission,
...props
}: { permission: `${PermissionTypeEnum}` } & FlexProps) => {
}: {
permission?: `${PermissionTypeEnum}`;
defaultPermission?: PermissionValueType;
} & FlexProps) => {
const { t } = useTranslation();
return PermissionTypeMap[permission] ? (
const per = useMemo(() => {
if (permission) return permission;
if (defaultPermission) {
return new Permission({ per: defaultPermission }).hasReadPer ? 'public' : 'private';
}
return 'private';
}, [defaultPermission, permission]);
return PermissionTypeMap[per] ? (
<Flex alignItems={'center'} {...props}>
<MyIcon name={PermissionTypeMap[permission]?.iconLight as any} w={'14px'} />
<MyIcon name={PermissionTypeMap[per]?.iconLight as any} w={'14px'} />
<Box ml={'2px'} lineHeight={1}>
{t(PermissionTypeMap[permission]?.label)}
{t(PermissionTypeMap[per]?.label)}
</Box>
</Flex>
) : null;

View File

@@ -0,0 +1,216 @@
import {
Flex,
Box,
Grid,
ModalBody,
InputGroup,
InputLeftElement,
Input,
Checkbox,
ModalFooter,
Button,
useToast
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import MyAvatar from '@/components/Avatar';
import { useMemo, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
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';
export type AddModalPropsType = {
onClose: () => void;
};
export function AddMemberModal({ onClose }: AddModalPropsType) {
const toast = useToast();
const { userInfo } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPreLabelList } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const {
data: members = [],
refetch: refetchMembers,
isLoading: loadingMembers
} = useQuery(['getMembers', userInfo?.team?.teamId], async () => {
if (!userInfo?.team?.teamId) return [];
const members = await getTeamMembers();
return members;
});
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.permission.isOwner) return false;
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
});
}, [members, searchText, userInfo?.team?.tmbId]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPreLabelList(selectedPermission).join('、');
}, [getPreLabelList, selectedPermission]);
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
mutationFn: () => {
return onUpdateCollaborators(selectedMemberIdList, selectedPermission);
},
successToast: '添加成功',
errorToast: 'Error',
onSuccess() {
onClose();
}
});
return (
<MyModal isOpen onClose={onClose} iconSrc="modal/AddClb" title="添加协作者" minW="800px">
<ModalBody>
<MyBox
isLoading={loadingMembers}
display={'grid'}
minH="400px"
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="55% 45%"
>
<Flex
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
minH="200px"
>
<InputGroup alignItems="center" h="32px" my="2" py="1">
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder="搜索用户名"
fontSize="lg"
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
</InputGroup>
<Flex flexDirection="column" mt="2">
{filterMembers.map((member) => {
const onChange = () => {
if (selectedMemberIdList.includes(member.tmbId)) {
setSelectedMembers(selectedMemberIdList.filter((v) => v !== member.tmbId));
} else {
setSelectedMembers([...selectedMemberIdList, member.tmbId]);
}
};
const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId);
return (
<Flex
key={member.tmbId}
mt="1"
py="1"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
>
<Checkbox
size="lg"
mr="3"
isChecked={selectedMemberIdList.includes(member.tmbId)}
onChange={onChange}
/>
<Flex
flexDirection="row"
onClick={onChange}
w="full"
justifyContent="space-between"
>
<Flex flexDirection="row" alignItems="center">
<MyAvatar src={member.avatar} w="32px" />
<Box ml="2">{member.memberName}</Box>
</Flex>
{!!collaborator && <PermissionTags permission={collaborator.permission} />}
</Flex>
</Flex>
);
})}
</Flex>
</Flex>
<Flex p="4" flexDirection="column">
<Box>: {selectedMemberIdList.length}</Box>
<Flex flexDirection="column" mt="2">
{selectedMemberIdList.map((tmbId) => {
const member = filterMembers.find((v) => v.tmbId === tmbId);
return member ? (
<Flex
key={tmbId}
alignItems="center"
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="24px" />
<Box w="full" fontSize="lg">
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
/>
</Flex>
) : null;
})}
</Flex>
</Flex>
</MyBox>
</ModalBody>
<ModalFooter>
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{perLabel}
<ChevronDownIcon fontSize={'lg'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
</Button>
</ModalFooter>
</MyModal>
);
}

View File

@@ -0,0 +1,119 @@
import {
ModalBody,
Table,
TableContainer,
Tbody,
Th,
Thead,
Tr,
Td,
Box,
Flex
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
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 { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
export type ManageModalProps = {
onClose: () => void;
};
function ManageModal({ onClose }: ManageModalProps) {
const { userInfo } = useUserStore();
const { collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
const { mutate: onDelete, isLoading: isDeleting } = useRequest({
mutationFn: (tmbId: string) => onDelOneCollaborator(tmbId)
});
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
return onUpdateCollaborators([tmbId], per);
},
successToast: '更新成功',
errorToast: 'Error'
});
const loading = isDeleting || isUpdating;
return (
<MyModal
isLoading={loading}
isOpen
onClose={onClose}
minW="600px"
title="管理协作者"
iconSrc="common/settingLight"
>
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
<Thead bg="myGray.100">
<Tr>
<Th border="none"></Th>
<Th border="none"></Th>
<Th border="none"></Th>
</Tr>
</Thead>
<Tbody>
{collaboratorList?.map((item) => {
return (
<Tr
key={item.tmbId}
_hover={{
bg: 'myGray.50'
}}
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
{item.name}
</Flex>
</Td>
<Td border="none">
<PermissionTags permission={item.permission} />
</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);
}}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={'暂无协作者'} />}
</TableContainer>
</ModalBody>
</MyModal>
);
}
export default ManageModal;

View File

@@ -0,0 +1,243 @@
import {
ButtonProps,
Flex,
Menu,
MenuButton,
MenuList,
Box,
Radio,
useOutsideClick
} from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useContextSelector } from 'use-context-selector';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next';
import MyDivider from '@fastgpt/web/components/common/MyDivider';
export type PermissionSelectProps = {
value?: PermissionValueType;
onChange: (value: PermissionValueType) => void;
trigger?: 'hover' | 'click';
offset?: [number, number];
Button: React.ReactNode;
onDelete?: () => void;
} & Omit<ButtonProps, 'onChange' | 'value'>;
const MenuStyle = {
py: 2,
px: 3,
_hover: {
bg: 'myGray.50'
},
borderRadius: 'md',
cursor: 'pointer',
fontSize: 'sm'
};
function PermissionSelect({
value,
onChange,
trigger = 'click',
offset = [0, 5],
Button,
width = 'auto',
onDelete,
...props
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
const list = Object.entries(permissionList).map(([key, value]) => {
return {
name: value.name,
value: value.value,
description: value.description,
checkBoxType: value.checkBoxType
};
});
return {
singleCheckBoxList: list.filter((item) => item.checkBoxType === 'single'),
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
};
}, [permissionList]);
const selectedSingleValue = useMemo(() => {
const per = new Permission({ per: value });
if (per.hasManagePer) return permissionList['manage'].value;
if (per.hasWritePer) return permissionList['write'].value;
return permissionList['read'].value;
}, [permissionList, value]);
const selectedMultipleValues = useMemo(() => {
const per = new Permission({ per: value });
return permissionSelectList.multipleCheckBoxList
.filter((item) => {
return per.checkPer(item.value);
})
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
useOutsideClick({
ref: ref,
handler: () => {
setIsOpen(false);
}
});
return (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
ref={ref}
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
}
clearTimeout(closeTimer.current);
}}
onMouseLeave={() => {
if (trigger === 'hover') {
closeTimer.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}
}}
>
<Box
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
setIsOpen(!isOpen);
}
}}
>
<MenuButton
w={'100%'}
h={'100%'}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
/>
<Box position={'relative'} cursor={'pointer'} userSelect={'none'}>
{Button}
</Box>
</Box>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p="3"
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
}
zIndex={99}
overflowY={'auto'}
>
{/* The list of single select permissions */}
{permissionSelectList.singleCheckBoxList.map((item) => {
const change = () => {
const per = new Permission({ per: value });
per.removePer(selectedSingleValue);
per.addPer(item.value);
onChange(per.value);
setIsOpen(false);
};
return (
<Flex
key={item.value}
{...(selectedSingleValue === item.value
? {
color: 'primary.600'
}
: {})}
{...MenuStyle}
onClick={change}
maxW={['70vw', '300px']}
>
<Radio size="lg" isChecked={selectedSingleValue === item.value} />
<Box ml={4}>
<Box>{item.name}</Box>
<Box color={'myGray.500'}>{item.description}</Box>
</Box>
</Flex>
);
})}
{/* <MyDivider my={3} />
{multipleValues.length > 0 && <Box m="4">其他权限(多选)</Box>} */}
{/* The list of multiple select permissions */}
{/* {list
.filter((item) => item.type === 'multiple')
.map((item) => {
const change = () => {
if (checkPermission(valueState, item.value)) {
setValueState(new Permission(valueState).remove(item.value).value);
} else {
setValueState(new Permission(valueState).add(item.value).value);
}
};
return (
<Flex
key={item.value}
{...(checkPermission(valueState, item.value)
? {
color: 'primary.500',
bg: 'myWhite.300'
}
: {})}
whiteSpace="pre-wrap"
flexDirection="row"
justifyContent="start"
p="2"
_hover={{
bg: 'myGray.50'
}}
>
<Checkbox
size="lg"
isChecked={checkPermission(valueState, item.value)}
onChange={change}
/>
<Flex px="4" flexDirection="column" onClick={change}>
<Box fontWeight="500">{item.name}</Box>
<Box fontWeight="400">{item.description}</Box>
</Flex>
</Flex>
);
})}*/}
{onDelete && (
<>
<MyDivider my={2} h={'2px'} borderColor={'myGray.200'} />
<Flex
{...MenuStyle}
onClick={() => {
onDelete();
setIsOpen(false);
}}
>
<MyIcon name="delete" w="20px" color="red.600" />
<Box color="red.600">{t('common.Delete')}</Box>
</Flex>
</>
)}
</MenuList>
</Box>
</Menu>
);
}
export default React.memo(PermissionSelect);

View File

@@ -0,0 +1,36 @@
import { Flex } from '@chakra-ui/react';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Tag from '@fastgpt/web/components/common/Tag';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
export type PermissionTagsProp = {
permission: PermissionValueType;
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPreLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const perTagList = getPreLabelList(permission);
return (
<Flex gap="2" alignItems="center">
{perTagList.map((item) => (
<Tag
mixBlendMode={'multiply'}
key={item}
colorSchema="blue"
border="none"
py={2}
px={3}
fontSize={'xs'}
>
{item}
</Tag>
))}
</Flex>
);
}
export default PermissionTags;

View File

@@ -0,0 +1,107 @@
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useQuery } from '@tanstack/react-query';
import { ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
export type MemberManagerInputPropsType = {
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (tmbIds: string[], permission: PermissionValueType) => any;
onDelOneCollaborator: (tmbId: string) => any;
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void;
isFetchingCollaborator: boolean;
getPreLabelList: (per: PermissionValueType) => string[];
};
type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
throw new Error('Function not implemented.');
},
getPreLabelList: function (): string[] {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false
});
export const CollaboratorContextProvider = ({
onGetCollaboratorList,
permissionList,
onUpdateCollaborators,
onDelOneCollaborator,
children
}: MemberManagerInputPropsType & {
children: 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) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const getPreLabelList = useCallback(
(per: PermissionValueType) => {
const Per = new Permission({ per });
const labels: string[] = [];
if (Per.hasManagePer) {
labels.push(permissionList['manage'].name);
} else if (Per.hasWritePer) {
labels.push(permissionList['write'].name);
} else {
labels.push(permissionList['read'].name);
}
Object.values(permissionList).forEach((item) => {
if (item.checkBoxType === 'multiple') {
if (Per.checkPer(item.value)) {
labels.push(item.name);
}
}
});
return labels;
},
[permissionList]
);
const contextValue = {
onGetCollaboratorList,
collaboratorList,
refetchCollaboratorList,
isFetchingCollaborator,
permissionList,
onUpdateCollaborators: onUpdateCollaboratorsThen,
onDelOneCollaborator: onDelOneCollaboratorThem,
getPreLabelList
};
return (
<CollaboratorContext.Provider value={contextValue}>{children}</CollaboratorContext.Provider>
);
};

View File

@@ -0,0 +1,99 @@
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></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);