Update permission (#1522)

* Permission (#1442)

* Revert "lafAccount add pat & re request when token invalid (#76)" (#77)

This reverts commit 83d85dfe37adcaef4833385ea52ee79fd84720be.

* feat: add permission display in the team manager modal

* feat: add permission i18n

* feat: let team module acquire permission ablity

* feat: add ownerPermission property into metaData

* feat: team premission system

* feat: extract the resourcePermission from resource schemas

* fix: move enum definition to constant

* feat: auth member permission handler, invite user

* feat: permission manage

* feat: adjust the style

* feat: team card style
- add a new icon

* feat: team permission in guest mode

* chore: change the type

* chore: delete useless file

* chore: delete useless code

* feat: do not show owner in PermissionManage view

* chore: fix style

* fix: icon remove fill

* feat: adjust the codes

---------

Co-authored-by: Archer <545436317@qq.com>

* perf: permission modal

* lock

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-05-17 17:42:33 +08:00
committed by GitHub
parent 67c52992d7
commit 2f93dedfb6
30 changed files with 1079 additions and 377 deletions

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Flex, Box, BoxProps, border } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
type Props = BoxProps & {
list: {
icon?: string;
label: string | React.ReactNode;
value: string;
}[];
value: string;
onChange: (e: string) => void;
};
const RowTabs = ({ list, value, onChange, py = '7px', px = '12px', ...props }: Props) => {
return (
<Box display={'inline-flex'} px={'3px'} {...props}>
{list.map((item) => (
<Flex
key={item.value}
flex={'1 0 0'}
alignItems={'center'}
cursor={'pointer'}
px={px}
py={py}
userSelect={'none'}
whiteSpace={'noWrap'}
borderBottom={'2px solid'}
{...(value === item.value
? {
bg: 'white',
color: 'primary.600',
borderColor: 'primary.600'
}
: {
borderColor: 'myGray.100',
onClick: () => onChange(item.value)
})}
>
{item.icon && <MyIcon name={item.icon as any} mr={1} w={'14px'} />}
<Box>{item.label}</Box>
</Flex>
))}
</Box>
);
};
export default RowTabs;

View File

@@ -0,0 +1,174 @@
import {
Box,
Button,
ModalBody,
ModalCloseButton,
ModalFooter,
Grid,
Input,
Flex,
Checkbox,
CloseButton,
InputGroup,
InputLeftElement
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import { useTranslation } from 'react-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';
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const { t } = useTranslation();
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 [selected, setSelected] = useState<typeof members>([]);
const [search, setSearch] = useState<string>('');
const [searched, setSearched] = useState<typeof members>(members);
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,
memberIds: selected.map((item) => {
return item.tmbId;
})
});
},
onSuccess: () => {
refetchMembers();
onSuccess();
},
successToast: '成功',
errorToast: '失败'
});
return (
<MyModal
isOpen
iconSrc={'support/permission/collaborator'}
maxW={['90vw']}
minW={['900px']}
overflow={'unset'}
title={
<Box>
<Box></Box>
</Box>
}
>
<ModalCloseButton onClick={onClose} />
<ModalBody py={6} px={10}>
<Grid
templateColumns="1fr 1fr"
h="448px"
borderRadius="8px"
border="1px solid"
borderColor="myGray.200"
>
<Flex flexDirection="column" p="4">
<InputGroup alignItems="center" h="32px" my="2" py="1">
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder="搜索用户名"
fontSize="lg"
bg={'myGray.50'}
onChange={(e) => {
setSearch(e.target.value);
setSearched(
members.filter((member) => member.memberName.includes(e.target.value))
);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{searched.map((member) => {
return (
<Flex
py="2"
px={3}
borderRadius={'md'}
fontSize="lg"
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
onClick={() => {
if (selected.indexOf(member) == -1) {
setSelected([...selected, member]);
} else {
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)]);
}
}}
>
<Checkbox isChecked={selected.includes(member)} size="lg" />
<Avatar src={member.avatar} w="24px" />
{member.memberName}
</Flex>
);
})}
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4">
<Box mt={3}>: {selected.length} </Box>
<Box mt={5}>
{selected.map((member) => {
return (
<Flex
alignItems="center"
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={member.tmbId}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="24px" />
<Box w="full" fontSize="lg">
{member.memberName}
</Box>
<CloseButton
onClick={() =>
setSelected([...selected.filter((item) => item.tmbId != member.tmbId)])
}
/>
</Flex>
);
})}
</Box>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button h={'30px'} isLoading={isLoading} onClick={submit}>
</Button>
</ModalFooter>
</MyModal>
);
}
export default AddManagerModal;

