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 { TeamPermission } from '../user/controller';
import { GroupMemberRole } from './constant';
import { Permission } from '../controller';
type MemberGroupSchemaType = {
_id: string;
@@ -16,23 +17,25 @@ type GroupMemberSchemaType = {
role: `${GroupMemberRole}`;
};
type MemberGroupType = MemberGroupSchemaType & {
members: {
tmbId: string;
name: string;
avatar: string;
}[];
count: number;
owner: {
tmbId: string;
name: string;
avatar: string;
};
canEdit: boolean;
type MemberGroupListItemType<T extends boolean | undefined> = MemberGroupSchemaType & {
members: T extends true
? {
tmbId: string;
name: string;
avatar: string;
}[]
: undefined;
count: T extends true ? number : undefined;
owner?: T extends true
? {
tmbId: string;
name: string;
avatar: string;
}
: undefined;
permission: T extends true ? Permission : undefined;
};
type MemberGroupListType = MemberGroupType[];
type GroupMemberItemType = {
tmbId: 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 { TrackRegisterParams } from './login/api';
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 }) {
if (!tmbId) {
return Promise.reject('tmbId or userId is required');

View File

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

View File

@@ -3,16 +3,16 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import React from 'react';
type Props = {
name: string;
avatar: string;
name?: string;
avatar?: string;
};
function MemberTag({ name, avatar }: Props) {
return (
<HStack>
<Avatar src={avatar} w={['18px', '22px']} rounded="50%" />
{avatar && <Avatar src={avatar} w={['18px', '22px']} rounded="50%" />}
<Box maxW={'150px'} className={'textEllipsis'}>
{name}
{name || '-'}
</Box>
</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 { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import React from 'react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
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 { 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 = {
avatar: string;
@@ -21,17 +19,15 @@ export type GroupFormType = {
function GroupInfoModal({
onClose,
editGroupId,
groups,
refetchGroups
editGroup,
onSuccess
}: {
onClose: () => void;
editGroupId?: string;
groups: MemberGroupListType;
refetchGroups: () => void;
editGroup?: MemberGroupListItemType<true>;
onSuccess: () => void;
}) {
const { refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation();
const {
File: AvatarSelect,
onOpen: onOpenSelectAvatar,
@@ -41,14 +37,10 @@ function GroupInfoModal({
multiple: false
});
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const { register, handleSubmit, getValues, setValue } = useForm<GroupFormType>({
defaultValues: {
name: group?.name || '',
avatar: group?.avatar || DEFAULT_TEAM_AVATAR
name: editGroup?.name || '',
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(
async (data: GroupFormType) => {
if (!editGroupId) return;
if (!editGroup) return;
return putUpdateGroup({
groupId: editGroupId,
groupId: editGroup._id,
name: data.name,
avatar: data.avatar
});
},
{
onSuccess: () => Promise.all([onClose(), refetchGroups(), refetchMembers()])
onSuccess: () => Promise.all([onClose(), onSuccess()])
}
);
@@ -97,8 +89,8 @@ function GroupInfoModal({
return (
<MyModal
onClose={onClose}
title={editGroupId ? t('user:team.group.edit') : t('user:team.group.create')}
iconSrc={group?.avatar ?? DEFAULT_TEAM_AVATAR}
title={editGroup ? t('user:team.group.edit') : t('user:team.group.create')}
iconSrc={editGroup?.avatar ?? DEFAULT_TEAM_AVATAR}
>
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
@@ -120,14 +112,14 @@ function GroupInfoModal({
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (editGroupId) {
if (editGroup) {
onUpdate(data);
} else {
onCreate(data);
}
})}
>
{editGroupId ? t('common:common.Save') : t('common:new_create')}
{editGroup ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<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 {
GroupMemberItemType,
MemberGroupListType
MemberGroupListItemType
} from '@fastgpt/global/support/permission/memberGroup/type';
import { useMount } from 'ahooks';
@@ -43,24 +43,19 @@ export type GroupFormType = {
function GroupEditModal({
onClose,
editGroupId,
groups,
refetchGroups
group,
onSuccess
}: {
onClose: () => void;
editGroupId?: string;
groups: MemberGroupListType;
refetchGroups: () => void;
group: MemberGroupListItemType<true>;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { toast } = useToast();
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
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 {
Box,
Flex,
@@ -15,26 +15,24 @@ 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 React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import { TeamContext } from '../context';
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 { Omit } from '@fastgpt/web/components/common/DndDrag';
export type ChangeOwnerModalProps = {
groupId: string;
groups: MemberGroupListType;
refetchGroups: () => void;
};
export function ChangeOwnerModal({
onClose,
groupId,
groups,
refetchGroups
}: ChangeOwnerModalProps & { onClose: () => void }) {
group,
onSuccess,
onClose
}: {
group: MemberGroupListItemType<true>;
onSuccess: () => void;
onClose: () => void;
}) {
const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState('');
const { data: searchedData } = useRequest2(
async () => {
@@ -50,14 +48,8 @@ export function ChangeOwnerModal({
);
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 [keepAdmin, setKeepAdmin] = useState(true);
const {
isOpen: isOpenMemberListMenu,
onClose: onCloseMemberListMenu,
@@ -69,10 +61,12 @@ export function ChangeOwnerModal({
'permission' | 'teamId'
> | null>(null);
const { runAsync, loading } = useRequest2(
(tmbId: string) => putGroupChangeOwner(groupId, tmbId),
const [keepAdmin, setKeepAdmin] = useState(true);
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'),
errorToast: t('common:permission.change_owner_failed')
}
@@ -82,7 +76,7 @@ export function ChangeOwnerModal({
if (!selectedMember) {
return;
}
await runAsync(selectedMember.tmbId);
await onTransfer(selectedMember.tmbId);
};
return (
@@ -92,7 +86,6 @@ export function ChangeOwnerModal({
iconColor="primary.600"
onClose={onClose}
title={t('common:permission.change_owner')}
isLoading={loading}
>
<ModalBody>
<HStack>
@@ -181,7 +174,9 @@ export function ChangeOwnerModal({
<Button onClick={onClose} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button onClick={onConfirm}>{t('common:common.Confirm')}</Button>
<Button isLoading={loading} isDisabled={!selectedMember} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</HStack>
</ModalFooter>
</MyModal>

View File

@@ -3,7 +3,6 @@ import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
Tbody,
@@ -16,20 +15,18 @@ import {
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
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 MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore';
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 MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
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 GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
@@ -39,26 +36,23 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { members, teamSize } = useContextSelector(TeamContext, (v) => v);
const {
data: groups = [],
loading: isLoadingGroups,
refresh: refetchGroups
} = useRequest2(getGroupList, {
} = useRequest2(() => getGroupList<true>({ withMembers: true }), {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const [editGroup, setEditGroup] = useState<MemberGroupType>();
const [editGroup, setEditGroup] = useState<MemberGroupListItemType<true>>();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const onEditGroupInfo = (e: MemberGroupType) => {
const onEditGroupInfo = (e: MemberGroupListItemType<true>) => {
setEditGroup(e);
onOpenGroupInfo();
};
@@ -67,7 +61,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete',
content: t('account_team:confirm_delete_group')
});
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => {
refetchGroups();
@@ -79,21 +72,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onManageMember = (e: MemberGroupType) => {
const onManageMember = (e: MemberGroupListItemType<true>) => {
setEditGroup(e);
onOpenManageGroupMember();
};
const hasGroupManagePer = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
const isGroupOwner = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
const {
isOpen: isOpenChangeOwner,
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (e: MemberGroupType) => {
const onChangeOwner = (e: MemberGroupListItemType<true>) => {
setEditGroup(e);
onOpenChangeOwner();
};
@@ -134,58 +123,38 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Tbody>
{groups?.map((group) => (
<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>
<MemberTag
name={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: group.owner.name
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: group.owner.avatar
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
avatar={group.avatar}
/>
</Td>
<Td>
{group.name === DefaultGroupName ? (
<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}
/>
)}
<MemberTag name={group.owner?.name} avatar={group.owner?.avatar} />
</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
Button={<IconButton name={'more'} />}
menuList={[
@@ -205,7 +174,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onManageMember(group);
}
},
...(isGroupOwner(group)
...(group.permission?.isOwner
? [
{
label: t('account_team:transfer_ownership'),
@@ -239,34 +208,33 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</MyBox>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal
groupId={editGroup._id}
onClose={onCloseChangeOwner}
groups={groups}
refetchGroups={refetchGroups}
/>
)}
{isOpenGroupInfo && (
<GroupInfoModal
groups={groups}
refetchGroups={refetchGroups}
editGroup={editGroup}
onSuccess={refetchGroups}
onClose={() => {
onCloseGroupInfo();
setEditGroup(undefined);
}}
editGroupId={editGroup?._id}
/>
)}
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal
group={editGroup}
onClose={onCloseChangeOwner}
onSuccess={refetchGroups}
/>
)}
{isOpenManageGroupMember && editGroup && (
<GroupManageMember
groups={groups}
refetchGroups={refetchGroups}
group={editGroup}
onClose={() => {
onCloseManageGroupMember();
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 {
delRemoveMember,
postRestoreMember,
putUpdateMemberNameByManager
getTeamMembers,
putUpdateMemberNameByManager,
postRestoreMember
} from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag';
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 { delLeaveTeam } from '@/web/support/user/team/api';
import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading';
import {
TeamMemberRoleEnum,
TeamMemberStatusEnum
@@ -41,10 +41,12 @@ import {
import format from 'date-fns/format';
import OrgTags from '@/components/support/user/team/OrgTags';
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 { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
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 TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@@ -55,20 +57,72 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs?.register_method?.includes('sync');
const { myTeams, refetchTeams, members, refetchMembers, onSwitchTeam, MemberScrollData } =
useContextSelector(TeamContext, (v) => v);
const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = 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 { 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({
type: 'delete'
});
const { runAsync: onRemoveMember } = useRequest2(delRemoveMember, {
onSuccess: onRefreshMembers
});
const { ConfirmModal: ConfirmRestoreMemberModal, openConfirm: openRestoreMember } = useConfirm({
type: 'common',
@@ -76,59 +130,13 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
iconSrc: 'common/confirm/restoreTip',
iconColor: 'primary.500'
});
const [searchText, setSearchText] = useState<string>('');
const isSyncMember = feConfigs.register_method?.includes('sync');
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'),
const { runAsync: onRestore } = useRequest2(postRestoreMember, {
onSuccess: onRefreshMembers,
successToast: t('common:common.Success'),
errorToast: t('common:user.team.invite.Reject')
});
const isLoading = isUpdateInvite || isSyncing;
const isLoading = loadingMembers || isSyncing;
const { EditModal: EditMemberNameModal, onOpenModal: openEditMemberName } = useEditTextarea({
title: t('account_team:edit_member'),
@@ -141,7 +149,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
defaultVal: memberName,
onSuccess: (newName: string) => {
return putUpdateMemberNameByManager(tmbId, newName).then(() => {
refetchMembers();
onRefreshMembers();
});
},
onError: (err) => {
@@ -155,7 +163,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
return (
<>
{isLoading && <MyLoading />}
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack alignItems={'center'}>
@@ -237,7 +244,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</HStack>
</Flex>
<Box flex={'1 0 0'} overflow={'auto'}>
<MyBox isLoading={isLoading} flex={'1 0 0'} overflow={'auto'}>
<MemberScrollData>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
@@ -321,27 +328,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}}
onClick={() => {
openRemoveMember(
() => delRemoveMember(member.tmbId).then(refetchMembers),
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),
() => onRemoveMember(member.tmbId),
undefined,
t('account_team:remove_tip', {
username: member.memberName
@@ -385,7 +372,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<EditMemberNameModal />
</TableContainer>
</MemberScrollData>
</Box>
</MyBox>
<ConfirmLeaveTeamModal />
{isOpenInvite && userInfo?.team?.teamId && <InviteModal onClose={onCloseInvite} />}

View File

@@ -14,7 +14,7 @@ import {
Tr,
VStack
} 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 MyIcon from '@fastgpt/web/components/common/Icon';
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 MyBox from '@fastgpt/web/components/common/MyBox';
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 { delRemoveMember } from '@/web/support/user/team/api';
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';
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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
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 { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useRouter } from 'next/router';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -55,6 +53,8 @@ export const TeamContext = createContext<TeamModalContextType>({
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const router = useRouter();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo } = useUserStore();
@@ -96,10 +96,12 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
refetchMembers();
return initUserInfo();
},
{
onSuccess: () => {
router.reload();
},
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 { GetGroupListBody } from '@fastgpt/global/support/permission/memberGroup/api';
import type {
GroupMemberItemType,
MemberGroupListType
MemberGroupListItemType
} from '@fastgpt/global/support/permission/memberGroup/type';
import type {
postCreateGroupData,
putUpdateGroupData
} 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) =>
POST('/proApi/support/user/team/group/create', data);