diff --git a/packages/global/support/permission/memberGroup/type.d.ts b/packages/global/support/permission/memberGroup/type.d.ts index 7045cf534..f70e9a89e 100644 --- a/packages/global/support/permission/memberGroup/type.d.ts +++ b/packages/global/support/permission/memberGroup/type.d.ts @@ -19,9 +19,23 @@ type GroupMemberSchemaType = { type MemberGroupType = MemberGroupSchemaType & { members: { tmbId: string; - role: `${GroupMemberRole}`; - }[]; // we can get tmb's info from other api. there is no need but only need to get tmb's id - permission: TeamPermission; + name: string; + avatar: string; + }[]; + count: number; + owner: { + tmbId: string; + name: string; + avatar: string; + }; + canEdit: boolean; }; type MemberGroupListType = MemberGroupType[]; + +type GroupMemberItemType = { + tmbId: string; + name: string; + avatar: string; + role: `${GroupMemberRole}`; +}; diff --git a/packages/global/support/user/team/org/type.d.ts b/packages/global/support/user/team/org/type.d.ts index b742ece48..b25384a6e 100644 --- a/packages/global/support/user/team/org/type.d.ts +++ b/packages/global/support/user/team/org/type.d.ts @@ -1,5 +1,6 @@ import type { TeamPermission } from 'support/permission/user/controller'; import { ResourcePermissionType } from '../type'; +import { SourceMemberType } from 'support/user/type'; type OrgSchemaType = { _id: string; @@ -23,4 +24,5 @@ type OrgType = Omit & { avatar: string; permission: TeamPermission; members: OrgMemberSchemaType[]; + total: number; // members + children orgs }; diff --git a/packages/global/support/user/team/type.d.ts b/packages/global/support/user/team/type.d.ts index 3be913870..c715d93b5 100644 --- a/packages/global/support/user/team/type.d.ts +++ b/packages/global/support/user/team/type.d.ts @@ -82,6 +82,7 @@ export type TeamMemberItemType = { contact?: string; createTime: Date; updateTime?: Date; + orgs?: string[]; // full path name, pattern: /teamName/orgname1/orgname2 }; export type TeamTagItemType = { diff --git a/packages/service/support/permission/memberGroup/memberGroupSchema.ts b/packages/service/support/permission/memberGroup/memberGroupSchema.ts index 6964785bc..cc4ee05bc 100644 --- a/packages/service/support/permission/memberGroup/memberGroupSchema.ts +++ b/packages/service/support/permission/memberGroup/memberGroupSchema.ts @@ -1,6 +1,7 @@ import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; +import { GroupMemberCollectionName } from './groupMemberSchema'; const { Schema } = connectionMongo; export const MemberGroupCollectionName = 'team_member_groups'; diff --git a/packages/service/support/permission/org/controllers.ts b/packages/service/support/permission/org/controllers.ts index ce0746055..dd0d7dbab 100644 --- a/packages/service/support/permission/org/controllers.ts +++ b/packages/service/support/permission/org/controllers.ts @@ -90,6 +90,6 @@ export async function createRootOrg({ path: '' } ], - { session } + { session, ordered: true } ); } diff --git a/packages/web/components/common/Avatar/AvatarGroup.tsx b/packages/web/components/common/Avatar/AvatarGroup.tsx index 6f9f3ce03..947e488bf 100644 --- a/packages/web/components/common/Avatar/AvatarGroup.tsx +++ b/packages/web/components/common/Avatar/AvatarGroup.tsx @@ -10,7 +10,16 @@ import { Box, Flex } from '@chakra-ui/react'; * @param [groupId] - group id to make the key unique * @returns */ -function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] }) { +function AvatarGroup({ + avatars, + max = 3, + total +}: { + max?: number; + avatars: string[]; + total?: number; +}) { + const remain = total ?? avatars.length - max; return ( {avatars.slice(0, max).map((avatar, index) => ( @@ -24,10 +33,10 @@ function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] }) borderRadius={'50%'} /> ))} - {avatars.length > max && ( + {remain > 0 && ( - +{avatars.length - max} + +{String(remain)} )} diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index ab6138c35..da5468d8f 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -35,7 +35,7 @@ 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 } from '@/web/support/user/team/org/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'; @@ -57,14 +57,52 @@ function MemberModal({ const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const [searchText, setSearchText] = useState(''); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); - const { data: members, ScrollData } = useScrollPagination(getTeamMembers, { + const [path, setPath] = useState(''); + const [orgStack, setOrgStack] = useState([]); + const currentOrg = useMemo(() => orgStack[orgStack.length - 1], [orgStack]); + + const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, { pageSize: 15 }); - const { data: [groups = [], orgs = []] = [], loading: loadingGroupsAndOrgs } = useRequest2( + const [rootOrg, setRootOrg] = useState(); + const { data: orgMembers = [], ScrollData: OrgMemberScrollData } = useScrollPagination( + getOrgMembers, + { + pageSize: 20, + params: { + orgId: currentOrg?._id ?? rootOrg?._id + }, + refreshDeps: [currentOrg?._id] + } + ); + const onClickOrg = (org: OrgType) => { + setOrgStack([...orgStack, org]); + setPath(getOrgChildrenPath(org)); + }; + + const { data: orgs = [] } = useRequest2( + () => { + const splitPath = path.split('/').filter(Boolean); + const orgs = orgStack.filter((o) => splitPath.includes(o.pathId)); + setOrgStack(orgs); + return getOrgList(path); + }, + { + manual: false, + refreshDeps: [path], + onSuccess: (data) => { + if (!rootOrg) { + setRootOrg(data[0]); + } + } + } + ); + + const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2( async () => { - if (!userInfo?.team?.teamId) return [[], []]; - return Promise.all([getGroupList(), getOrgList()]); + if (!userInfo?.team?.teamId) return []; + return getGroupList(); }, { manual: false, @@ -72,69 +110,49 @@ function MemberModal({ } ); - const [parentPath, setParentPath] = useState(''); - const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), { manual: false, throttleWait: 500, + debounceWait: 200, refreshDeps: [searchText] }); const paths = useMemo(() => { - const splitPath = parentPath.split('/').filter(Boolean); - return splitPath - .map((id) => { - const org = orgs.find((org) => org.pathId === id)!; - - if (org.path === '') return; - + return orgStack + .map((org) => { + if (org?.path === '') return; return { parentId: getOrgChildrenPath(org), parentName: org.name }; }) .filter(Boolean) as ParentTreePathItemType[]; - }, [parentPath, orgs]); + }, [orgStack]); const [selectedOrgIdList, setSelectedOrgIdList] = useState([]); - const currentOrg = useMemo(() => { - const splitPath = parentPath.split('/'); - const currentOrgId = splitPath[splitPath.length - 1]; - if (!currentOrgId) return; - return orgs.find((org) => org.pathId === currentOrgId); - }, [orgs, parentPath]); const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => { if (searchText && searchedData) { const orgids = searchedData.orgs.map((item) => item._id); return orgs.filter((org) => orgids.includes(String(org._id))); } - if (!searchText && filterClass !== 'org') return []; - if (parentPath === '') { - setParentPath(`/${orgs[0].pathId}`); - return []; - } return orgs - .filter((org) => org.path === parentPath) - .map((item) => ({ - ...item, - count: - item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length + .filter((org) => org.path !== '') + .map((org) => ({ + ...org, + count: org.total })); - }, [searchText, filterClass, parentPath, orgs, searchedData]); + }, [searchText, orgs, searchedData]); const [selectedMemberIdList, setSelectedMembers] = useState([]); const filterMembers = useMemo(() => { if (searchText) { return searchedData?.members || []; } - if (!searchText && filterClass !== 'member' && filterClass !== 'org') return []; - if (currentOrg && filterClass === 'org') { - return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId)); - } return members; - }, [members, searchedData, searchText, filterClass, currentOrg]); + }, [searchText, members, searchedData?.members]); + console.log(filterMembers); const [selectedGroupIdList, setSelectedGroupIdList] = useState([]); const filterGroups = useMemo(() => { @@ -197,19 +215,22 @@ function MemberModal({ id: `org-${item._id}`, avatar: item.avatar, name: item.name, - onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id)) + onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id)), + orgs: undefined })), ...selectedGroups.map((item) => ({ id: `group-${item._id}`, avatar: item.avatar, name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, - onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id)) + onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id)), + orgs: undefined })), ...selectedMembers.map((item) => ({ id: `member-${item.tmbId}`, avatar: item.avatar, name: item.memberName, - onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId)) + onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId)), + orgs: item.orgs })) ]; }, [ @@ -303,31 +324,57 @@ function MemberModal({ onClick={(parentId) => { if (parentId === '') { setFilterClass(undefined); - setParentPath(''); + setPath(''); } else if ( parentId === 'member' || parentId === 'org' || parentId === 'group' ) { setFilterClass(parentId); - setParentPath(''); + setPath(''); } else { - setParentPath(parentId); + setPath(parentId); } }} rootName={t('common:common.Team')} /> )} - - {(filterClass === 'org' || filterClass === 'member') && ( - 0)) && ( + - {filterOrgs?.map((org) => { + {filterMembers?.map((member) => { + const onChange = () => { + setSelectedMembers((state) => { + if (state.includes(member.tmbId)) { + return state.filter((v) => v !== member.tmbId); + } + return [...state, member.tmbId]; + }); + }; + const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); + return ( + + ); + })} + + )} + + {(filterClass === 'org' || searchText) && + (() => { + const orgs = filterOrgs?.map((org) => { const onChange = () => { setSelectedOrgIdList((state) => { if (state.includes(org._id)) { @@ -374,47 +421,42 @@ function MemberModal({ bgColor: 'myGray.200' }} onClick={(e) => { - setParentPath(getOrgChildrenPath(org)); + onClickOrg(org); + // setPath(getOrgChildrenPath(org)); e.stopPropagation(); }} /> )} ); - })} - {filterMembers?.map((member) => { - const onChange = () => { - setSelectedMembers((state) => { - if (state.includes(member.tmbId)) { - return state.filter((v) => v !== member.tmbId); - } - return [...state, member.tmbId]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); - const memberOrgs = orgs.filter((org) => - org.members.find((v) => String(v.tmbId) === String(member.tmbId)) - ); - const memberPathIds = memberOrgs.map((org) => - (org.path + '/' + org.pathId).split('/').slice(0) - ); - const memberOrgNames = memberPathIds.map((pathIds) => - pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/') - ); - return ( - - ); - })} - - )} + }); + return searchText ? ( + orgs + ) : ( + + {orgs} + {orgMembers.map((member) => { + return ( + { + setSelectedMembers((state) => { + if (state.includes(member.tmbId)) { + return state.filter((v) => v !== member.tmbId); + } + return [...state, member.tmbId]; + }); + }} + isChecked={selectedMemberIdList.includes(member.tmbId)} + orgs={member.orgs} + /> + ); + })} + + ); + })()} {filterGroups?.map((group) => { const onChange = () => { setSelectedGroupIdList((state) => { @@ -455,20 +497,7 @@ function MemberModal({ name={item.name ?? ''} onChange={item.onDelete} onDelete={item.onDelete} - orgs={(() => { - if (!item.id.startsWith('member-')) return []; - const id = item.id.replace('member-', ''); - const memberOrgs = orgs.filter((org) => - org.members.find((v) => v.tmbId === id) - ); - const memberPathIds = memberOrgs.map((org) => - (org.path + '/' + org.pathId).split('/').slice(0) - ); - const memberOrgNames = memberPathIds.map((pathIds) => - pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/') - ); - return memberOrgNames; - })()} + orgs={item?.orgs} /> ); })} diff --git a/projects/app/src/components/support/user/team/OrgTags/index.tsx b/projects/app/src/components/support/user/team/OrgTags/index.tsx index eebd6a26e..31f97a521 100644 --- a/projects/app/src/components/support/user/team/OrgTags/index.tsx +++ b/projects/app/src/components/support/user/team/OrgTags/index.tsx @@ -4,8 +4,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import Tag from '@fastgpt/web/components/common/Tag'; import React from 'react'; -function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' | 'tag' }) { - return ( +function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' | 'tag' }) { + return orgs?.length ? ( @@ -39,6 +39,10 @@ function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' | )} + ) : ( + + - + ); } diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx index fbd8df13b..7cd017e2d 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupManageMember.tsx @@ -18,7 +18,7 @@ import React, { useMemo, useRef, useState } from 'react'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from '../context'; -import { putUpdateGroup } from '@/web/support/user/team/group/api'; +import { getGroupMembers, putUpdateGroup } from '@/web/support/user/team/group/api'; import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; import { useUserStore } from '@/web/support/user/useUserStore'; import { useToast } from '@fastgpt/web/hooks/useToast'; @@ -46,13 +46,26 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro return groups.find((item) => item._id === editGroupId); }, [editGroupId, groups]); + const { data: groupMembers } = useRequest2( + () => { + if (editGroupId) return getGroupMembers(editGroupId); + return Promise.resolve(undefined); + }, + { + manual: false, + onSuccess: (data) => { + setMembers(data ?? []); + } + } + ); + 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(); const selectedMembersRef = useRef(null); - const [members, setMembers] = useState(group?.members || []); + const [members, setMembers] = useState(groupMembers || []); const [searchKey, setSearchKey] = useState(''); const filtered = useMemo(() => { @@ -67,6 +80,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2( async () => { if (!editGroupId || !members.length) return; + console.log(members); return putUpdateGroup({ groupId: editGroupId, memberList: members @@ -89,10 +103,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro }, [members, userInfo]); const handleToggleSelect = (memberId: string) => { - if ( - myRole === 'owner' && - memberId === group?.members.find((item) => item.role === 'owner')?.tmbId - ) { + if (myRole === 'owner' && memberId === members.find((item) => item.role === 'owner')?.tmbId) { toast({ title: t('user:team.group.toast.can_not_delete_owner'), status: 'error' @@ -102,7 +113,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro if ( myRole === 'admin' && - group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member' + members.find((item) => String(item.tmbId) === memberId)?.role !== 'member' ) { return; } @@ -110,7 +121,17 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro if (isSelected(memberId)) { setMembers(members.filter((item) => item.tmbId !== memberId)); } else { - setMembers([...members, { tmbId: memberId, role: 'member' }]); + const member = allMembers.find((m) => m.tmbId === memberId); + if (!member) return; + setMembers([ + ...members, + { + name: member.memberName, + avatar: member.avatar, + tmbId: member.tmbId, + role: 'member' + } + ]); } }; @@ -188,7 +209,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro {t('common:chosen') + ': ' + members.length} - {members.map((member) => { + {members?.map((member) => { return ( setHoveredMemberId(member.tmbId)} @@ -202,14 +223,8 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro _notLast={{ mb: 2 }} > - item.tmbId === member.tmbId)?.avatar} - w="1.5rem" - borderRadius={'md'} - /> - - {allMembers.find((item) => item.tmbId === member.tmbId)?.memberName} - + + {member.name} {(() => { diff --git a/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx b/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx index 3a583fbf0..aab5696b1 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/GroupTransferOwnerModal.tsx @@ -1,4 +1,4 @@ -import { putUpdateGroup } from '@/web/support/user/team/group/api'; +import { putGroupChangeOwner, putUpdateGroup } from '@/web/support/user/team/group/api'; import { Box, Flex, @@ -38,10 +38,6 @@ export function ChangeOwnerModal({ return item.memberName.toLowerCase().includes(inputValue.toLowerCase()); }); - const OldOwnerId = useMemo(() => { - return group?.members.find((item) => item.role === 'owner')?.tmbId; - }, [group]); - const [keepAdmin, setKeepAdmin] = useState(true); const { @@ -52,36 +48,14 @@ export function ChangeOwnerModal({ const [selectedMember, setSelectedMember] = useState(null); - const onChangeOwner = async (tmbId: string) => { - if (!group) { - return; + const { runAsync, loading } = useRequest2( + (tmbId: string) => putGroupChangeOwner(groupId, tmbId), + { + onSuccess: () => Promise.all([onClose(), refetchGroups()]), + successToast: t('common:permission.change_owner_success'), + errorToast: t('common:permission.change_owner_failed') } - - const newMemberList = group.members - .map((item) => { - if (item.tmbId === OldOwnerId) { - if (keepAdmin) { - return { tmbId: OldOwnerId, role: 'admin' }; - } - return { tmbId: OldOwnerId, role: 'member' }; - } - return item; - }) - .filter((item) => item.tmbId !== tmbId) as any; - - newMemberList.push({ tmbId, role: 'owner' }); - - return putUpdateGroup({ - groupId, - memberList: newMemberList - }); - }; - - const { runAsync, loading } = useRequest2(onChangeOwner, { - onSuccess: () => Promise.all([onClose(), refetchGroups()]), - successToast: t('common:permission.change_owner_success'), - errorToast: t('common:permission.change_owner_failed') - }); + ); const onConfirm = async () => { if (!selectedMember) { diff --git a/projects/app/src/pageComponents/account/team/GroupManage/index.tsx b/projects/app/src/pageComponents/account/team/GroupManage/index.tsx index dc9ae458c..f411bc3f5 100644 --- a/projects/app/src/pageComponents/account/team/GroupManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/GroupManage/index.tsx @@ -22,7 +22,7 @@ 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 } from '@/web/support/user/team/group/api'; +import { deleteGroup, getGroupMembers } 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'; @@ -39,10 +39,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { const { t } = useTranslation(); const { userInfo } = useUserStore(); - const { groups, refetchGroups, members, refetchMembers } = useContextSelector( - TeamContext, - (v) => v - ); + const { groups, refetchGroups, members, teamSize } = useContextSelector(TeamContext, (v) => v); const [editGroup, setEditGroup] = useState(); const { @@ -64,7 +61,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, { onSuccess: () => { refetchGroups(); - refetchMembers(); } }); @@ -78,14 +74,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { onOpenManageGroupMember(); }; - const hasGroupManagePer = (group: (typeof groups)[0]) => - userInfo?.team.permission.hasManagePer || - ['admin', 'owner'].includes( - group.members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? '' - ); - const isGroupOwner = (group: (typeof groups)[0]) => - userInfo?.team.permission.hasManagePer || - group.members.find((item) => item.role === 'owner')?.tmbId === userInfo?.team.tmbId; + const hasGroupManagePer = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer; + + const isGroupOwner = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer; const { isOpen: isOpenChangeOwner, @@ -143,9 +134,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { } avatar={group.avatar} /> - - ({group.name === DefaultGroupName ? members.length : group.members.length}) - + ({group.name === DefaultGroupName ? teamSize : group.count}) @@ -153,26 +142,18 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { name={ group.name === DefaultGroupName ? members.find((item) => item.role === 'owner')?.memberName ?? '' - : members.find( - (item) => - item.tmbId === - group.members.find((item) => item.role === 'owner')?.tmbId - )?.memberName ?? '' + : group.owner.name } avatar={ group.name === DefaultGroupName ? members.find((item) => item.role === 'owner')?.avatar ?? '' - : members.find( - (i) => - i.tmbId === - group.members.find((item) => item.role === 'owner')?.tmbId - )?.avatar ?? '' + : group.owner.avatar } /> {group.name === DefaultGroupName ? ( - v.avatar)} /> + v.avatar)} total={teamSize} /> ) : hasGroupManagePer(group) ? ( onManageMember(group)}> @@ -180,6 +161,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { avatars={group.members.map( (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' )} + total={group.count} /> @@ -188,6 +170,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { avatars={group.members.map( (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' )} + total={group.count} /> )} diff --git a/projects/app/src/pageComponents/account/team/MemberTable.tsx b/projects/app/src/pageComponents/account/team/MemberTable.tsx index 7055244e6..d6232c549 100644 --- a/projects/app/src/pageComponents/account/team/MemberTable.tsx +++ b/projects/app/src/pageComponents/account/team/MemberTable.tsx @@ -30,8 +30,6 @@ import { TeamContext } from './context'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import MyIcon from '@fastgpt/web/components/common/Icon'; import dynamic from 'next/dynamic'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { delLeaveTeam } from '@/web/support/user/team/api'; import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api'; @@ -52,9 +50,8 @@ const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTa function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { const { t } = useTranslation(); - const { toast } = useToast(); - const { userInfo, teamPlanStatus } = useUserStore(); - const { feConfigs, setNotSufficientModalType } = useSystemStore(); + const { userInfo } = useUserStore(); + const { feConfigs } = useSystemStore(); const { refetchGroups, @@ -63,8 +60,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { members, refetchMembers, onSwitchTeam, - MemberScrollData, - orgs + MemberScrollData } = useContextSelector(TeamContext, (v) => v); const { @@ -93,6 +89,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { { manual: false, throttleWait: 500, + debounceWait: 200, refreshDeps: [searchText] } ); @@ -281,16 +278,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { {member.contact || '-'} {(() => { - const memberOrgs = orgs.filter((org) => - org.members.find((v) => String(v.tmbId) === String(member.tmbId)) - ); - const memberPathIds = memberOrgs.map((org) => - (org.path + '/' + org.pathId).split('/').slice(0) - ); - const memberOrgNames = memberPathIds.map((pathIds) => - pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/') - ); - return ; + return ; })()} diff --git a/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx b/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx index ca726383a..74e63bd8f 100644 --- a/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx +++ b/projects/app/src/pageComponents/account/team/OrgManage/OrgMemberManageModal.tsx @@ -1,4 +1,4 @@ -import { putUpdateOrgMembers } from '@/web/support/user/team/org/api'; +import { getOrgMembers, putUpdateOrgMembers } from '@/web/support/user/team/org/api'; import { Box, Button, @@ -18,10 +18,11 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useTranslation } from 'next-i18next'; import type React from 'react'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { TeamContext } from '../context'; import { OrgType } from '@fastgpt/global/support/user/team/org/type'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; export type GroupFormType = { members: { @@ -51,11 +52,21 @@ function OrgMemberManageModal({ }) { const { t } = useTranslation(); const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v); + const { data: orgMembers, ScrollData: OrgMemberScrollData } = useScrollPagination(getOrgMembers, { + pageSize: 20, + params: { + orgId: currentOrg?._id ?? '' + } + }); const [selectedMembers, setSelectedMembers] = useState( - currentOrg.members.map((item) => item.tmbId) + orgMembers.map((item) => item.tmbId) ); + useEffect(() => { + setSelectedMembers(orgMembers.map((item) => item.tmbId)); + }, [orgMembers]); + const [searchKey, setSearchKey] = useState(''); const filterMembers = useMemo(() => { if (!searchKey) return allMembers; @@ -150,9 +161,10 @@ function OrgMemberManageModal({ })} - - {`${t('common:chosen')}:${selectedMembers.length}`} - + {/* */} + + + {`${t('common:chosen')}:${selectedMembers.length}`} {selectedMembers.map((tmbId) => { const member = allMembers.find((item) => item.tmbId === tmbId)!; return ( @@ -179,7 +191,7 @@ function OrgMemberManageModal({ ); })} - + diff --git a/projects/app/src/pageComponents/account/team/OrgManage/index.tsx b/projects/app/src/pageComponents/account/team/OrgManage/index.tsx index e4e2c5cda..5b5fc4f81 100644 --- a/projects/app/src/pageComponents/account/team/OrgManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/OrgManage/index.tsx @@ -26,7 +26,12 @@ import { useMemo, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import MemberTag from '@/components/support/user/team/Info/MemberTag'; import { TeamContext } from '../context'; -import { deleteOrg, deleteOrgMember, getOrgList } from '@/web/support/user/team/org/api'; +import { + deleteOrg, + deleteOrgMember, + getOrgList, + getOrgMembers +} from '@/web/support/user/team/org/api'; import IconButton from './IconButton'; import { defaultOrgForm, type OrgFormType } from './OrgInfoModal'; @@ -37,8 +42,9 @@ import Path from '@/components/common/folder/Path'; import { 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 { delRemoveMember, getTeamMembers } from '@/web/support/user/team/api'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; const OrgInfoModal = dynamic(() => import('./OrgInfoModal')); const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal')); @@ -77,66 +83,63 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { const { t } = useTranslation(); const { userInfo, isTeamAdmin } = useUserStore(); const [searchOrg, setSearchOrg] = useState(''); + const [orgStack, setOrgStack] = useState([]); + const currentOrg = useMemo(() => orgStack[orgStack.length - 1], [orgStack]); + + const [rootOrg, setRootOrg] = useState(); + + const { data: members = [], ScrollData: MemberScrollData } = useScrollPagination(getOrgMembers, { + pageSize: 20, + params: { + orgId: currentOrg?._id ?? rootOrg?._id + }, + refreshDeps: [currentOrg?._id, rootOrg?._id] + }); - const { members, MemberScrollData, refetchMembers } = useContextSelector(TeamContext, (v) => v); const { feConfigs } = useSystemStore(); const isSyncMember = feConfigs.register_method?.includes('sync'); - const [parentPath, setParentPath] = useState(''); + const [path, setPath] = useState(''); + const { data: orgs = [], loading: isLoadingOrgs, refresh: refetchOrgs - } = useRequest2(getOrgList, { - manual: false, - refreshDeps: [userInfo?.team?.teamId] - }); - - const currentOrgs = useMemo(() => { - if (orgs.length === 0) return []; - if (parentPath === '') { - const rootOrg = orgs.find((org) => org.path === ''); - if (rootOrg) { - setParentPath(getOrgChildrenPath(rootOrg)); + } = useRequest2( + () => { + // sync path to orgStack + const splitPath = path.split('/').filter(Boolean); + const orgs = orgStack.filter((o) => splitPath.includes(o.pathId)); + setOrgStack(orgs); + return getOrgList(path); + }, + { + manual: false, + refreshDeps: [userInfo?.team?.teamId, path], + onSuccess: (data) => { + if (!rootOrg) { + setRootOrg(data[0]); + } } - return []; } - - return orgs - .filter((org) => org.path === parentPath) - .map((item) => { - return { - ...item, - // Member + org - count: - item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length - }; - }); - }, [orgs, parentPath]); - - const currentOrg = useMemo(() => { - const splitPath = parentPath.split('/'); - const currentOrgId = splitPath[splitPath.length - 1]; - if (!currentOrgId) return; - - return orgs.find((org) => org.pathId === currentOrgId); - }, [orgs, parentPath]); + ); const paths = useMemo(() => { - const splitPath = parentPath.split('/').filter(Boolean); - return splitPath - .map((id) => { - const org = orgs.find((org) => org.pathId === id)!; - + return orgStack + .map((org) => { if (org?.path === '') return; - return { parentId: getOrgChildrenPath(org), parentName: org.name }; }) .filter(Boolean) as ParentTreePathItemType[]; - }, [parentPath, orgs]); + }, [orgStack]); + + const onClickOrg = (org: OrgType) => { + setOrgStack([...orgStack, org]); + setPath(getOrgChildrenPath(org)); + }; const [editOrg, setEditOrg] = useState(); const [manageMemberOrg, setManageMemberOrg] = useState(); @@ -174,7 +177,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, { onSuccess: () => { refetchOrgs(); - refetchMembers(); } }); @@ -184,9 +186,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { return orgs .filter((org) => org.name.includes(searchOrg)) .map((org) => ({ - ...org, - count: - org.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(org)).length + ...org })); }, [orgs, searchOrg]); @@ -210,11 +210,10 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { isLoading={isLoadingOrgs} > - + - - {/* Table */} + @@ -229,21 +228,11 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { {searchedOrgs.map((org) => ( - setParentPath(getOrgChildrenPath(org))} - > + onClickOrg(org)}> + {isTeamAdmin && !isSyncMember && ( + + )} ))} {!searchOrg && - currentOrgs.map((org) => ( - - - {isTeamAdmin && !isSyncMember && ( - + - )} - - ))} + {isTeamAdmin && !isSyncMember && ( + + )} + + ))} {!searchOrg && - currentOrg?.members.map((member) => { - const memberInfo = members.find((m) => m.tmbId === member.tmbId); - if (!memberInfo) return null; - + members.map((member) => { return (
- { - setParentPath(getOrgChildrenPath(org)); - setSearchOrg(''); - }} - > + onClickOrg(org)}> - {org.count} + {org.total} + } + menuList={[ + { + children: [ + { + icon: 'edit', + label: t('account_team:edit_info'), + onClick: () => setEditOrg(org) + }, + { + icon: 'common/file/move', + label: t('common:Move'), + onClick: () => setMovingOrg(org) + }, + { + icon: 'delete', + label: t('account_team:delete'), + type: 'danger', + onClick: () => deleteOrgHandler(org._id) + } + ] + } + ]} + /> +
- { - setParentPath(getOrgChildrenPath(org)); - setSearchOrg(''); - }} - > - - {org.count} - - - - } - menuList={[ - { - children: [ - { - icon: 'edit', - label: t('account_team:edit_info'), - onClick: () => setEditOrg(org) - }, - { - icon: 'common/file/move', - label: t('common:Move'), - onClick: () => setMovingOrg(org) - }, - { - icon: 'delete', - label: t('account_team:delete'), - type: 'danger', - onClick: () => deleteOrgHandler(org._id) - } - ] - } - ]} - /> + orgs + .filter((org) => org.path !== '') + .map((org) => ( +
+ onClickOrg(org)}> + + {org.total} + +
+ } + menuList={[ + { + children: [ + { + icon: 'edit', + label: t('account_team:edit_info'), + onClick: () => setEditOrg(org) + }, + { + icon: 'common/file/move', + label: t('common:Move'), + onClick: () => setMovingOrg(org) + }, + { + icon: 'delete', + label: t('account_team:delete'), + type: 'danger', + onClick: () => deleteOrgHandler(org._id) + } + ] + } + ]} + /> +
- + {isTeamAdmin && ( @@ -333,14 +345,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { } }, label: t('account_team:delete_from_team', { - username: memberInfo.memberName + username: member.memberName }), onClick: () => { openDeleteMemberFromTeamModal( () => deleteMemberFromTeamReq(member.tmbId), undefined, t('account_team:confirm_delete_from_team', { - username: memberInfo.memberName + username: member.memberName }) )(); } @@ -362,7 +374,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { deleteMemberReq(currentOrg._id, member.tmbId), undefined, t('account_team:confirm_delete_from_org', { - username: memberInfo.memberName + username: member.memberName }) )() } @@ -385,22 +397,29 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) { {!isSyncMember && ( - + - {currentOrg?.name} + {currentOrg?.name || userInfo?.team.teamName} - {currentOrg?.path !== '' && ( + {currentOrg && currentOrg?.path !== '' && ( setEditOrg(currentOrg)} /> )} - {currentOrg?.description || t('common:common.no_intro')} + {currentOrg && ( + {currentOrg?.description || t('common:common.no_intro')} + )} {t('common:common.Action')} - {currentOrg && isTeamAdmin && ( + {isTeamAdmin && ( { setEditOrg({ ...defaultOrgForm, - parentId: currentOrg?._id + parentId: currentOrg?._id ?? rootOrg?._id }); }} /> setManageMemberOrg(currentOrg)} + onClick={() => setManageMemberOrg(currentOrg ?? rootOrg)} /> - {currentOrg?.path !== '' && ( + {currentOrg && currentOrg?.path !== '' && ( <> GetSearchUserGroupOrg(searchKey), { manual: false, throttleWait: 500, + debounceWait: 200, refreshDeps: [searchKey] }); diff --git a/projects/app/src/pageComponents/account/team/context.tsx b/projects/app/src/pageComponents/account/team/context.tsx index 509eda64f..b8cf4c88c 100644 --- a/projects/app/src/pageComponents/account/team/context.tsx +++ b/projects/app/src/pageComponents/account/team/context.tsx @@ -104,7 +104,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode }) refreshList: refetchMemberList, ScrollData: MemberScrollData } = useScrollPagination(getTeamMembers, { - pageSize: 1000, + pageSize: 20, params: { withLeaved: true } diff --git a/projects/app/src/pages/account/team/index.tsx b/projects/app/src/pages/account/team/index.tsx index a23c32c6f..5d613dbb5 100644 --- a/projects/app/src/pages/account/team/index.tsx +++ b/projects/app/src/pages/account/team/index.tsx @@ -48,7 +48,7 @@ const Team = () => { const { t } = useTranslation(); const { userInfo } = useUserStore(); - const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v); + const { setEditTeamData, teamSize } = useContextSelector(TeamContext, (v) => v); const Tabs = useMemo( () => ( @@ -75,7 +75,7 @@ const Team = () => { ); return ( - + {/* header */} GET('/proApi/support/user/search', { searchKey, ...options }); +) => + GET('/proApi/support/user/search', { searchKey, ...options }, { maxQuantity: 1 }); export const ExportMembers = () => GET<{ csv: string }>('/proApi/support/user/team/member/export'); diff --git a/projects/app/src/web/support/user/team/group/api.ts b/projects/app/src/web/support/user/team/group/api.ts index e6d47f61c..7de009cfa 100644 --- a/projects/app/src/web/support/user/team/group/api.ts +++ b/projects/app/src/web/support/user/team/group/api.ts @@ -1,5 +1,8 @@ import { DELETE, GET, POST, PUT } from '@/web/common/api/request'; -import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type'; +import type { + GroupMemberItemType, + MemberGroupListType +} from '@fastgpt/global/support/permission/memberGroup/type'; import type { postCreateGroupData, putUpdateGroupData @@ -15,3 +18,9 @@ export const deleteGroup = (groupId: string) => export const putUpdateGroup = (data: putUpdateGroupData) => PUT('/proApi/support/user/team/group/update', data); + +export const getGroupMembers = (groupId: string) => + GET(`/proApi/support/user/team/group/members`, { groupId }); + +export const putGroupChangeOwner = (groupId: string, tmbId: string) => + PUT(`/proApi/support/user/team/group/changeOwner`, { groupId, tmbId }); diff --git a/projects/app/src/web/support/user/team/org/api.ts b/projects/app/src/web/support/user/team/org/api.ts index f25a48479..c33a5ced9 100644 --- a/projects/app/src/web/support/user/team/org/api.ts +++ b/projects/app/src/web/support/user/team/org/api.ts @@ -6,8 +6,11 @@ import type { } from '@fastgpt/global/support/user/team/org/api'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; import type { putMoveOrgType } from '@fastgpt/global/support/user/team/org/api'; +import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; -export const getOrgList = () => GET('/proApi/support/user/team/org/list'); +export const getOrgList = (path: string) => + GET(`/proApi/support/user/team/org/list`, { orgPath: path }); export const postCreateOrg = (data: postCreateOrgData) => POST('/proApi/support/user/team/org/create', data); @@ -28,3 +31,6 @@ export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) => // export const putChnageOrgOwner = (data: putChnageOrgOwnerData) => // PUT('/proApi/support/user/team/org/changeOwner', data); + +export const getOrgMembers = (data: PaginationProps<{ orgId: string }>) => + GET>(`/proApi/support/user/team/org/members`, data); diff --git a/projects/app/src/web/support/user/useUserStore.ts b/projects/app/src/web/support/user/useUserStore.ts index 91128c703..a37715fac 100644 --- a/projects/app/src/web/support/user/useUserStore.ts +++ b/projects/app/src/web/support/user/useUserStore.ts @@ -32,8 +32,6 @@ type State = { loadAndGetGroups: (init?: boolean) => Promise; teamOrgs: OrgType[]; - myOrgs: OrgType[]; - loadAndGetOrgs: (init?: boolean) => Promise; }; export const useUserStore = create()( @@ -122,23 +120,6 @@ export const useUserStore = create()( ); }); - return res; - }, - myOrgs: [], - loadAndGetOrgs: async (init = false) => { - if (!useSystemStore.getState()?.feConfigs?.isPlus) return []; - - const randomRefresh = Math.random() > 0.7; - if (!randomRefresh && !init && get().myOrgs.length) return Promise.resolve(get().myOrgs); - - const res = await getOrgList(); - set((state) => { - state.teamOrgs = res; - state.myOrgs = res.filter((item) => - item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId)) - ); - }); - return res; } })),