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

@@ -1,103 +0,0 @@
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
function MemberTable() {
const members = useContextSelector(TeamContext, (v) => v.members);
const openRemoveMember = useContextSelector(TeamContext, (v) => v.openRemoveMember);
const onRemoveMember = useContextSelector(TeamContext, (v) => v.onRemoveMember);
const { userInfo } = useUserStore();
const { t } = useTranslation();
return (
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th>{t('common.Action')}</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{hasManage(
members.find((item) => item.tmbId === userInfo?.team.tmbId)?.permission!
) &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<MyMenu
width={20}
trigger="hover"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
);
}
export default MemberTable;

View File

@@ -1,72 +1,94 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import RowTabs from '../../../../common/Rowtabs';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DragHandleIcon } from '@chakra-ui/icons';
import MemberTable from './MemberTable';
import PermissionManage from './PermissionManage';
import MemberTable from './components/MemberTable';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useI18n } from '@/web/context/I18n';
type TabListType = Pick<React.ComponentProps<typeof RowTabs>, 'list'>['list'];
import { TeamModalContext } from './context';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import dynamic from 'next/dynamic';
enum TabListEnum {
member = 'member',
permission = 'permission'
}
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
function TeamCard() {
const { toast } = useToast();
const { t } = useTranslation();
const { userT } = useI18n();
const members = useContextSelector(TeamContext, (v) => v.members);
const onOpenInvite = useContextSelector(TeamContext, (v) => v.onOpenInvite);
const onOpenTeamTagsAsync = useContextSelector(TeamContext, (v) => v.onOpenTeamTagsAsync);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const onLeaveTeam = useContextSelector(TeamContext, (v) => v.onLeaveTeam);
const { myTeams, refetchTeams, members, refetchMembers, setEditTeamData, onSwitchTeam } =
useContextSelector(TeamModalContext, (v) => v);
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('user.team.member.Confirm Leave')
});
const { feConfigs } = useSystemStore();
const Tablist: TabListType = [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
];
onSuccess() {
refetchTeams();
},
errorToast: t('user.team.Leave Team Failed')
});
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const Tablist = useMemo(
() => [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
],
[members.length, t]
);
const [tab, setTab] = useState(Tablist[0].value);
const [tab, setTab] = useState<string>(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
h={'100%'}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
@@ -107,41 +129,38 @@ function TeamCard() {
list={Tablist}
value={tab}
onChange={(v) => {
setTab(v as string);
setTab(v as TabListEnum);
}}
></RowTabs>
{/* ctrl buttons */}
<Flex alignItems={'center'}>
{hasManage(
members.find((item) => item.tmbId.toString() === userInfo?.team.tmbId.toString())
?.permission!
) &&
tab === 'member' && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo?.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && (
{tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="sm"
@@ -155,13 +174,14 @@ function TeamCard() {
{t('user.team.Team Tags Async')}
</Button>
)}
{userInfo?.team.role !== TeamMemberRoleEnum.owner && (
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
@@ -173,8 +193,18 @@ function TeamCard() {
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === 'member' ? <MemberTable /> : <PermissionManage />}
{tab === TabListEnum.member && <MemberTable />}
{tab === TabListEnum.permission && <PermissionManage />}
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmLeaveTeamModal />
</Flex>
);

View File

@@ -3,20 +3,15 @@ import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import EditModal, { defaultForm } from './EditModal';
import { defaultForm } from './components/EditInfoModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { TeamModalContext } from './context';
function TeamList() {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const editTeamData = useContextSelector(TeamContext, (v) => v.editTeamData);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const myTeams = useContextSelector(TeamContext, (v) => v.myTeams);
const refetchTeam = useContextSelector(TeamContext, (v) => v.refetchTeam);
const onSwitchTeam = useContextSelector(TeamContext, (v) => v.onSwitchTeam);
// get the list of teams
const { userInfo } = useUserStore();
const { myTeams, onSwitchTeam, setEditTeamData } = useContextSelector(TeamModalContext, (v) => v);
return (
<Flex
@@ -100,16 +95,6 @@ function TeamList() {
</Flex>
))}
</Box>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
</Flex>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
@@ -14,7 +14,7 @@ import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
export type FormDataType = CreateTeamProps & {
export type EditTeamFormDataType = CreateTeamProps & {
id?: string;
};
@@ -28,17 +28,17 @@ function EditModal({
onClose,
onSuccess
}: {
defaultData?: FormDataType;
defaultData?: EditTeamFormDataType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const { register, setValue, getValues, handleSubmit } = useForm<CreateTeamProps>({
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
defaultValues: defaultData
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
@@ -57,7 +57,6 @@ function EditModal({
maxH: 300
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.Select File Failed')),
@@ -80,7 +79,7 @@ function EditModal({
errorToast: t('common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: FormDataType) => {
mutationFn: async (data: EditTeamFormDataType) => {
if (!data.id) return Promise.resolve('');
return putUpdateTeam({
name: data.name,
@@ -110,7 +109,7 @@ function EditModal({
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}

View File

@@ -0,0 +1,116 @@
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamModalContext } from '../context';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { delRemoveMember } from '@/web/support/user/team/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
function MemberTable() {
const { userInfo } = useUserStore();
const { t } = useTranslation();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({});
const { mutate: onRemoveMember, isLoading: isRemovingMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
refetchMembers();
},
successToast: t('user.team.Remove Member Success'),
errorToast: t('user.team.Remove Member Failed')
});
return (
<MyBox isLoading={isRemovingMember}>
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th>{t('common.Action')}</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<MyMenu
width={20}
trigger="hover"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
);
}
export default MemberTable;

View File

@@ -14,43 +14,30 @@ import {
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import {
hasManage,
constructPermission,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { TeamModalContext } from '../../context';
import { useI18n } from '@/web/context/I18n';
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const { t } = useTranslation();
const { userT } = useI18n();
const { userInfo } = useUserStore();
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const members = useContextSelector(TeamContext, (v) =>
v.members.filter((member) => {
return member.tmbId != userInfo!.team.tmbId && !hasManage(member.permission);
})
);
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const [selected, setSelected] = useState<typeof members>([]);
const [search, setSearch] = useState<string>('');
const [searched, setSearched] = useState<typeof members>(members);
const [searchKey, setSearchKey] = useState('');
const { mutate: submit, isLoading } = useRequest({
mutationFn: async () => {
console.log(selected);
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([
PermissionList['Read'],
PermissionList['Write'],
PermissionList['Manage']
]).value,
permission: ManagePermissionVal,
memberIds: selected.map((item) => {
return item.tmbId;
})
@@ -63,18 +50,23 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
successToast: '成功',
errorToast: '失败'
});
const filterMembers = useMemo(() => {
return members.filter((member) => {
if (member.permission.isOwner) return false;
if (!searchKey) return true;
return !!member.memberName.includes(searchKey);
});
}, [members, searchKey]);
return (
<MyModal
isOpen
iconSrc={'support/permission/collaborator'}
iconSrc={'modal/AddClb'}
maxW={['90vw']}
minW={['900px']}
overflow={'unset'}
title={
<Box>
<Box></Box>
</Box>
}
title={userT('team.Add manager')}
>
<ModalCloseButton onClick={onClose} />
<ModalBody py={6} px={10}>
@@ -95,15 +87,12 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
fontSize="lg"
bg={'myGray.50'}
onChange={(e) => {
setSearch(e.target.value);
setSearched(
members.filter((member) => member.memberName.includes(e.target.value))
);
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{searched.map((member) => {
{filterMembers.map((member) => {
return (
<Flex
py="2"
@@ -164,7 +153,7 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button h={'30px'} isLoading={isLoading} onClick={submit}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -1,26 +1,24 @@
import React from 'react';
import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { TeamContext } from '.';
import { useContextSelector } from 'use-context-selector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import AddManagerModal from './AddManager';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { delMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
constructPermission,
hasManage,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamModalContext } from '../../context';
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
const AddManagerModal = dynamic(() => import('./AddManager'));
function PermissionManage() {
const { t } = useTranslation();
const members = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const { userInfo } = useUserStore();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const {
isOpen: isOpenAddManager,
@@ -28,30 +26,19 @@ function PermissionManage() {
onClose: onCloseAddManager
} = useDisclosure();
const { mutate: removeManager } = useRequest({
const { mutate: removeManager, isLoading: isRemovingManager } = useRequest({
mutationFn: async (memberId: string) => {
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([PermissionList['Read'], PermissionList['Write']]).value,
memberIds: [memberId]
});
return delMemberPermission(memberId);
},
successToast: 'Success',
errorToast: 'Error',
successToast: '删除管理员成功',
errorToast: '删除管理员异常',
onSuccess: () => {
refetchMembers();
},
onError: () => {
refetchMembers();
}
});
return (
<Flex flexDirection={'column'} flex={'1'} h={['auto', '100%']} bg={'white'}>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
<MyBox h={'100%'} isLoading={isRemovingManager} bg={'white'}>
<Flex
mx={'5'}
flexDirection={'row'}
@@ -73,7 +60,7 @@ function PermissionManage() {
px={'3'}
borderRadius={'sm'}
>
,
{TeamPermissionList['manage'].description}
</Box>
</Flex>
{userInfo?.team.role === 'owner' && (
@@ -93,7 +80,7 @@ function PermissionManage() {
</Flex>
<Flex mt="4" mx="4">
{members.map((member) => {
if (hasManage(member.permission) && member.role !== TeamMemberRoleEnum.owner) {
if (member.permission.hasManagePer && !member.permission.isOwner) {
return (
<Tag key={member.memberName} mx={'2'} px="4" py="2" bg="myGray.100">
<Avatar src={member.avatar} w="20px" />
@@ -106,6 +93,7 @@ function PermissionManage() {
w="16px"
color="myGray.500"
cursor="pointer"
_hover={{ color: 'red.600' }}
onClick={() => {
removeManager(member.tmbId);
}}
@@ -116,7 +104,11 @@ function PermissionManage() {
}
})}
</Flex>
</Flex>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
</MyBox>
);
}

View File

@@ -0,0 +1,102 @@
import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './components/EditInfoModal';
import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import { getTeamList, getTeamMembers, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
const EditInfoModal = dynamic(() => import('./components/EditInfoModal'));
type TeamModalContextType = {
myTeams: TeamTmbItemType[];
refetchTeams: () => void;
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
members: TeamMemberItemType[];
refetchMembers: () => void;
};
export const TeamModalContext = createContext<TeamModalContextType>({
myTeams: [],
isLoading: false,
onSwitchTeam: function (teamId: string): void {
throw new Error('Function not implemented.');
},
setEditTeamData: function (value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
throw new Error('Function not implemented.');
},
members: [],
refetchTeams: function (): void {
throw new Error('Function not implemented.');
},
refetchMembers: function (): void {
throw new Error('Function not implemented.');
}
});
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeams
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
// member action
const {
data: members = [],
refetch: refetchMembers,
isLoading: loadingMembers
} = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers();
});
const { mutate: onSwitchTeam, isLoading: isSwitchingTeam } = useRequest({
mutationFn: async (teamId: string) => {
await putSwitchTeam(teamId);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers;
const contextValue = {
myTeams,
refetchTeams,
isLoading,
onSwitchTeam,
// create | update team
setEditTeamData,
members,
refetchMembers
};
return (
<TeamModalContext.Provider value={contextValue}>
{children}
{!!editTeamData && (
<EditInfoModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeams();
initUserInfo();
}}
/>
)}
</TeamModalContext.Provider>
);
};

View File

@@ -1,130 +1,24 @@
import React, { useMemo, useState } from 'react';
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import {
getTeamMembers,
delRemoveMember,
getTeamList,
delLeaveTeam,
putSwitchTeam
} from '@/web/support/user/team/api';
import { Box, useDisclosure } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import dynamic from 'next/dynamic';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { createContext } from 'use-context-selector';
import { createContext, useContextSelector } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
import { FormDataType } from './EditModal';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { setToken } from '@/web/support/user/auth';
import { TeamModalContext, TeamModalContextProvider } from './context';
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
export const TeamContext = createContext<{}>({} as any);
export const TeamContext = createContext<{
editTeamData?: FormDataType;
setEditTeamData: React.Dispatch<React.SetStateAction<any>>;
members: Awaited<ReturnType<typeof getTeamMembers>>;
myTeams: Awaited<ReturnType<typeof getTeamList>>;
refetchTeam: ReturnType<typeof useQuery>['refetch'];
onSwitchTeam: ReturnType<typeof useRequest>['mutate'];
refetchMembers: ReturnType<typeof useQuery>['refetch'];
openRemoveMember: ReturnType<typeof useConfirm>['openConfirm'];
onOpenInvite: ReturnType<typeof useDisclosure>['onOpen'];
onOpenTeamTagsAsync: ReturnType<typeof useDisclosure>['onOpen'];
onRemoveMember: ReturnType<typeof useRequest>['mutate'];
onLeaveTeam: ReturnType<typeof useRequest>['mutate'];
}>({} as any);
type Props = { onClose: () => void };
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const TeamManageModal = ({ onClose }: Props) => {
const { Loading } = useLoading();
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeam
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
const [editTeamData, setEditTeamData] = useState<FormDataType>();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
// member action
const { data: members = [], refetch: refetchMembers } = useQuery(
['getMembers', userInfo?.team?.teamId],
() => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers();
}
);
const { mutate: onRemoveMember, isLoading: isLoadingRemoveMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
refetchMembers();
},
successToast: t('user.team.Remove Member Success'),
errorToast: t('user.team.Remove Member Failed')
});
const defaultTeam = useMemo(
() => myTeams.find((item) => item.defaultTeam) || myTeams[0],
[myTeams]
);
const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
if (!teamId) return;
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
refetchTeam();
},
errorToast: t('user.team.Leave Team Failed')
});
return !!userInfo?.team ? (
<TeamContext.Provider
value={{
myTeams: myTeams,
refetchTeam: refetchTeam,
onSwitchTeam: onSwitchTeam,
members: members,
refetchMembers: refetchMembers,
openRemoveMember: openRemoveMember,
onOpenInvite: onOpenInvite,
onOpenTeamTagsAsync: onOpenTeamTagsAsync,
onRemoveMember: onRemoveMember,
editTeamData: editTeamData,
setEditTeamData: setEditTeamData,
onLeaveTeam: onLeaveTeam
}}
>
return (
<>
<MyModal
isOpen
onClose={onClose}
@@ -137,24 +31,23 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
<TeamList />
<TeamCard />
<Loading
loading={isLoadingRemoveMember || isLoadingTeams || isLoadingLeaveTeam || isSwitchTeam}
fixed={false}
/>
<Box h={'100%'} flex={'1 0 0'}>
<TeamCard />
</Box>
<Loading loading={isLoading} fixed={false} />
</Box>
</MyModal>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmRemoveMemberModal />
</TeamContext.Provider>
) : null;
</>
);
};
export default React.memo(TeamManageModal);
const Render = (props: Props) => {
const { userInfo } = useUserStore();
return !!userInfo?.team ? (
<TeamModalContextProvider>
<TeamManageModal {...props} />
</TeamModalContextProvider>
) : null;
};
export default React.memo(Render);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Image, useDisclosure, useTheme } from '@chakra-ui/react';
import { Box, Button, Flex, Image, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@/components/MyTooltip';
@@ -10,7 +10,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
const TeamMenu = () => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo } = useUserStore();
@@ -38,7 +37,7 @@ const TeamMenu = () => {
} else {
toast({
status: 'warning',
title: t('common.Business edition features')
title: t('common.system.Commercial version function')
});
}
}}