mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 12:20:34 +00:00
perf: member group (#4324)
* sync collection * remove lock * perf: member group
This commit is contained in:
4
packages/global/support/permission/memberGroup/api.d.ts
vendored
Normal file
4
packages/global/support/permission/memberGroup/api.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export type GetGroupListBody = {
|
||||
searchKey?: string;
|
||||
withMembers?: boolean;
|
||||
};
|
@@ -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;
|
||||
|
5
packages/global/support/user/api.d.ts
vendored
5
packages/global/support/user/api.d.ts
vendored
@@ -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';
|
||||
|
@@ -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');
|
||||
|
@@ -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) => ({
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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} />
|
||||
|
@@ -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()])
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@@ -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} />}
|
||||
|
@@ -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'));
|
||||
|
@@ -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')
|
||||
}
|
||||
);
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user