View File

@@ -0,0 +1,103 @@
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 'react-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

@@ -0,0 +1,123 @@
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 { 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';
function PermissionManage() {
const { t } = useTranslation();
const members = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const { userInfo } = useUserStore();
const {
isOpen: isOpenAddManager,
onOpen: onOpenAddManager,
onClose: onCloseAddManager
} = useDisclosure();
const { mutate: removeManager } = useRequest({
mutationFn: async (memberId: string) => {
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([PermissionList['Read'], PermissionList['Write']]).value,
memberIds: [memberId]
});
},
successToast: 'Success',
errorToast: 'Error',
onSuccess: () => {
refetchMembers();
},
onError: () => {
refetchMembers();
}
});
return (
<Flex flexDirection={'column'} flex={'1'} h={['auto', '100%']} bg={'white'}>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
<Flex
mx={'5'}
flexDirection={'row'}
alignItems={'center'}
rowGap={'8'}
justifyContent={'space-between'}
>
<Flex>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} alignItems={'center'}>
{t('user.team.role.Admin')}
</Box>
<Box
fontSize={['xs']}
color={'myGray.500'}
bgColor={'myGray.100'}
alignItems={'center'}
alignContent={'center'}
mx={'6'}
px={'3'}
borderRadius={'sm'}
>
,
</Box>
</Flex>
{userInfo?.team.role === 'owner' && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenAddManager();
}}
>
</Button>
)}
</Flex>
<Flex mt="4" mx="4">
{members.map((member) => {
if (hasManage(member.permission) && member.role !== TeamMemberRoleEnum.owner) {
return (
<Tag key={member.memberName} mx={'2'} px="4" py="2" bg="myGray.100">
<Avatar src={member.avatar} w="20px" />
<TagLabel fontSize={'md'} alignItems="center" mr="6" ml="2">
{member.memberName}
</TagLabel>
{userInfo?.team.role === 'owner' && (
<MyIcon
name="common/trash"
w="16px"
color="myGray.500"
cursor="pointer"
onClick={() => {
removeManager(member.tmbId);
}}
/>
)}
</Tag>
);
}
})}
</Flex>
</Flex>
);
}
export default PermissionManage;

View File

@@ -0,0 +1,183 @@
import { Box, Button, Flex } 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 { 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 { 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'];
enum TabListEnum {
member = 'member',
permission = 'permission'
}
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 { userInfo, teamPlanStatus } = useUserStore();
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
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
];
const [tab, setTab] = useState<string>(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo?.team.teamName}
</Box>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w={'14px'}
ml={2}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'} justifyContent={'space-between'}>
<RowTabs
overflow={'auto'}
list={Tablist}
value={tab}
onChange={(v) => {
setTab(v as string);
}}
></RowTabs>
<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 && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('user.team.Team Tags Async')}
</Button>
)}
{userInfo?.team.role !== TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('user.team.Leave Team')}
</Button>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === 'member' ? <MemberTable /> : <PermissionManage />}
</Box>
<ConfirmLeaveTeamModal />
</Flex>
);
}
export default TeamCard;

View File

@@ -0,0 +1,117 @@
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
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 { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
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
return (
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common.Team')}
</Box>
{/* if there is no team */}
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
</Flex>
);
}
export default TeamList;

View File

