perf: member group (#4324)

* sync collection

* remove lock

* perf: member group
This commit is contained in:
Archer
2025-03-26 00:02:14 +08:00
committed by archer
parent 64fb09146f
commit 4871a6980f
14 changed files with 213 additions and 255 deletions

View File

@@ -0,0 +1,4 @@
export type GetGroupListBody = {
searchKey?: string;
withMembers?: boolean;
};

View File

@@ -1,6 +1,7 @@
import { TeamMemberItemType } from 'support/user/team/type'; import { TeamMemberItemType } from 'support/user/team/type';
import { TeamPermission } from '../user/controller'; import { TeamPermission } from '../user/controller';
import { GroupMemberRole } from './constant'; import { GroupMemberRole } from './constant';
import { Permission } from '../controller';
type MemberGroupSchemaType = { type MemberGroupSchemaType = {
_id: string; _id: string;
@@ -16,23 +17,25 @@ type GroupMemberSchemaType = {
role: `${GroupMemberRole}`; role: `${GroupMemberRole}`;
}; };
type MemberGroupType = MemberGroupSchemaType & { type MemberGroupListItemType<T extends boolean | undefined> = MemberGroupSchemaType & {
members: { members: T extends true
tmbId: string; ? {
name: string; tmbId: string;
avatar: string; name: string;
}[]; avatar: string;
count: number; }[]
owner: { : undefined;
tmbId: string; count: T extends true ? number : undefined;
name: string; owner?: T extends true
avatar: string; ? {
}; tmbId: string;
canEdit: boolean; name: string;
avatar: string;
}
: undefined;
permission: T extends true ? Permission : undefined;
}; };
type MemberGroupListType = MemberGroupType[];
type GroupMemberItemType = { type GroupMemberItemType = {
tmbId: string; tmbId: string;
name: string; name: string;

View File

@@ -1,4 +1,7 @@
import { MemberGroupSchemaType, MemberGroupType } from 'support/permission/memberGroup/type'; import {
MemberGroupSchemaType,
MemberGroupListItemType
} from 'support/permission/memberGroup/type';
import { OAuthEnum } from './constant'; import { OAuthEnum } from './constant';
import { TrackRegisterParams } from './login/api'; import { TrackRegisterParams } from './login/api';
import { TeamMemberStatusEnum } from './team/constant'; import { TeamMemberStatusEnum } from './team/constant';

View File

@@ -55,6 +55,14 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemTyp
}; };
} }
export const getTeamOwner = async (teamId: string) => {
const tmb = await MongoTeamMember.findOne({
teamId,
role: TeamMemberRoleEnum.owner
}).lean();
return tmb;
};
export async function getTmbInfoByTmbId({ tmbId }: { tmbId: string }) { export async function getTmbInfoByTmbId({ tmbId }: { tmbId: string }) {
if (!tmbId) { if (!tmbId) {
return Promise.reject('tmbId or userId is required'); return Promise.reject('tmbId or userId is required');

View File

@@ -28,20 +28,17 @@ import {
DEFAULT_USER_AVATAR DEFAULT_USER_AVATAR
} from '@fastgpt/global/common/system/constants'; } from '@fastgpt/global/common/system/constants';
import Path from '@/components/common/folder/Path'; import Path from '@/components/common/folder/Path';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type'; import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api'; import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList, getOrgMembers } from '@/web/support/user/team/org/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MemberItemCard from './MemberItemCard'; import MemberItemCard from './MemberItemCard';
import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import useOrg from '@/web/support/user/team/org/hooks/useOrg';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
const HoverBoxStyle = { const HoverBoxStyle = {
bgColor: 'myGray.50', bgColor: 'myGray.50',
@@ -76,7 +73,9 @@ function MemberModal({
const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2( const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2(
async () => { async () => {
if (!userInfo?.team?.teamId) return []; if (!userInfo?.team?.teamId) return [];
return getGroupList(); return getGroupList<false>({
withMembers: false
});
}, },
{ {
manual: false, manual: false,
@@ -117,7 +116,7 @@ function MemberModal({
return members; return members;
}, [searchText, members, searchedData?.members]); }, [searchText, members, searchedData?.members]);
const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListType>([]); const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]);
const filterGroups = useMemo(() => { const filterGroups = useMemo(() => {
if (searchText) { if (searchText) {
return searchedData?.groups.map((item) => ({ return searchedData?.groups.map((item) => ({

View File

@@ -3,16 +3,16 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import React from 'react'; import React from 'react';
type Props = { type Props = {
name: string; name?: string;
avatar: string; avatar?: string;
}; };
function MemberTag({ name, avatar }: Props) { function MemberTag({ name, avatar }: Props) {
return ( return (
<HStack> <HStack>
<Avatar src={avatar} w={['18px', '22px']} rounded="50%" /> {avatar && <Avatar src={avatar} w={['18px', '22px']} rounded="50%" />}
<Box maxW={'150px'} className={'textEllipsis'}> <Box maxW={'150px'} className={'textEllipsis'}>
{name} {name || '-'}
</Box> </Box>
</HStack> </HStack>
); );

View File

@@ -4,15 +4,13 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react'; import React from 'react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api'; import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
export type GroupFormType = { export type GroupFormType = {
avatar: string; avatar: string;
@@ -21,17 +19,15 @@ export type GroupFormType = {
function GroupInfoModal({ function GroupInfoModal({
onClose, onClose,
editGroupId, editGroup,
groups, onSuccess
refetchGroups
}: { }: {
onClose: () => void; onClose: () => void;
editGroupId?: string; editGroup?: MemberGroupListItemType<true>;
groups: MemberGroupListType; onSuccess: () => void;
refetchGroups: () => void;
}) { }) {
const { refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
File: AvatarSelect, File: AvatarSelect,
onOpen: onOpenSelectAvatar, onOpen: onOpenSelectAvatar,
@@ -41,14 +37,10 @@ function GroupInfoModal({
multiple: false multiple: false
}); });
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({ const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
defaultValues: { defaultValues: {
name: group?.name || '', name: editGroup?.name || '',
avatar: group?.avatar || DEFAULT_TEAM_AVATAR avatar: editGroup?.avatar || DEFAULT_TEAM_AVATAR
} }
}); });
@@ -74,21 +66,21 @@ function GroupInfoModal({
}); });
}, },
{ {
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) onSuccess: () => Promise.all([onClose(), onSuccess()])
} }
); );
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: GroupFormType) => { async (data: GroupFormType) => {
if (!editGroupId) return; if (!editGroup) return;
return putUpdateGroup({ return putUpdateGroup({
groupId: editGroupId, groupId: editGroup._id,
name: data.name, name: data.name,
avatar: data.avatar avatar: data.avatar
}); });
}, },
{ {
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) onSuccess: () => Promise.all([onClose(), onSuccess()])
} }
); );
@@ -97,8 +89,8 @@ function GroupInfoModal({
return ( return (
<MyModal <MyModal
onClose={onClose} onClose={onClose}
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')} title={editGroup ? t('user:team.group.edit') : t('user:team.group.create')}
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR} iconSrc={editGroup?.avatar ?? DEFAULT_TEAM_AVATAR}
> >
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}> <ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel> <FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
@@ -120,14 +112,14 @@ function GroupInfoModal({
<Button <Button
isLoading={isLoading} isLoading={isLoading}
onClick={handleSubmit((data) => { onClick={handleSubmit((data) => {
if (editGroupId) { if (editGroup) {
onUpdate(data); onUpdate(data);
} else { } else {
onCreate(data); onCreate(data);
} }
})} })}
> >
{editGroupId ? t('common:common.Save') : t('common:new_create')} {editGroup ? t('common:common.Save') : t('common:new_create')}
</Button> </Button>
</ModalFooter> </ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} /> <AvatarSelect onSelect={onSelectAvatar} />

View File

@@ -26,7 +26,7 @@ import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { import {
GroupMemberItemType, GroupMemberItemType,
MemberGroupListType MemberGroupListItemType
} from '@fastgpt/global/support/permission/memberGroup/type'; } from '@fastgpt/global/support/permission/memberGroup/type';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
@@ -43,24 +43,19 @@ export type GroupFormType = {
function GroupEditModal({ function GroupEditModal({
onClose, onClose,
editGroupId, editGroupId,
groups, group,
refetchGroups onSuccess
}: { }: {
onClose: () => void; onClose: () => void;
editGroupId?: string; editGroupId?: string;
groups: MemberGroupListType; group: MemberGroupListItemType<true>;
refetchGroups: () => void; onSuccess: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { toast } = useToast(); const { toast } = useToast();
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const allMembers = useContextSelector(TeamContext, (v) => v.members); const allMembers = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData); const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
const [hoveredMemberId, setHoveredMemberId] = useState<string>(); const [hoveredMemberId, setHoveredMemberId] = useState<string>();
@@ -94,7 +89,7 @@ function GroupEditModal({
}); });
}, },
{ {
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()]) onSuccess: () => Promise.all([onClose(), onSuccess()])
} }
); );

View File

@@ -1,4 +1,4 @@
import { putGroupChangeOwner, putUpdateGroup } from '@/web/support/user/team/group/api'; import { putGroupChangeOwner } from '@/web/support/user/team/group/api';
import { import {
Box, Box,
Flex, Flex,
@@ -15,26 +15,24 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react'; import React, { useState } from 'react';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import { Omit } from '@fastgpt/web/components/common/DndDrag'; import { Omit } from '@fastgpt/web/components/common/DndDrag';
export type ChangeOwnerModalProps = {
groupId: string;
groups: MemberGroupListType;
refetchGroups: () => void;
};
export function ChangeOwnerModal({ export function ChangeOwnerModal({
onClose, group,
groupId, onSuccess,
groups, onClose
refetchGroups }: {
}: ChangeOwnerModalProps & { onClose: () => void }) { group: MemberGroupListItemType<true>;
onSuccess: () => void;
onClose: () => void;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState(''); const [inputValue, setInputValue] = React.useState('');
const { data: searchedData } = useRequest2( const { data: searchedData } = useRequest2(
async () => { async () => {
@@ -50,14 +48,8 @@ export function ChangeOwnerModal({
); );
const { members: allMembers } = useContextSelector(TeamContext, (v) => v); const { members: allMembers } = useContextSelector(TeamContext, (v) => v);
const group = useMemo(() => {
return groups.find((item) => item._id === groupId);
}, [groupId, groups]);
const memberList = searchedData ? searchedData.members : allMembers; const memberList = searchedData ? searchedData.members : allMembers;
const [keepAdmin, setKeepAdmin] = useState(true);
const { const {
isOpen: isOpenMemberListMenu, isOpen: isOpenMemberListMenu,
onClose: onCloseMemberListMenu, onClose: onCloseMemberListMenu,
@@ -69,10 +61,12 @@ export function ChangeOwnerModal({
'permission' | 'teamId' 'permission' | 'teamId'
> | null>(null); > | null>(null);
const { runAsync, loading } = useRequest2( const [keepAdmin, setKeepAdmin] = useState(true);
(tmbId: string) => putGroupChangeOwner(groupId, tmbId),
const { runAsync: onTransfer, loading } = useRequest2(
(tmbId: string) => putGroupChangeOwner(group._id, tmbId),
{ {
onSuccess: () => Promise.all([onClose(), refetchGroups()]), onSuccess: () => Promise.all([onClose(), onSuccess()]),
successToast: t('common:permission.change_owner_success'), successToast: t('common:permission.change_owner_success'),
errorToast: t('common:permission.change_owner_failed') errorToast: t('common:permission.change_owner_failed')
} }
@@ -82,7 +76,7 @@ export function ChangeOwnerModal({
if (!selectedMember) { if (!selectedMember) {
return; return;
} }
await runAsync(selectedMember.tmbId); await onTransfer(selectedMember.tmbId);
}; };
return ( return (
@@ -92,7 +86,6 @@ export function ChangeOwnerModal({
iconColor="primary.600" iconColor="primary.600"
onClose={onClose} onClose={onClose}
title={t('common:permission.change_owner')} title={t('common:permission.change_owner')}
isLoading={loading}
> >
<ModalBody> <ModalBody>
<HStack> <HStack>
@@ -181,7 +174,9 @@ export function ChangeOwnerModal({
<Button onClick={onClose} variant={'whiteBase'}> <Button onClick={onClose} variant={'whiteBase'}>
{t('common:common.Cancel')} {t('common:common.Cancel')}
</Button> </Button>
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button> <Button isLoading={loading} isDisabled={!selectedMember} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</HStack> </HStack>
</ModalFooter> </ModalFooter>
</MyModal> </MyModal>

View File

@@ -3,7 +3,6 @@ import {
Box, Box,
Button, Button,
Flex, Flex,
HStack,
Table, Table,
TableContainer, TableContainer,
Tbody, Tbody,
@@ -16,20 +15,18 @@ import {
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu'; import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup, getGroupList, getGroupMembers } from '@/web/support/user/team/group/api'; import { deleteGroup, getGroupList } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useState } from 'react'; import { useState } from 'react';
import IconButton from '../OrgManage/IconButton'; import IconButton from '../OrgManage/IconButton';
import { MemberGroupType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal')); const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal')); const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
@@ -39,26 +36,23 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { members, teamSize } = useContextSelector(TeamContext, (v) => v);
const { const {
data: groups = [], data: groups = [],
loading: isLoadingGroups, loading: isLoadingGroups,
refresh: refetchGroups refresh: refetchGroups
} = useRequest2(getGroupList, { } = useRequest2(() => getGroupList<true>({ withMembers: true }), {
manual: false, manual: false,
refreshDeps: [userInfo?.team?.teamId] refreshDeps: [userInfo?.team?.teamId]
}); });
const [editGroup, setEditGroup] = useState<MemberGroupType>(); const [editGroup, setEditGroup] = useState<MemberGroupListItemType<true>>();
const { const {
isOpen: isOpenGroupInfo, isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo, onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo onClose: onCloseGroupInfo
} = useDisclosure(); } = useDisclosure();
const onEditGroupInfo = (e: MemberGroupListItemType<true>) => {
const onEditGroupInfo = (e: MemberGroupType) => {
setEditGroup(e); setEditGroup(e);
onOpenGroupInfo(); onOpenGroupInfo();
}; };
@@ -67,7 +61,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete', type: 'delete',
content: t('account_team:confirm_delete_group') content: t('account_team:confirm_delete_group')
}); });
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, { const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => { onSuccess: () => {
refetchGroups(); refetchGroups();
@@ -79,21 +72,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onOpen: onOpenManageGroupMember, onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember onClose: onCloseManageGroupMember
} = useDisclosure(); } = useDisclosure();
const onManageMember = (e: MemberGroupType) => { const onManageMember = (e: MemberGroupListItemType<true>) => {
setEditGroup(e); setEditGroup(e);
onOpenManageGroupMember(); onOpenManageGroupMember();
}; };
const hasGroupManagePer = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
const isGroupOwner = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
const { const {
isOpen: isOpenChangeOwner, isOpen: isOpenChangeOwner,
onOpen: onOpenChangeOwner, onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner onClose: onCloseChangeOwner
} = useDisclosure(); } = useDisclosure();
const onChangeOwner = (e: MemberGroupType) => { const onChangeOwner = (e: MemberGroupListItemType<true>) => {
setEditGroup(e); setEditGroup(e);
onOpenChangeOwner(); onOpenChangeOwner();
}; };
@@ -134,58 +123,38 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Tbody> <Tbody>
{groups?.map((group) => ( {groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}> <Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
<Box>({group.name === DefaultGroupName ? teamSize : group.count})</Box>
</HStack>
</Td>
<Td> <Td>
<MemberTag <MemberTag
name={ name={
group.name === DefaultGroupName group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: group.owner.name
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: group.owner.avatar
} }
avatar={group.avatar}
/> />
</Td> </Td>
<Td> <Td>
{group.name === DefaultGroupName ? ( <MemberTag name={group.owner?.name} avatar={group.owner?.avatar} />
<AvatarGroup avatars={members.map((v) => v.avatar)} total={teamSize} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
total={group.count}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
total={group.count}
/>
)}
</Td> </Td>
<Td> <Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && ( <MyTooltip
label={group.permission?.hasManagePer ? t('account_team:manage_member') : ''}
>
<Box
{...(group.permission?.hasManagePer
? {
cursor: 'pointer',
onClick: () => onManageMember(group)
}
: {})}
>
<AvatarGroup
avatars={group?.members.map((v) => v.avatar)}
total={group.count}
/>
</Box>
</MyTooltip>
</Td>
<Td>
{group.permission?.hasManagePer && (
<MyMenu <MyMenu
Button={<IconButton name={'more'} />} Button={<IconButton name={'more'} />}
menuList={[ menuList={[
@@ -205,7 +174,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onManageMember(group); onManageMember(group);
} }
}, },
...(isGroupOwner(group) ...(group.permission?.isOwner
? [ ? [
{ {
label: t('account_team:transfer_ownership'), label: t('account_team:transfer_ownership'),
@@ -239,34 +208,33 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</MyBox> </MyBox>
<ConfirmDeleteGroupModal /> <ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal
groupId={editGroup._id}
onClose={onCloseChangeOwner}
groups={groups}
refetchGroups={refetchGroups}
/>
)}
{isOpenGroupInfo && ( {isOpenGroupInfo && (
<GroupInfoModal <GroupInfoModal
groups={groups} editGroup={editGroup}
refetchGroups={refetchGroups} onSuccess={refetchGroups}
onClose={() => { onClose={() => {
onCloseGroupInfo(); onCloseGroupInfo();
setEditGroup(undefined); setEditGroup(undefined);
}} }}
editGroupId={editGroup?._id}
/> />
)} )}
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal
group={editGroup}
onClose={onCloseChangeOwner}
onSuccess={refetchGroups}
/>
)}
{isOpenManageGroupMember && editGroup && ( {isOpenManageGroupMember && editGroup && (
<GroupManageMember <GroupManageMember
groups={groups} group={editGroup}
refetchGroups={refetchGroups}
onClose={() => { onClose={() => {
onCloseManageGroupMember(); onCloseManageGroupMember();
setEditGroup(undefined); setEditGroup(undefined);
}} }}
editGroupId={editGroup._id} onSuccess={refetchGroups}
/> />
)} )}
</> </>

View File

@@ -20,8 +20,9 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea'; import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
import { import {
delRemoveMember, delRemoveMember,
postRestoreMember, getTeamMembers,
putUpdateMemberNameByManager putUpdateMemberNameByManager,
postRestoreMember
} from '@/web/support/user/team/api'; } from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag'; import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon'; import Icon from '@fastgpt/web/components/common/Icon';
@@ -33,7 +34,6 @@ import dynamic from 'next/dynamic';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api'; import { delLeaveTeam } from '@/web/support/user/team/api';
import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api'; import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading';
import { import {
TeamMemberRoleEnum, TeamMemberRoleEnum,
TeamMemberStatusEnum TeamMemberStatusEnum
@@ -41,10 +41,12 @@ import {
import format from 'date-fns/format'; import format from 'date-fns/format';
import OrgTags from '@/components/support/user/team/OrgTags'; import OrgTags from '@/components/support/user/team/OrgTags';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react'; import { useCallback, useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils'; import { downloadFetch } from '@/web/common/system/utils';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const InviteModal = dynamic(() => import('./Invite/InviteModal')); const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@@ -55,20 +57,72 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs?.register_method?.includes('sync');
const { myTeams, refetchTeams, members, refetchMembers, onSwitchTeam, MemberScrollData } = const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
useContextSelector(TeamContext, (v) => v);
const { const {
isOpen: isOpenTeamTagsAsync, isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync, onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync onClose: onCloseTeamTagsAsync
} = useDisclosure(); } = useDisclosure();
// member action
const {
data: members = [],
isLoading: loadingMembers,
refreshList: refetchMemberList,
ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, {
pageSize: 20,
params: {
withLeaved: true
}
});
const [searchText, setSearchText] = useState<string>('');
const { data: searchMembersData, run: refreshSearchMembers } = useRequest2(
async () => {
if (!searchText) return Promise.resolve();
return GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false });
},
{
manual: false,
throttleWait: 500,
refreshDeps: [searchText]
}
);
const onRefreshMembers = useCallback(() => {
refetchMemberList();
refreshSearchMembers();
}, [refetchMemberList, refreshSearchMembers]);
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure(); const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
onSuccess: onRefreshMembers,
successToast: t('account_team:sync_member_success'),
errorToast: t('account_team:sync_member_failed')
});
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const { runAsync: onLeaveTeam } = useRequest2(delLeaveTeam, {
onSuccess() {
const defaultTeam = myTeams[0];
onSwitchTeam(defaultTeam.teamId);
},
errorToast: t('account_team:user_team_leave_team_failed')
});
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({ const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete' type: 'delete'
}); });
const { runAsync: onRemoveMember } = useRequest2(delRemoveMember, {
onSuccess: onRefreshMembers
});
const { ConfirmModal: ConfirmRestoreMemberModal, openConfirm: openRestoreMember } = useConfirm({ const { ConfirmModal: ConfirmRestoreMemberModal, openConfirm: openRestoreMember } = useConfirm({
type: 'common', type: 'common',
@@ -76,59 +130,13 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
iconSrc: 'common/confirm/restoreTip', iconSrc: 'common/confirm/restoreTip',
iconColor: 'primary.500' iconColor: 'primary.500'
}); });
const { runAsync: onRestore } = useRequest2(postRestoreMember, {
const [searchText, setSearchText] = useState<string>(''); onSuccess: onRefreshMembers,
const isSyncMember = feConfigs.register_method?.includes('sync'); successToast: t('common:common.Success'),
const { data: searchMembersData } = useRequest2(
async () => {
if (!searchText) return Promise.resolve();
return GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false });
},
{
manual: false,
throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchText]
}
);
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams[0];
// change to personal team
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam();
},
{
onSuccess() {
refetchTeams();
refetchMembers();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const { runAsync: onSyncMember, loading: isSyncing } = useRequest2(postSyncMembers, {
onSuccess() {
refetchMembers();
},
successToast: t('account_team:sync_member_success'),
errorToast: t('account_team:sync_member_failed')
});
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(postRestoreMember, {
onSuccess() {
refetchMembers();
},
successToast: t('common:user.team.invite.Accepted'),
errorToast: t('common:user.team.invite.Reject') errorToast: t('common:user.team.invite.Reject')
}); });
const isLoading = isUpdateInvite || isSyncing; const isLoading = loadingMembers || isSyncing;
const { EditModal: EditMemberNameModal, onOpenModal: openEditMemberName } = useEditTextarea({ const { EditModal: EditMemberNameModal, onOpenModal: openEditMemberName } = useEditTextarea({
title: t('account_team:edit_member'), title: t('account_team:edit_member'),
@@ -141,7 +149,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
defaultVal: memberName, defaultVal: memberName,
onSuccess: (newName: string) => { onSuccess: (newName: string) => {
return putUpdateMemberNameByManager(tmbId, newName).then(() => { return putUpdateMemberNameByManager(tmbId, newName).then(() => {
refetchMembers(); onRefreshMembers();
}); });
}, },
onError: (err) => { onError: (err) => {
@@ -155,7 +163,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
return ( return (
<> <>
{isLoading && <MyLoading />}
<Flex justify={'space-between'} align={'center'} pb={'1rem'}> <Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs} {Tabs}
<HStack alignItems={'center'}> <HStack alignItems={'center'}>
@@ -237,7 +244,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</HStack> </HStack>
</Flex> </Flex>
<Box flex={'1 0 0'} overflow={'auto'}> <MyBox isLoading={isLoading} flex={'1 0 0'} overflow={'auto'}>
<MemberScrollData> <MemberScrollData>
<TableContainer overflow={'unset'} fontSize={'sm'}> <TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}> <Table overflow={'unset'}>
@@ -321,27 +328,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}} }}
onClick={() => { onClick={() => {
openRemoveMember( openRemoveMember(
() => delRemoveMember(member.tmbId).then(refetchMembers), () => onRemoveMember(member.tmbId),
undefined,
t('account_team:remove_tip', {
username: member.memberName
})
)();
}}
/>
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() => delRemoveMember(member.tmbId).then(refetchMembers),
undefined, undefined,
t('account_team:remove_tip', { t('account_team:remove_tip', {
username: member.memberName username: member.memberName
@@ -385,7 +372,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<EditMemberNameModal /> <EditMemberNameModal />
</TableContainer> </TableContainer>
</MemberScrollData> </MemberScrollData>
</Box> </MyBox>
<ConfirmLeaveTeamModal /> <ConfirmLeaveTeamModal />
{isOpenInvite && userInfo?.team?.teamId && <InviteModal onClose={onCloseInvite} />} {isOpenInvite && userInfo?.team?.teamId && <InviteModal onClose={onCloseInvite} />}

View File

@@ -14,7 +14,7 @@ import {
Tr, Tr,
VStack VStack
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type'; import type { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type'; import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
@@ -32,12 +32,9 @@ import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import Path from '@/components/common/folder/Path'; import Path from '@/components/common/folder/Path';
import { ParentIdType, ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { delRemoveMember } from '@/web/support/user/team/api'; import { delRemoveMember } from '@/web/support/user/team/api';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import useOrg from '@/web/support/user/team/org/hooks/useOrg';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal')); const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));

View File

@@ -13,10 +13,8 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { useRouter } from 'next/router';
const EditInfoModal = dynamic(() => import('./EditInfoModal')); const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -55,6 +53,8 @@ export const TeamContext = createContext<TeamModalContextType>({
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => { export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>(); const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo } = useUserStore(); const { userInfo, initUserInfo } = useUserStore();
@@ -96,10 +96,12 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2( const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => { async (teamId: string) => {
await putSwitchTeam(teamId); await putSwitchTeam(teamId);
refetchMembers();
return initUserInfo(); return initUserInfo();
}, },
{ {
onSuccess: () => {
router.reload();
},
errorToast: t('common:user.team.Switch Team Failed') errorToast: t('common:user.team.Switch Team Failed')
} }
); );

View File

@@ -1,14 +1,19 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request'; import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
import { GetGroupListBody } from '@fastgpt/global/support/permission/memberGroup/api';
import type { import type {
GroupMemberItemType, GroupMemberItemType,
MemberGroupListType MemberGroupListItemType
} from '@fastgpt/global/support/permission/memberGroup/type'; } from '@fastgpt/global/support/permission/memberGroup/type';
import type { import type {
postCreateGroupData, postCreateGroupData,
putUpdateGroupData putUpdateGroupData
} from '@fastgpt/global/support/user/team/group/api'; } from '@fastgpt/global/support/user/team/group/api';
export const getGroupList = () => GET<MemberGroupListType>('/proApi/support/user/team/group/list'); export const getGroupList = <T extends boolean>(data: GetGroupListBody) =>
POST<MemberGroupListItemType<T>[]>('/proApi/support/user/team/group/list', data).then((res) => {
console.log(res);
return res;
});
export const postCreateGroup = (data: postCreateGroupData) => export const postCreateGroup = (data: postCreateGroupData) =>
POST('/proApi/support/user/team/group/create', data); POST('/proApi/support/user/team/group/create', data);