feat: invitation link (#3979)
* feat: invitation link schema and apis * feat: add invitation link * feat: member status: active, leave, forbidden * fix: expires show hours and minutes * feat: invalid invitation link hint * fix: typo * chore: fix typo & i18n * fix * pref: fe * feat: add ttl index for 30-day-clean-up
This commit is contained in:
@@ -18,7 +18,6 @@ import WorkorderButton from './WorkorderButton';
|
||||
|
||||
const Navbar = dynamic(() => import('./navbar'));
|
||||
const NavbarPhone = dynamic(() => import('./navbarPhone'));
|
||||
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
|
||||
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
|
||||
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
|
||||
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
|
||||
@@ -151,7 +150,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
</Box>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
{!!userInfo && <UpdateInviteModal />}
|
||||
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
|
||||
{!!userInfo && <SystemMsgModal />}
|
||||
{showUpdateNotification && (
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { Button, ModalFooter, ModalBody, Flex, Box, useTheme } from '@chakra-ui/react';
|
||||
import { getTeamList, updateInviteResult } from '@/web/support/user/team/api';
|
||||
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
|
||||
const UpdateInviteModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { toast } = useToast();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { initUserInfo } = useUserStore();
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({});
|
||||
|
||||
const { data: inviteList = [], run: fetchInviteList } = useRequest2(
|
||||
async () => (feConfigs.isPlus ? getTeamList(TeamMemberStatusEnum.waiting) : []),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onAccept, loading: isLoadingAccept } = useRequest2(updateInviteResult, {
|
||||
onSuccess() {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:user.team.invite.Accepted')
|
||||
});
|
||||
fetchInviteList();
|
||||
initUserInfo();
|
||||
}
|
||||
});
|
||||
const { runAsync: onReject, loading: isLoadingReject } = useRequest2(updateInviteResult, {
|
||||
onSuccess() {
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:user.team.invite.Reject')
|
||||
});
|
||||
fetchInviteList();
|
||||
initUserInfo();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={inviteList && inviteList.length > 0}
|
||||
iconSrc="/imgs/modal/team.svg"
|
||||
title={
|
||||
<Box>
|
||||
<Box>{t('common:user.team.Processing invitations')}</Box>
|
||||
<Box fontWeight={'normal'} fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('common:user.team.Processing invitations Tips', { amount: inviteList?.length })}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
maxW={['90vw', '500px']}
|
||||
>
|
||||
<ModalBody>
|
||||
{inviteList?.map((item) => (
|
||||
<Flex
|
||||
key={item.teamId}
|
||||
alignItems={'center'}
|
||||
border={theme.borders.base}
|
||||
borderRadius={'md'}
|
||||
px={3}
|
||||
py={2}
|
||||
_notFirst={{
|
||||
mt: 3
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w={['16px', '23px']} />
|
||||
<Box mx={2}>{item.teamName}</Box>
|
||||
<Box flex={1} />
|
||||
<Button
|
||||
size="sm"
|
||||
variant={'solid'}
|
||||
colorScheme="green"
|
||||
isLoading={isLoadingAccept}
|
||||
onClick={() => {
|
||||
openConfirm(
|
||||
() =>
|
||||
onAccept({
|
||||
tmbId: item.tmbId,
|
||||
status: TeamMemberStatusEnum.active
|
||||
}),
|
||||
undefined,
|
||||
t('common:user.team.invite.Accept Confirm')
|
||||
)();
|
||||
}}
|
||||
>
|
||||
{t('common:user.team.invite.accept')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
ml={2}
|
||||
variant={'solid'}
|
||||
colorScheme="red"
|
||||
isLoading={isLoadingReject}
|
||||
onClick={() => {
|
||||
openConfirm(
|
||||
() =>
|
||||
onReject({
|
||||
tmbId: item.tmbId,
|
||||
status: TeamMemberStatusEnum.reject
|
||||
}),
|
||||
undefined,
|
||||
t('common:user.team.invite.Reject Confirm')
|
||||
)();
|
||||
}}
|
||||
>
|
||||
{t('common:user.team.invite.reject')}
|
||||
</Button>
|
||||
</Flex>
|
||||
))}
|
||||
</ModalBody>
|
||||
<ModalFooter justifyContent={'center'}>
|
||||
<Box>{t('common:user.team.invite.Deal Width Footer Tip')}</Box>
|
||||
</ModalFooter>
|
||||
|
||||
<ConfirmModal />
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UpdateInviteModal);
|
||||
@@ -0,0 +1,99 @@
|
||||
import { postCreateInvitationLink } from '@/web/support/user/team/api';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
HStack,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
InvitationLinkCreateType,
|
||||
InvitationLinkExpiresType
|
||||
} from '@fastgpt/service/support/user/team/invitationLink/type';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
function CreateInvitationModal({ onClose }: { onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const expiresOptions: Array<{ label: string; value: InvitationLinkExpiresType }> = [
|
||||
{ label: t('account_team:30mins'), value: '30m' }, // 30 mins
|
||||
{ label: t('account_team:7days'), value: '7d' }, // 7 days
|
||||
{ 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
|
||||
}
|
||||
});
|
||||
|
||||
const expires = watch('expires');
|
||||
const usedTimesLimit = watch('usedTimesLimit');
|
||||
|
||||
const { runAsync: createInvitationLink } = useRequest2(postCreateInvitationLink, {
|
||||
manual: true,
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
onFinally: () => onClose()
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
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 })}
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button isLoading={false} onClick={onClose} variant="outline">
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button isLoading={false} onClick={handleSubmit(createInvitationLink)} ml="4">
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateInvitationModal;
|
||||
@@ -172,7 +172,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</Td>
|
||||
<Td>
|
||||
{group.name === DefaultGroupName ? (
|
||||
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
|
||||
<AvatarGroup avatars={members.map((v) => v.avatar)} />
|
||||
) : hasGroupManagePer(group) ? (
|
||||
<MyTooltip label={t('account_team:manage_member')}>
|
||||
<Box cursor="pointer" onClick={() => onManageMember(group)}>
|
||||
@@ -180,7 +180,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
@@ -189,7 +188,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
avatars={group.members.map(
|
||||
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
|
||||
)}
|
||||
groupId={group._id}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
@@ -1,12 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
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,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
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 { useTranslation } from 'next-i18next';
|
||||
import { ModalCloseButton, ModalBody, Box, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import TagTextarea from '@/components/common/Textarea/TagTextarea';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postInviteTeamMember } from '@/web/support/user/team/api';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import type { InviteMemberResponse } from '@fastgpt/global/support/user/team/controller.d';
|
||||
import format from 'date-fns/format';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const CreateInvitationModal = dynamic(() => import('./CreateInvitationModal'));
|
||||
|
||||
const InviteModal = ({
|
||||
teamId,
|
||||
@@ -18,43 +47,47 @@ const InviteModal = ({
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
title: t('user:team.Invite Member Result Tip'),
|
||||
showCancel: false
|
||||
|
||||
const {
|
||||
data: invitationLinkList,
|
||||
loading: isLoadingLink,
|
||||
runAsync: refetchInvitationLinkList
|
||||
} = useRequest2(() => getInvitationLinkList(), {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const [inviteUsernames, setInviteUsernames] = useState<string[]>([]);
|
||||
const { isOpen: isOpenCreate, onOpen: onOpenCreate, onClose: onCloseCreate } = useDisclosure();
|
||||
|
||||
const { runAsync: onInvite, loading: isLoading } = useRequest2(
|
||||
() =>
|
||||
postInviteTeamMember({
|
||||
teamId,
|
||||
usernames: inviteUsernames
|
||||
const isLoading = isLoadingLink;
|
||||
const { copyData } = useCopyData();
|
||||
|
||||
const onCopy = useCallback(
|
||||
(linkId: string) => {
|
||||
copyData(location.origin + `/account/team?invitelinkid=${linkId}`);
|
||||
},
|
||||
[copyData]
|
||||
);
|
||||
|
||||
const { runAsync: onForbid } = useRequest2(
|
||||
(linkId: string) =>
|
||||
putUpdateInvitationInfo({
|
||||
linkId,
|
||||
forbidden: true
|
||||
}),
|
||||
{
|
||||
onSuccess(res: InviteMemberResponse) {
|
||||
onSuccess();
|
||||
openConfirm(
|
||||
() => onClose(),
|
||||
undefined,
|
||||
<Box whiteSpace={'pre-wrap'}>
|
||||
{t('user:team.Invite Member Success Tip', {
|
||||
success: res.invite.length,
|
||||
inValid: res.inValid.map((item) => item.username).join(', '),
|
||||
inTeam: res.inTeam.map((item) => item.username).join(', ')
|
||||
})}
|
||||
</Box>
|
||||
)();
|
||||
},
|
||||
errorToast: t('user:team.Invite Member Failed Tip')
|
||||
manual: true,
|
||||
onSuccess: refetchInvitationLinkList,
|
||||
successToast: t('account_team:forbid_success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isLoading={isLoading}
|
||||
isOpen
|
||||
iconSrc="common/inviteLight"
|
||||
iconColor="primary.600"
|
||||
minW={'600px'}
|
||||
title={
|
||||
<Box>
|
||||
<Box>{t('common:user.team.Invite Member')}</Box>
|
||||
@@ -63,26 +96,177 @@ const InviteModal = ({
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
maxW={['90vw', '400px']}
|
||||
maxW={['90vw']}
|
||||
overflow={'unset'}
|
||||
>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<ModalBody>
|
||||
<Box mb={2}>{t('common:user.Account')}</Box>
|
||||
<TagTextarea defaultValues={inviteUsernames} onUpdate={setInviteUsernames} />
|
||||
<ModalHeader pb="0">
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'} mx="2">
|
||||
<HStack>
|
||||
<Icon name="common/list" w="16px" />
|
||||
<Box ml="6px" fontSize="md">
|
||||
{t('account_team:invitation_link_list')}
|
||||
</Box>
|
||||
</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>
|
||||
<Tr bgColor={'white !important'}>
|
||||
<Th borderLeftRadius="6px" bgColor="myGray.100">
|
||||
{t('account_team:invitation_link_description')}
|
||||
</Th>
|
||||
<Th bgColor="myGray.100">{t('account_team:expires')}</Th>
|
||||
<Th bgColor="myGray.100">{t('account_team:used_times_limit')}</Th>
|
||||
<Th bgColor="myGray.100">{t('account_team:invited')}</Th>
|
||||
<Th bgColor="myGray.100" borderRightRadius="6px">
|
||||
{t('common:common.Action')}
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
{!!invitationLinkList?.length && (
|
||||
<Tbody overflow={'unset'}>
|
||||
{invitationLinkList?.map((item) => {
|
||||
const isForbidden = item.forbidden || new Date(item.expires) < new Date();
|
||||
return (
|
||||
<Tr key={item._id} overflow={'unset'}>
|
||||
<Td maxW="200px" minW="100px">
|
||||
{item.description}
|
||||
</Td>
|
||||
<Td>
|
||||
{isForbidden ? (
|
||||
<Tag colorSchema="gray">{t('account_team:has_forbidden')}</Tag>
|
||||
) : (
|
||||
format(new Date(item.expires), 'yyyy-MM-dd HH:mm')
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{item.usedTimesLimit === -1
|
||||
? t('account_team:unlimited')
|
||||
: 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
|
||||
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 && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onCopy(item._id)}
|
||||
color="myGray.900"
|
||||
>
|
||||
<Icon name="common/link" w="16px" mr="1" />
|
||||
{t('account_team:copy_link')}
|
||||
</Button>
|
||||
<MyPopover
|
||||
placement="bottom-end"
|
||||
Trigger={
|
||||
<Button variant="outline" ml="10px" size="sm" color="myGray.900">
|
||||
<Icon name="common/lineStop" w="16px" mr="1" />
|
||||
{t('account_team:forbidden')}
|
||||
</Button>
|
||||
}
|
||||
closeOnBlur={true}
|
||||
>
|
||||
{({ onClose: onClosePopover }) => (
|
||||
<Box p={4}>
|
||||
<Box fontWeight={400} whiteSpace="pre-wrap">
|
||||
{t('account_team:forbid_hint')}
|
||||
</Box>
|
||||
<Flex gap={2} mt={2} justifyContent={'flex-end'}>
|
||||
<Button variant="outline" onClick={onClosePopover}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
onClick={() => {
|
||||
onForbid(item._id);
|
||||
onClosePopover();
|
||||
}}
|
||||
>
|
||||
{t('account_team:confirm_forbidden')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
</>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
)}
|
||||
</Table>
|
||||
{!invitationLinkList?.length && <EmptyTip />}
|
||||
</TableContainer>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
w={'100%'}
|
||||
h={'34px'}
|
||||
isDisabled={inviteUsernames.length === 0}
|
||||
isLoading={isLoading}
|
||||
onClick={onInvite}
|
||||
>
|
||||
{t('user:team.Confirm Invite')}
|
||||
</Button>
|
||||
<ModalFooter justifyContent={'flex-start'}>
|
||||
<Tag colorSchema="blue" marginBlock="2">
|
||||
<Box>{t('account_team:invitation_link_auto_clean_hint')}</Box>
|
||||
</Tag>
|
||||
</ModalFooter>
|
||||
<ConfirmModal />
|
||||
{isOpenCreate && (
|
||||
<CreateInvitationModal
|
||||
onClose={() => Promise.all([onCloseCreate(), refetchInvitationLinkList()])}
|
||||
/>
|
||||
)}
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { delRemoveMember, updateStatus } from '@/web/support/user/team/api';
|
||||
import { delRemoveMember, postRestoreMember } from '@/web/support/user/team/api';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
@@ -118,7 +118,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
errorToast: t('account_team:sync_member_failed')
|
||||
});
|
||||
|
||||
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(updateStatus, {
|
||||
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(postRestoreMember, {
|
||||
onSuccess() {
|
||||
refetchMembers();
|
||||
},
|
||||
@@ -253,12 +253,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{member.memberName}
|
||||
{member.status === 'waiting' && (
|
||||
<Tag ml="2" colorSchema="yellow">
|
||||
{t('account_team:waiting')}
|
||||
</Tag>
|
||||
)}
|
||||
{member.status === 'leave' && (
|
||||
{member.status !== 'active' && (
|
||||
<Tag ml="2" colorSchema="gray">
|
||||
{t('account_team:leave')}
|
||||
</Tag>
|
||||
@@ -295,7 +290,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
member.role !== TeamMemberRoleEnum.owner &&
|
||||
member.tmbId !== userInfo?.team.tmbId &&
|
||||
(member.status !== TeamMemberStatusEnum.leave ? (
|
||||
(member.status === TeamMemberStatusEnum.active ? (
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
@@ -320,30 +315,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
name={'common/confirm/restoreTip'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'primary.500',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRestoreMember(
|
||||
() =>
|
||||
onRestore({
|
||||
tmbId: member.tmbId,
|
||||
status: TeamMemberStatusEnum.active
|
||||
}),
|
||||
undefined,
|
||||
t('account_team:restore_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
member.status === TeamMemberStatusEnum.forbidden && (
|
||||
<Icon
|
||||
name={'common/confirm/restoreTip'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'primary.500',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRestoreMember(
|
||||
() => onRestore(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:restore_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -20,6 +20,7 @@ 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'));
|
||||
|
||||
export enum TeamTabEnum {
|
||||
member = 'member',
|
||||
@@ -30,6 +31,16 @@ export enum TeamTabEnum {
|
||||
|
||||
const Team = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const invitelinkid = useMemo(() => {
|
||||
const _id = router.query.invitelinkid;
|
||||
if (!_id && typeof _id !== 'string') {
|
||||
return '';
|
||||
} else {
|
||||
return _id as string;
|
||||
}
|
||||
}, [router.query.invitelinkid]);
|
||||
|
||||
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -142,6 +153,7 @@ const Team = () => {
|
||||
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
{invitelinkid && <HandleInviteModal invitelinkid={invitelinkid} />}
|
||||
</AccountContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
InviteMemberProps,
|
||||
InviteMemberResponse,
|
||||
UpdateInviteProps,
|
||||
UpdateStatusProps,
|
||||
UpdateTeamProps
|
||||
} from '@fastgpt/global/support/user/team/controller.d';
|
||||
import type { TeamTagItemType, TeamTagSchema } from '@fastgpt/global/support/user/team/type';
|
||||
@@ -21,6 +20,12 @@ import {
|
||||
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
|
||||
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
|
||||
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import {
|
||||
InvitationInfoType,
|
||||
InvitationLinkCreateType,
|
||||
InvitationLinkUpdateType,
|
||||
InvitationType
|
||||
} from '@fastgpt/service/support/user/team/invitationLink/type';
|
||||
|
||||
/* --------------- team ---------------- */
|
||||
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
|
||||
@@ -34,18 +39,37 @@ export const putSwitchTeam = (teamId: string) =>
|
||||
/* --------------- team member ---------------- */
|
||||
export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) =>
|
||||
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
|
||||
export const postInviteTeamMember = (data: InviteMemberProps) =>
|
||||
POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
|
||||
|
||||
// export const postInviteTeamMember = (data: InviteMemberProps) =>
|
||||
// POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
|
||||
|
||||
export const putUpdateMemberName = (name: string) =>
|
||||
PUT(`/proApi/support/user/team/member/updateName`, { name });
|
||||
export const delRemoveMember = (tmbId: string) =>
|
||||
DELETE(`/proApi/support/user/team/member/delete`, { tmbId });
|
||||
export const updateInviteResult = (data: UpdateInviteProps) =>
|
||||
PUT('/proApi/support/user/team/member/updateInvite', data);
|
||||
export const updateStatus = (data: UpdateStatusProps) =>
|
||||
PUT('/proApi/support/user/team/member/updateStatus', data);
|
||||
export const postRestoreMember = (tmbId: string) =>
|
||||
POST('/proApi/support/user/team/member/restore', { tmbId });
|
||||
export const delLeaveTeam = () => DELETE('/proApi/support/user/team/member/leave');
|
||||
|
||||
/* -------------- team invitaionlink -------------------- */
|
||||
|
||||
export const postCreateInvitationLink = (data: InvitationLinkCreateType) =>
|
||||
POST<string>(`/proApi/support/user/team/invitationLink/create`, data);
|
||||
|
||||
export const getInvitationLinkList = () =>
|
||||
GET<InvitationType[]>(`/proApi/support/user/team/invitationLink/list`);
|
||||
|
||||
export const postAcceptInvitationLink = (linkId: string) =>
|
||||
POST<string>(`/proApi/support/user/team/invitationLink/accept`, { linkId });
|
||||
|
||||
export const getInvitationInfo = (linkId: string) =>
|
||||
GET<InvitationInfoType>(`/proApi/support/user/team/invitationLink/info`, { linkId });
|
||||
|
||||
export const putUpdateInvitationInfo = (data: InvitationLinkUpdateType) =>
|
||||
PUT('/proApi/support/user/team/invitationLink/update', data);
|
||||
|
||||
/* -------------- team collaborator -------------------- */
|
||||
export const getTeamClbs = () =>
|
||||
GET<CollaboratorItemType[]>(`/proApi/support/user/team/collaborator/list`);
|
||||
|
||||
Reference in New Issue
Block a user