@@ -2,66 +2,59 @@ import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { DragHandleIcon } from '@chakra-ui/icons';
import {
getTeamList,
getTeamMembers,
putSwitchTeam,
putUpdateMember,
delRemoveMember,
delLeaveTeam
getTeamList,
delLeaveTeam,
putSwitchTeam
} from '@/web/support/user/team/api';
import {
Box,
Button,
Flex,
IconButton,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
useDisclosure,
MenuButton
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { Box, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusEnum,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import dynamic from 'next/dynamic';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { setToken } from '@/web/support/user/auth';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { FormDataType, defaultForm } from './EditModal';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { createContext } 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';
const EditModal = dynamic(() => import('./EditModal'));
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
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);
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const { toast } = useToast();
const { teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('user.team.member.Confirm Leave')
});
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 {
@@ -70,25 +63,6 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeam
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
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')
});
// member action
const { data: members = [], refetch: refetchMembers } = useQuery(
['getMembers', userInfo?.team?.teamId],
@@ -104,6 +78,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
refetchMembers();
}
});
const { mutate: onRemoveMember, isLoading: isLoadingRemoveMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
@@ -112,13 +87,27 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
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
await onSwitchTeam(defaultTeam.teamId);
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
@@ -128,7 +117,22 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
});
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
}}
>
<MyModal
isOpen
onClose={onClose}
@@ -140,323 +144,20 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
overflow={'hidden'}
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
{/* teams */}
<Flex
flexDirection={'column'}
w={['auto', '270px']}
h={['auto', '100%']}
pt={3}
px={5}
mb={[2, 0]}
>
<Flex
alignItems={'center'}
py={2}
h={'40px'}
borderBottom={'1.5px solid rgba(0, 0, 0, 0.05)'}
>
<Box flex={['0 0 auto', 1]} fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common.Team')}
</Box>
{myTeams.length < 1 && (
<IconButton
variant={'ghost'}
border={'none'}
icon={
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'primary.500'}
cursor={'pointer'}
/>
}
aria-label={''}
onClick={() => setEditTeamData(defaultForm)}
/>
)}
</Flex>
<Box flex={['auto', '1 0 0']} overflow={'auto'}>
{myTeams.map((team) => (
<Flex
key={team.teamId}
alignItems={'center'}
mt={3}
borderRadius={'md'}
p={3}
cursor={'default'}
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'primary.200'
}
: {
_hover: {
bg: 'myGray.100'
}
})}
>
<Avatar src={team.avatar} w={['18px', '22px']} />
<Box
flex={'1 0 0'}
w={0}
{...(team.role === TeamMemberRoleEnum.owner
? {
fontWeight: 'bold'
}
: {})}
>
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'primary.500'} />
) : (
<Button
size={'xs'}
variant={'whitePrimary'}
onClick={() => onSwitchTeam(team.teamId)}
>
{t('user.team.Check Team')}
</Button>
)}
</Flex>
))}
</Box>
</Flex>
{/* team card */}
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
alignItems={'center'}
px={5}
py={4}
borderBottom={'1.5px solid'}
borderBottomColor={'myGray.100'}
mb={3}
>
<Box fontSize={['lg', 'xl']} fontWeight={'bold'} alignItems={'center'}>
{userInfo.team.teamName}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
<MyIcon
name="edit"
w={'14px'}
ml={2}
cursor={'pointer'}
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
)}
</Flex>
<Flex px={5} alignItems={'center'}>
<MyIcon name="support/team/memberLight" w={'14px'} />
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
{userInfo.team.role === TeamMemberRoleEnum.owner && (
<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 && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<DragHandleIcon w={'14px'} color={'primary.500'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('user.team.Team Tags Async')}
</Button>
)}
<Box flex={1} />
{userInfo.team.role !== TeamMemberRoleEnum.owner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('user.team.Leave Team')}
</Button>
)}
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
<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></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?.role === TeamMemberRoleEnum.owner &&
item.role !== TeamMemberRoleEnum.owner && (
<MyMenu
width={20}
trigger="click"
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={[
{
isActive: item.role === TeamMemberRoleEnum.visitor,
label: t('user.team.Invite Role Visitor Tip'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
role: TeamMemberRoleEnum.visitor
});
}
},
{
isActive: item.role === TeamMemberRoleEnum.admin,
label: t('user.team.Invite Role Admin Tip'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
role: TeamMemberRoleEnum.admin
});
}
},
...(item.status === TeamMemberStatusEnum.reject
? [
{
label: t('user.team.Reinvite'),
onClick: () => {
onUpdateMember({
teamId: item.teamId,
memberId: item.tmbId,
status: TeamMemberStatusEnum.waiting
});
}
}
]
: []),
{
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>
</Box>
</Flex>
<TeamList />
<TeamCard />
<Loading
loading={
isSwitchTeam ||
isLoadingTeams ||
isLoadingUpdateMember ||
isLoadingRemoveMember ||
isLoadingLeaveTeam
isLoadingTeams ||
isLoadingLeaveTeam ||
isSwitchTeam
}
fixed={false}
/>
</Box>
</MyModal>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
@@ -466,8 +167,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmRemoveMemberModal />
<ConfirmLeaveTeamModal />
</>
</TeamContext.Provider>
) : null;
};

