perf: invite member code (#4118)

* perf: invite member code

* fix: ts
This commit is contained in:
Archer
2025-03-12 14:35:38 +08:00
committed by archer
parent a9e5017492
commit 7eda599181
20 changed files with 284 additions and 342 deletions

View File

@@ -79,7 +79,7 @@ const UpdateContactModal = ({
title={
mode === 'notification_account'
? t('common:support.user.info.notification_receiving_hint')
: t('account_info:contact')
: t('common:contact_way')
}
>
<ModalBody px={10}>

View File

@@ -1,106 +0,0 @@
import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
import {
Box,
Button,
CloseButton,
Flex,
ModalBody,
ModalCloseButton,
ModalHeader
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from './context';
import { isForbidden } from '@fastgpt/service/support/user/team/invitationLink/controllers';
import { useToast } from '@fastgpt/web/hooks/useToast';
function Invite({ invitelinkid }: { invitelinkid: string }) {
const router = useRouter();
const { t } = useTranslation();
const { onSwitchTeam, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const onClose = () => {
router.push('/account/team');
};
const { toast } = useToast();
const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
manual: false,
onSuccess: (data) => {
if (isForbidden(data)) {
toast({
description: t('account_team:invitation_link_has_been_invalid'),
status: 'warning'
});
onClose();
}
},
onError: onClose
});
const { runAsync: acceptInvitation } = useRequest2(() => postAcceptInvitationLink(invitelinkid), {
manual: true,
successToast: t('common:common.Success'),
onSuccess: () => {
toast({
description: t('common:common.Success'),
status: 'success'
});
onSwitchTeam(invitationInfo!.teamId);
refetchMembers();
onClose();
},
onError: (e) => {
toast({
description: t('common:common.Error'),
status: 'error'
});
onClose();
}
});
return (
<>
{invitationInfo && (
<MyModal
isOpen={true}
iconSrc="support/user/usersLight"
title={t('account_team:handle_invitation')}
iconColor={'primary.600'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Flex
key={invitationInfo._id}
alignItems={'center'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'md'}
px={3}
py={2}
>
<Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
<Box mx={2}>{invitationInfo.teamName}</Box>
<Box flex={1} />
<Button size="sm" variant={'solid'} colorScheme="green" onClick={acceptInvitation}>
{t('common:user.team.invite.accept')}
</Button>
<Button size="sm" ml={2} variant="outline" onClick={onClose}>
{t('account_team:ignore')}
</Button>
</Flex>
</ModalBody>
</MyModal>
)}
</>
);
}
export default Invite;

View File

@@ -3,11 +3,13 @@ import {
Box,
Button,
Grid,
HStack,
Radio,
RadioGroup,
Input,
ModalBody,
ModalCloseButton,
ModalFooter
ModalFooter,
HStack
} from '@chakra-ui/react';
import {
InvitationLinkCreateType,
@@ -28,22 +30,18 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
{ label: t('account_team:1year'), value: '1y' } // 1 year
];
const usedTimesLimitOptions = [
{ label: t('account_team:unlimited'), value: -1 },
{ label: t('account_team:1person'), value: 1 }
];
const { register, handleSubmit, watch, setValue } = useForm<InvitationLinkCreateType>({
defaultValues: {
description: '',
expires: expiresOptions[1].value,
usedTimesLimit: usedTimesLimitOptions[1].value
usedTimesLimit: 1
}
});
const expires = watch('expires');
const usedTimesLimit = watch('usedTimesLimit');
const { runAsync: createInvitationLink } = useRequest2(postCreateInvitationLink, {
const { runAsync: createInvitationLink, loading } = useRequest2(postCreateInvitationLink, {
manual: true,
successToast: t('common:common.Create Success'),
errorToast: t('common:common.Create Failed'),
@@ -56,39 +54,47 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
iconSrc="common/addLight"
iconColor="primary.500"
title={<Box>{t('account_team:create_invitation_link')}</Box>}
minW={'500px'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Grid gap={4} w="full" templateColumns="max-content 1fr" alignItems="center">
<FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
<Input
placeholder={t('account_team:invitation_link_description')}
{...register('description', { required: true })}
/>
<Grid gap={6} templateColumns="max-content 1fr" alignItems="center">
<>
<FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
<Input
placeholder={t('account_team:invitation_link_description')}
{...register('description', { required: true })}
/>
</>
<FormLabel required={true}>{t('account_team:expires')}</FormLabel>
<MySelect
list={expiresOptions}
value={expires}
onchange={(val) => setValue('expires', val)}
minW="120px"
/>
<>
<FormLabel required={true}>{t('account_team:expires')}</FormLabel>
<MySelect
list={expiresOptions}
value={expires}
onchange={(val) => setValue('expires', val)}
minW="120px"
/>
</>
<FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
<MySelect
list={usedTimesLimitOptions}
value={usedTimesLimit}
onchange={(val) => setValue('usedTimesLimit', val)}
minW="120px"
/>
<>
<FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
<RadioGroup
onChange={(val: '1' | '-1') => setValue('usedTimesLimit', Number(val) as 1 | -1)}
value={String(usedTimesLimit)}
>
<HStack gap={6}>
<Radio value="1">{t('account_team:1person')}</Radio>
<Radio value="-1">{t('account_team:unlimited')}</Radio>
</HStack>
</RadioGroup>
</>
</Grid>
</ModalBody>
<ModalFooter>
<Button isLoading={false} onClick={onClose} variant="outline">
<Button isLoading={loading} onClick={onClose} variant="outline">
{t('common:common.Cancel')}
</Button>
<Button isLoading={false} onClick={handleSubmit(createInvitationLink)} ml="4">
<Button isLoading={loading} onClick={handleSubmit(createInvitationLink)} ml="4">
{t('common:common.Confirm')}
</Button>
</ModalFooter>

View File

@@ -0,0 +1,77 @@
import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
import { Box, Button, Flex, ModalBody, ModalCloseButton } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
function Invite({ invitelinkid }: { invitelinkid: string }) {
const router = useRouter();
const { t } = useTranslation();
const { onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
const onClose = () => {
router.push('/account/team');
};
const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
manual: false,
onError: onClose
});
const { runAsync: acceptInvitation, loading: accepting } = useRequest2(
() => postAcceptInvitationLink(invitelinkid),
{
manual: true,
successToast: t('common:common.Success'),
onSuccess: async () => {
onSwitchTeam(invitationInfo!.teamId);
onClose();
}
}
);
return invitationInfo ? (
<MyModal
isOpen={true}
iconSrc="support/user/usersLight"
title={t('account_team:handle_invitation')}
iconColor={'primary.600'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Flex
key={invitationInfo._id}
alignItems={'center'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'md'}
px={3}
py={2}
>
<Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
<Box mx={2}>{invitationInfo.teamName}</Box>
<Box flex={1} />
<Button
size="sm"
variant={'solid'}
colorScheme="green"
onClick={acceptInvitation}
isLoading={accepting}
>
{t('account_team:accept')}
</Button>
<Button size="sm" ml={2} variant="outline" onClick={onClose} isLoading={accepting}>
{t('account_team:ignore')}
</Button>
</Flex>
</ModalBody>
</MyModal>
) : null;
}
export default Invite;

View File

@@ -1,5 +1,4 @@
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import Empty from '@/pageComponents/chat/Empty';
import { getInvitationLinkList, putUpdateInvitationInfo } from '@/web/support/user/team/api';
import {
Box,
@@ -9,9 +8,7 @@ import {
Grid,
HStack,
ModalBody,
ModalCloseButton,
ModalFooter,
ModalHeader,
Table,
TableContainer,
Tbody,
@@ -24,7 +21,6 @@ import {
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Icon from '@fastgpt/web/components/common/Icon';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import Tag from '@fastgpt/web/components/common/Tag';
@@ -68,7 +64,7 @@ const InviteModal = ({
[copyData]
);
const { runAsync: onForbid } = useRequest2(
const { runAsync: onForbid, loading: forbiding } = useRequest2(
(linkId: string) =>
putUpdateInvitationInfo({
linkId,
@@ -87,21 +83,14 @@ const InviteModal = ({
isOpen
iconSrc="common/inviteLight"
iconColor="primary.600"
minW={'600px'}
title={
<Box>
<Box>{t('common:user.team.Invite Member')}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{t('common:user.team.Invite Member Tips')}
</Box>
</Box>
}
maxW={['90vw']}
title={t('account_team:invite_member')}
overflow={'unset'}
onClose={onClose}
w={'100%'}
maxW={['90vw', '820px']}
>
<ModalCloseButton onClick={onClose} />
<ModalHeader pb="0">
<Flex alignItems={'center'} justifyContent={'space-between'} mx="2">
<ModalBody maxH="500px">
<Flex alignItems={'center'} justifyContent={'space-between'} mb={4}>
<HStack>
<Icon name="common/list" w="16px" />
<Box ml="6px" fontSize="md">
@@ -110,8 +99,6 @@ const InviteModal = ({
</HStack>
<Button onClick={onOpenCreate}>{t('account_team:create_invitation_link')}</Button>
</Flex>
</ModalHeader>
<ModalBody maxH="500px">
<TableContainer overflowY={'auto'}>
<Table fontSize={'sm'} overflow={'unset'}>
<Thead>
@@ -149,56 +136,57 @@ const InviteModal = ({
: item.usedTimesLimit}
</Td>
<Td>
<MyPopover
w="fit-content"
Trigger={
<Box
minW="100px"
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
p="1.5"
w="fit-content"
>
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
</Box>
}
trigger="click"
closeOnBlur={true}
>
{() => (
<Box py="4" maxH="200px" w="fit-content">
<Flex mx="4" justifyContent="center" alignItems={'center'}>
<Box>{t('account_team:has_invited')}</Box>
<Box
ml="auto"
bg="myGray.200"
px="2"
borderRadius="md"
fontSize="sm"
>
{item.members.length}
</Box>
</Flex>
<Divider my="2" mx="4" />
<Grid
{item.members.length > 0 && (
<MyPopover
w="fit-content"
Trigger={
<Box
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
p="1.5"
w="fit-content"
mt="2"
gridRowGap="4"
gridTemplateColumns="1fr 1fr"
overflow="auto"
alignItems="center"
mx="4"
>
{item.members.map((member) => (
<Box key={member.tmbId} justifySelf="start">
<MemberTag name={member.name} avatar={member.avatar} />
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
</Box>
}
trigger="click"
closeOnBlur={true}
>
{() => (
<Box py="4" maxH="200px" w="fit-content">
<Flex mx="4" justifyContent="center" alignItems={'center'}>
<Box>{t('account_team:has_invited')}</Box>
<Box
ml="auto"
bg="myGray.200"
px="2"
borderRadius="md"
fontSize="sm"
>
{item.members.length}
</Box>
))}
</Grid>
</Box>
)}
</MyPopover>
</Flex>
<Divider my="2" mx="4" />
<Grid
w="fit-content"
mt="2"
gridRowGap="4"
gridTemplateColumns="1fr 1fr"
overflow="auto"
alignItems="center"
mx="4"
>
{item.members.map((member) => (
<Box key={member.tmbId} justifySelf="start">
<MemberTag name={member.name} avatar={member.avatar} />
</Box>
))}
</Grid>
</Box>
)}
</MyPopover>
)}
</Td>
<Td>
{!isForbidden && (
@@ -232,6 +220,7 @@ const InviteModal = ({
{t('common:common.Cancel')}
</Button>
<Button
isLoading={forbiding}
variant="outline"
colorScheme="red"
onClick={() => {

View File

@@ -41,7 +41,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils';
const InviteModal = dynamic(() => import('./InviteModal'));
const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
@@ -236,7 +236,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:contact')}</Th>
<Th bgColor="myGray.100">{t('common:contact_way')}</Th>
<Th bgColor="myGray.100">{t('account_team:org')}</Th>
<Th bgColor="myGray.100">{t('account_team:join_update_time')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">

View File

@@ -101,6 +101,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
refetchMembers();
return initUserInfo();
},
{

View File

@@ -260,7 +260,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:contact')}:&nbsp;</Box>
<Box {...labelStyles}>{t('common:contact_way')}:&nbsp;</Box>
<Box flex={1} {...(!userInfo?.contact ? { color: 'red.600' } : {})}>
{userInfo?.contact ? userInfo?.contact : t('account_info:please_bind_contact')}
</Box>

View File

@@ -20,7 +20,9 @@ const PermissionManage = dynamic(
);
const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
const HandleInviteModal = dynamic(() => import('@/pageComponents/account/team/HandleInviteModal'));
const HandleInviteModal = dynamic(
() => import('@/pageComponents/account/team/Invite/HandleInviteModal')
);
export enum TeamTabEnum {
member = 'member',
@@ -99,7 +101,7 @@ const Team = () => {
</Box>
</Flex>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} onChange={refetchMembers} />
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>