View File

@@ -190,7 +190,7 @@ export async function getServerSideProps(content: any) {
return {
props: {
currentTab: content?.query?.currentTab || TabEnum.info,
...(await serviceSideProps(content, ['publish']))
...(await serviceSideProps(content, ['publish', 'user']))
}
};
}

View File

@@ -5,6 +5,7 @@ import app from '../../i18n/zh/app.json';
import file from '../../i18n/zh/file.json';
import publish from '../../i18n/zh/publish.json';
import workflow from '../../i18n/zh/workflow.json';
import user from '../../i18n/zh/user.json';
export interface I18nNamespaces {
common: typeof common;
@@ -13,6 +14,7 @@ export interface I18nNamespaces {
file: typeof file;
publish: typeof publish;
workflow: typeof workflow;
user: typeof user;
}
export type I18nNsType = (keyof I18nNamespaces)[];

View File

@@ -9,6 +9,7 @@ type I18nContextType = {
fileT: TFunction<['file'], undefined>;
publishT: TFunction<['publish'], undefined>;
workflowT: TFunction<['workflow'], undefined>;
userT: TFunction<['user'], undefined>;
};
export const I18nContext = createContext<I18nContextType>({
@@ -23,6 +24,7 @@ const I18nContextProvider = ({ children }: { children: React.ReactNode }) => {
const { t: fileT } = useTranslation('file');
const { t: publishT } = useTranslation('publish');
const { t: workflowT } = useTranslation('workflow');
const { t: userT } = useTranslation('user');
return (
<I18nContext.Provider
@@ -32,7 +34,8 @@ const I18nContextProvider = ({ children }: { children: React.ReactNode }) => {
datasetT,
fileT,
publishT,
workflowT
workflowT,
userT
}}
>
{children}

View File

@@ -5,6 +5,7 @@ import {
InviteMemberProps,
InviteMemberResponse,
UpdateInviteProps,
UpdateTeamMemberPermissionProps,
UpdateTeamMemberProps,
UpdateTeamProps
} from '@fastgpt/global/support/user/team/controller.d';
@@ -41,6 +42,8 @@ export const updateInviteResult = (data: UpdateInviteProps) =>
PUT('/proApi/support/user/team/member/updateInvite', data);
export const delLeaveTeam = (teamId: string) =>
DELETE('/proApi/support/user/team/member/leave', { teamId });
export const updateMemberPermission = (data: UpdateTeamMemberPermissionProps) =>
PUT('/proApi/support/user/team/member/updatePermission', data);
/* --------------- team tags ---------------- */
export const getTeamsTags = () => GET<TeamTagSchema[]>(`/proApi/support/user/team/tag/list`);