pref: member/org/gourp list (#4295)

* refactor: org api

* refactor: org api

* pref: member/org/group list

* feat: change group owner api

* fix: manage org member

* pref: member search
This commit is contained in:
Finley Ge 2025-03-25 00:10:26 +08:00 committed by archer
parent 6ea57e4609
commit 5a47af6fff
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
21 changed files with 413 additions and 364 deletions

View File

@ -19,9 +19,23 @@ type GroupMemberSchemaType = {
type MemberGroupType = MemberGroupSchemaType & { type MemberGroupType = MemberGroupSchemaType & {
members: { members: {
tmbId: string; tmbId: string;
role: `${GroupMemberRole}`; name: string;
}[]; // we can get tmb's info from other api. there is no need but only need to get tmb's id avatar: string;
permission: TeamPermission; }[];
count: number;
owner: {
tmbId: string;
name: string;
avatar: string;
};
canEdit: boolean;
}; };
type MemberGroupListType = MemberGroupType[]; type MemberGroupListType = MemberGroupType[];
type GroupMemberItemType = {
tmbId: string;
name: string;
avatar: string;
role: `${GroupMemberRole}`;
};

View File

@ -1,5 +1,6 @@
import type { TeamPermission } from 'support/permission/user/controller'; import type { TeamPermission } from 'support/permission/user/controller';
import { ResourcePermissionType } from '../type'; import { ResourcePermissionType } from '../type';
import { SourceMemberType } from 'support/user/type';
type OrgSchemaType = { type OrgSchemaType = {
_id: string; _id: string;
@ -23,4 +24,5 @@ type OrgType = Omit<OrgSchemaType, 'avatar'> & {
avatar: string; avatar: string;
permission: TeamPermission; permission: TeamPermission;
members: OrgMemberSchemaType[]; members: OrgMemberSchemaType[];
total: number; // members + children orgs
}; };

View File

@ -82,6 +82,7 @@ export type TeamMemberItemType = {
contact?: string; contact?: string;
createTime: Date; createTime: Date;
updateTime?: Date; updateTime?: Date;
orgs?: string[]; // full path name, pattern: /teamName/orgname1/orgname2
}; };
export type TeamTagItemType = { export type TeamTagItemType = {

View File

@ -1,6 +1,7 @@
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant'; import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type';
import { GroupMemberCollectionName } from './groupMemberSchema';
const { Schema } = connectionMongo; const { Schema } = connectionMongo;
export const MemberGroupCollectionName = 'team_member_groups'; export const MemberGroupCollectionName = 'team_member_groups';

View File

@ -90,6 +90,6 @@ export async function createRootOrg({
path: '' path: ''
} }
], ],
{ session } { session, ordered: true }
); );
} }

View File

@ -10,7 +10,16 @@ import { Box, Flex } from '@chakra-ui/react';
* @param [groupId] - group id to make the key unique * @param [groupId] - group id to make the key unique
* @returns * @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 ( return (
<Flex position="relative"> <Flex position="relative">
{avatars.slice(0, max).map((avatar, index) => ( {avatars.slice(0, max).map((avatar, index) => (
@ -24,10 +33,10 @@ function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] })
borderRadius={'50%'} borderRadius={'50%'}
/> />
))} ))}
{avatars.length > max && ( {remain > 0 && (
<Box <Box
position="relative" position="relative"
left={`${(max - 1) * 15}px`} left={`${(max - 1) * 15 + 15}px`}
w={'24px'} w={'24px'}
h={'24px'} h={'24px'}
borderRadius="50%" borderRadius="50%"
@ -37,7 +46,7 @@ function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] })
fontSize="sm" fontSize="sm"
color="myGray.500" color="myGray.500"
> >
+{avatars.length - max} +{String(remain)}
</Box> </Box>
)} )}
</Flex> </Flex>

View File

@ -35,7 +35,7 @@ import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api'; import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } 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 { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MemberItemCard from './MemberItemCard'; import MemberItemCard from './MemberItemCard';
import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { GetSearchUserGroupOrg } from '@/web/support/user/api';
@ -57,14 +57,52 @@ function MemberModal({
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { data: members, ScrollData } = useScrollPagination(getTeamMembers, { const [path, setPath] = useState('');
const [orgStack, setOrgStack] = useState<OrgType[]>([]);
const currentOrg = useMemo(() => orgStack[orgStack.length - 1], [orgStack]);
const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, {
pageSize: 15 pageSize: 15
}); });
const { data: [groups = [], orgs = []] = [], loading: loadingGroupsAndOrgs } = useRequest2( const [rootOrg, setRootOrg] = useState<OrgType>();
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 () => { async () => {
if (!userInfo?.team?.teamId) return [[], []]; if (!userInfo?.team?.teamId) return [];
return Promise.all([getGroupList(), getOrgList()]); return getGroupList();
}, },
{ {
manual: false, manual: false,
@ -72,69 +110,49 @@ function MemberModal({
} }
); );
const [parentPath, setParentPath] = useState('');
const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), { const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), {
manual: false, manual: false,
throttleWait: 500, throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchText] refreshDeps: [searchText]
}); });
const paths = useMemo(() => { const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean); return orgStack
return splitPath .map((org) => {
.map((id) => { if (org?.path === '') return;
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return { return {
parentId: getOrgChildrenPath(org), parentId: getOrgChildrenPath(org),
parentName: org.name parentName: org.name
}; };
}) })
.filter(Boolean) as ParentTreePathItemType[]; .filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]); }, [orgStack]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]); const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
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(() => { const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
if (searchText && searchedData) { if (searchText && searchedData) {
const orgids = searchedData.orgs.map((item) => item._id); const orgids = searchedData.orgs.map((item) => item._id);
return orgs.filter((org) => orgids.includes(String(org._id))); return orgs.filter((org) => orgids.includes(String(org._id)));
} }
if (!searchText && filterClass !== 'org') return [];
if (parentPath === '') {
setParentPath(`/${orgs[0].pathId}`);
return [];
}
return orgs return orgs
.filter((org) => org.path === parentPath) .filter((org) => org.path !== '')
.map((item) => ({ .map((org) => ({
...item, ...org,
count: count: org.total
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
})); }));
}, [searchText, filterClass, parentPath, orgs, searchedData]); }, [searchText, orgs, searchedData]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]); const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const filterMembers = useMemo(() => { const filterMembers = useMemo(() => {
if (searchText) { if (searchText) {
return searchedData?.members || []; 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; return members;
}, [members, searchedData, searchText, filterClass, currentOrg]); }, [searchText, members, searchedData?.members]);
console.log(filterMembers);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]); const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const filterGroups = useMemo(() => { const filterGroups = useMemo(() => {
@ -197,19 +215,22 @@ function MemberModal({
id: `org-${item._id}`, id: `org-${item._id}`,
avatar: item.avatar, avatar: item.avatar,
name: item.name, name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id)) onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id)),
orgs: undefined
})), })),
...selectedGroups.map((item) => ({ ...selectedGroups.map((item) => ({
id: `group-${item._id}`, id: `group-${item._id}`,
avatar: item.avatar, avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, 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) => ({ ...selectedMembers.map((item) => ({
id: `member-${item.tmbId}`, id: `member-${item.tmbId}`,
avatar: item.avatar, avatar: item.avatar,
name: item.memberName, 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) => { onClick={(parentId) => {
if (parentId === '') { if (parentId === '') {
setFilterClass(undefined); setFilterClass(undefined);
setParentPath(''); setPath('');
} else if ( } else if (
parentId === 'member' || parentId === 'member' ||
parentId === 'org' || parentId === 'org' ||
parentId === 'group' parentId === 'group'
) { ) {
setFilterClass(parentId); setFilterClass(parentId);
setParentPath(''); setPath('');
} else { } else {
setParentPath(parentId); setPath(parentId);
} }
}} }}
rootName={t('common:common.Team')} rootName={t('common:common.Team')}
/> />
</Box> </Box>
)} )}
{(filterClass === 'member' || (searchText && filterMembers.length > 0)) && (
{(filterClass === 'org' || filterClass === 'member') && ( <TeamMemberScrollData
<ScrollData
flexDirection={'column'} flexDirection={'column'}
gap={1} gap={1}
userSelect={'none'} userSelect={'none'}
height={'fit-content'} height={'fit-content'}
> >
{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 (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={selectedMemberIdList.includes(member.tmbId)}
orgs={member.orgs}
/>
);
})}
</TeamMemberScrollData>
)}
{(filterClass === 'org' || searchText) &&
(() => {
const orgs = filterOrgs?.map((org) => {
const onChange = () => { const onChange = () => {
setSelectedOrgIdList((state) => { setSelectedOrgIdList((state) => {
if (state.includes(org._id)) { if (state.includes(org._id)) {
@ -374,47 +421,42 @@ function MemberModal({
bgColor: 'myGray.200' bgColor: 'myGray.200'
}} }}
onClick={(e) => { onClick={(e) => {
setParentPath(getOrgChildrenPath(org)); onClickOrg(org);
// setPath(getOrgChildrenPath(org));
e.stopPropagation(); e.stopPropagation();
}} }}
/> />
)} )}
</HStack> </HStack>
); );
})} });
{filterMembers?.map((member) => { return searchText ? (
const onChange = () => { orgs
setSelectedMembers((state) => { ) : (
if (state.includes(member.tmbId)) { <OrgMemberScrollData>
return state.filter((v) => v !== member.tmbId); {orgs}
} {orgMembers.map((member) => {
return [...state, member.tmbId]; return (
}); <MemberItemCard
}; avatar={member.avatar}
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); key={member.tmbId}
const memberOrgs = orgs.filter((org) => name={member.memberName}
org.members.find((v) => String(v.tmbId) === String(member.tmbId)) onChange={() => {
); setSelectedMembers((state) => {
const memberPathIds = memberOrgs.map((org) => if (state.includes(member.tmbId)) {
(org.path + '/' + org.pathId).split('/').slice(0) return state.filter((v) => v !== member.tmbId);
); }
const memberOrgNames = memberPathIds.map((pathIds) => return [...state, member.tmbId];
pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/') });
); }}
return ( isChecked={selectedMemberIdList.includes(member.tmbId)}
<MemberItemCard orgs={member.orgs}
avatar={member.avatar} />
key={member.tmbId} );
name={member.memberName} })}
permission={collaborator?.permission.value} </OrgMemberScrollData>
onChange={onChange} );
isChecked={selectedMemberIdList.includes(member.tmbId)} })()}
orgs={memberOrgNames}
/>
);
})}
</ScrollData>
)}
{filterGroups?.map((group) => { {filterGroups?.map((group) => {
const onChange = () => { const onChange = () => {
setSelectedGroupIdList((state) => { setSelectedGroupIdList((state) => {
@ -455,20 +497,7 @@ function MemberModal({
name={item.name ?? ''} name={item.name ?? ''}
onChange={item.onDelete} onChange={item.onDelete}
onDelete={item.onDelete} onDelete={item.onDelete}
orgs={(() => { orgs={item?.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;
})()}
/> />
); );
})} })}

View File

@ -4,8 +4,8 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Tag from '@fastgpt/web/components/common/Tag'; import Tag from '@fastgpt/web/components/common/Tag';
import React from 'react'; import React from 'react';
function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' | 'tag' }) { function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' | 'tag' }) {
return ( return orgs?.length ? (
<MyTooltip <MyTooltip
label={ label={
<VStack gap="1" alignItems={'start'}> <VStack gap="1" alignItems={'start'}>
@ -39,6 +39,10 @@ function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' |
</Flex> </Flex>
)} )}
</MyTooltip> </MyTooltip>
) : (
<Box fontSize="xs" fontWeight={400} w="full" color="myGray.400" whiteSpace={'nowrap'}>
-
</Box>
); );
} }

View File

@ -18,7 +18,7 @@ import React, { useMemo, useRef, useState } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context'; 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 { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast'; 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); return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]); }, [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 allMembers = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers); const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData); const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
const [hoveredMemberId, setHoveredMemberId] = useState<string>(); const [hoveredMemberId, setHoveredMemberId] = useState<string>();
const selectedMembersRef = useRef<HTMLDivElement>(null); const selectedMembersRef = useRef<HTMLDivElement>(null);
const [members, setMembers] = useState(group?.members || []); const [members, setMembers] = useState(groupMembers || []);
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const filtered = useMemo(() => { const filtered = useMemo(() => {
@ -67,6 +80,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async () => { async () => {
if (!editGroupId || !members.length) return; if (!editGroupId || !members.length) return;
console.log(members);
return putUpdateGroup({ return putUpdateGroup({
groupId: editGroupId, groupId: editGroupId,
memberList: members memberList: members
@ -89,10 +103,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
}, [members, userInfo]); }, [members, userInfo]);
const handleToggleSelect = (memberId: string) => { const handleToggleSelect = (memberId: string) => {
if ( if (myRole === 'owner' && memberId === members.find((item) => item.role === 'owner')?.tmbId) {
myRole === 'owner' &&
memberId === group?.members.find((item) => item.role === 'owner')?.tmbId
) {
toast({ toast({
title: t('user:team.group.toast.can_not_delete_owner'), title: t('user:team.group.toast.can_not_delete_owner'),
status: 'error' status: 'error'
@ -102,7 +113,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
if ( if (
myRole === 'admin' && myRole === 'admin' &&
group?.members.find((item) => String(item.tmbId) === memberId)?.role !== 'member' members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
) { ) {
return; return;
} }
@ -110,7 +121,17 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
if (isSelected(memberId)) { if (isSelected(memberId)) {
setMembers(members.filter((item) => item.tmbId !== memberId)); setMembers(members.filter((item) => item.tmbId !== memberId));
} else { } 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
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}> <Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box> <Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
<MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}> <MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}>
{members.map((member) => { {members?.map((member) => {
return ( return (
<HStack <HStack
onMouseEnter={() => setHoveredMemberId(member.tmbId)} onMouseEnter={() => setHoveredMemberId(member.tmbId)}
@ -202,14 +223,8 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
_notLast={{ mb: 2 }} _notLast={{ mb: 2 }}
> >
<HStack> <HStack>
<Avatar <Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar} <Box>{member.name}</Box>
w="1.5rem"
borderRadius={'md'}
/>
<Box>
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
</Box>
</HStack> </HStack>
<Box mr="auto"> <Box mr="auto">
{(() => { {(() => {

View File

@ -1,4 +1,4 @@
import { putUpdateGroup } from '@/web/support/user/team/group/api'; import { putGroupChangeOwner, putUpdateGroup } from '@/web/support/user/team/group/api';
import { import {
Box, Box,
Flex, Flex,
@ -38,10 +38,6 @@ export function ChangeOwnerModal({
return item.memberName.toLowerCase().includes(inputValue.toLowerCase()); 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 [keepAdmin, setKeepAdmin] = useState(true);
const { const {
@ -52,36 +48,14 @@ export function ChangeOwnerModal({
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null); const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
const onChangeOwner = async (tmbId: string) => { const { runAsync, loading } = useRequest2(
if (!group) { (tmbId: string) => putGroupChangeOwner(groupId, tmbId),
return; {
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 () => { const onConfirm = async () => {
if (!selectedMember) { if (!selectedMember) {

View File

@ -22,7 +22,7 @@ import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup } 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 { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@ -39,10 +39,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { groups, refetchGroups, members, refetchMembers } = useContextSelector( const { groups, refetchGroups, members, teamSize } = useContextSelector(TeamContext, (v) => v);
TeamContext,
(v) => v
);
const [editGroup, setEditGroup] = useState<MemberGroupType>(); const [editGroup, setEditGroup] = useState<MemberGroupType>();
const { const {
@ -64,7 +61,6 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, { const { runAsync: delDeleteGroup } = useRequest2(deleteGroup, {
onSuccess: () => { onSuccess: () => {
refetchGroups(); refetchGroups();
refetchMembers();
} }
}); });
@ -78,14 +74,9 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onOpenManageGroupMember(); onOpenManageGroupMember();
}; };
const hasGroupManagePer = (group: (typeof groups)[0]) => const hasGroupManagePer = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
userInfo?.team.permission.hasManagePer ||
['admin', 'owner'].includes( const isGroupOwner = (group: (typeof groups)[0]) => userInfo?.team.permission.hasManagePer;
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 { const {
isOpen: isOpenChangeOwner, isOpen: isOpenChangeOwner,
@ -143,9 +134,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
} }
avatar={group.avatar} avatar={group.avatar}
/> />
<Box> <Box>({group.name === DefaultGroupName ? teamSize : group.count})</Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack> </HStack>
</Td> </Td>
<Td> <Td>
@ -153,26 +142,18 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
name={ name={
group.name === DefaultGroupName group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? '' ? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find( : group.owner.name
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
} }
avatar={ avatar={
group.name === DefaultGroupName group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? '' ? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find( : group.owner.avatar
(i) =>
i.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
} }
/> />
</Td> </Td>
<Td> <Td>
{group.name === DefaultGroupName ? ( {group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} /> <AvatarGroup avatars={members.map((v) => v.avatar)} total={teamSize} />
) : hasGroupManagePer(group) ? ( ) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}> <MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group)}> <Box cursor="pointer" onClick={() => onManageMember(group)}>
@ -180,6 +161,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
avatars={group.members.map( avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)} )}
total={group.count}
/> />
</Box> </Box>
</MyTooltip> </MyTooltip>
@ -188,6 +170,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
avatars={group.members.map( avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? '' (v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)} )}
total={group.count}
/> />
)} )}
</Td> </Td>

View File

@ -30,8 +30,6 @@ import { TeamContext } from './context';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic'; 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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api'; import { delLeaveTeam } from '@/web/support/user/team/api';
import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api'; import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api';
@ -52,9 +50,8 @@ const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTa
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { userInfo } = useUserStore();
const { userInfo, teamPlanStatus } = useUserStore(); const { feConfigs } = useSystemStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { const {
refetchGroups, refetchGroups,
@ -63,8 +60,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
members, members,
refetchMembers, refetchMembers,
onSwitchTeam, onSwitchTeam,
MemberScrollData, MemberScrollData
orgs
} = useContextSelector(TeamContext, (v) => v); } = useContextSelector(TeamContext, (v) => v);
const { const {
@ -93,6 +89,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
{ {
manual: false, manual: false,
throttleWait: 500, throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchText] refreshDeps: [searchText]
} }
); );
@ -281,16 +278,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Td maxW={'300px'}>{member.contact || '-'}</Td> <Td maxW={'300px'}>{member.contact || '-'}</Td>
<Td maxWidth="300px"> <Td maxWidth="300px">
{(() => { {(() => {
const memberOrgs = orgs.filter((org) => return <OrgTags orgs={member.orgs || undefined} type="tag" />;
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 <OrgTags orgs={memberOrgNames} type="tag" />;
})()} })()}
</Td> </Td>
<Td maxW={'300px'}> <Td maxW={'300px'}>

View File

@ -1,4 +1,4 @@
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api'; import { getOrgMembers, putUpdateOrgMembers } from '@/web/support/user/team/org/api';
import { import {
Box, Box,
Button, Button,
@ -18,10 +18,11 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type React from 'react'; import type React from 'react';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
export type GroupFormType = { export type GroupFormType = {
members: { members: {
@ -51,11 +52,21 @@ function OrgMemberManageModal({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v); 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<string[]>( const [selectedMembers, setSelectedMembers] = useState<string[]>(
currentOrg.members.map((item) => item.tmbId) orgMembers.map((item) => item.tmbId)
); );
useEffect(() => {
setSelectedMembers(orgMembers.map((item) => item.tmbId));
}, [orgMembers]);
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const filterMembers = useMemo(() => { const filterMembers = useMemo(() => {
if (!searchKey) return allMembers; if (!searchKey) return allMembers;
@ -150,9 +161,10 @@ function OrgMemberManageModal({
})} })}
</MemberScrollData> </MemberScrollData>
</Flex> </Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}> {/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */}
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box> <Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}> <OrgMemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box>
{selectedMembers.map((tmbId) => { {selectedMembers.map((tmbId) => {
const member = allMembers.find((item) => item.tmbId === tmbId)!; const member = allMembers.find((item) => item.tmbId === tmbId)!;
return ( return (
@ -179,7 +191,7 @@ function OrgMemberManageModal({
</HStack> </HStack>
); );
})} })}
</Flex> </OrgMemberScrollData>
</Flex> </Flex>
</Grid> </Grid>
</ModalBody> </ModalBody>

View File

@ -26,7 +26,12 @@ import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag'; import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context'; 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 IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal'; 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 { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant'; import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import { delRemoveMember } from '@/web/support/user/team/api'; import { delRemoveMember, getTeamMembers } from '@/web/support/user/team/api';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal')); const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal')); const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
@ -77,66 +83,63 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore(); const { userInfo, isTeamAdmin } = useUserStore();
const [searchOrg, setSearchOrg] = useState(''); const [searchOrg, setSearchOrg] = useState('');
const [orgStack, setOrgStack] = useState<OrgType[]>([]);
const currentOrg = useMemo(() => orgStack[orgStack.length - 1], [orgStack]);
const [rootOrg, setRootOrg] = useState<OrgType>();
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 { feConfigs } = useSystemStore();
const isSyncMember = feConfigs.register_method?.includes('sync'); const isSyncMember = feConfigs.register_method?.includes('sync');
const [parentPath, setParentPath] = useState(''); const [path, setPath] = useState('');
const { const {
data: orgs = [], data: orgs = [],
loading: isLoadingOrgs, loading: isLoadingOrgs,
refresh: refetchOrgs refresh: refetchOrgs
} = useRequest2(getOrgList, { } = useRequest2(
manual: false, () => {
refreshDeps: [userInfo?.team?.teamId] // sync path to orgStack
}); const splitPath = path.split('/').filter(Boolean);
const orgs = orgStack.filter((o) => splitPath.includes(o.pathId));
const currentOrgs = useMemo(() => { setOrgStack(orgs);
if (orgs.length === 0) return []; return getOrgList(path);
if (parentPath === '') { },
const rootOrg = orgs.find((org) => org.path === ''); {
if (rootOrg) { manual: false,
setParentPath(getOrgChildrenPath(rootOrg)); 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 paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean); return orgStack
return splitPath .map((org) => {
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org?.path === '') return; if (org?.path === '') return;
return { return {
parentId: getOrgChildrenPath(org), parentId: getOrgChildrenPath(org),
parentName: org.name parentName: org.name
}; };
}) })
.filter(Boolean) as ParentTreePathItemType[]; .filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]); }, [orgStack]);
const onClickOrg = (org: OrgType) => {
setOrgStack([...orgStack, org]);
setPath(getOrgChildrenPath(org));
};
const [editOrg, setEditOrg] = useState<OrgFormType>(); const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>(); const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
@ -174,7 +177,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, { const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, {
onSuccess: () => { onSuccess: () => {
refetchOrgs(); refetchOrgs();
refetchMembers();
} }
}); });
@ -184,9 +186,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
return orgs return orgs
.filter((org) => org.name.includes(searchOrg)) .filter((org) => org.name.includes(searchOrg))
.map((org) => ({ .map((org) => ({
...org, ...org
count:
org.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(org)).length
})); }));
}, [orgs, searchOrg]); }, [orgs, searchOrg]);
@ -210,11 +210,10 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
isLoading={isLoadingOrgs} isLoading={isLoadingOrgs}
> >
<Box mb={3}> <Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} /> <Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setPath} />
</Box> </Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}> <Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData h={'100%'} fontSize={'sm'} flexGrow={1}> <MemberScrollData flex="1">
{/* Table */}
<TableContainer> <TableContainer>
<Table> <Table>
<Thead> <Thead>
@ -229,21 +228,11 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
</Thead> </Thead>
<Tbody> <Tbody>
{searchedOrgs.map((org) => ( {searchedOrgs.map((org) => (
<Tr <Tr key={org._id} overflow={'unset'} onClick={() => onClickOrg(org)}>
key={org._id}
overflow={'unset'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<Td> <Td>
<HStack <HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
cursor={'pointer'}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
setSearchOrg('');
}}
>
<MemberTag name={org.name} avatar={org.avatar} /> <MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag> <Tag size="sm">{org.total}</Tag>
<MyIcon <MyIcon
name="core/chat/chevronRight" name="core/chat/chevronRight"
w={'1rem'} w={'1rem'}
@ -252,70 +241,93 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
/> />
</HStack> </HStack>
</Td> </Td>
{isTeamAdmin && !isSyncMember && (
<Td w={'6rem'}>
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
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)
}
]
}
]}
/>
</Td>
)}
</Tr> </Tr>
))} ))}
{!searchOrg && {!searchOrg &&
currentOrgs.map((org) => ( orgs
<Tr key={org._id} overflow={'unset'}> .filter((org) => org.path !== '')
<Td> .map((org) => (
<HStack <Tr key={org._id} overflow={'unset'}>
cursor={'pointer'} <Td>
onClick={() => { <HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
setParentPath(getOrgChildrenPath(org)); <MemberTag name={org.name} avatar={org.avatar} />
setSearchOrg(''); <Tag size="sm">{org.total}</Tag>
}} <MyIcon
> name="core/chat/chevronRight"
<MemberTag name={org.name} avatar={org.avatar} /> w={'1rem'}
<Tag size="sm">{org.count}</Tag> h={'1rem'}
<MyIcon color={'myGray.500'}
name="core/chat/chevronRight" />
w={'1rem'} </HStack>
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
{isTeamAdmin && !isSyncMember && (
<Td w={'6rem'}>
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
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)
}
]
}
]}
/>
</Td> </Td>
)} {isTeamAdmin && !isSyncMember && (
</Tr> <Td w={'6rem'}>
))} <MyMenu
trigger="hover"
Button={<IconButton name="more" />}
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)
}
]
}
]}
/>
</Td>
)}
</Tr>
))}
{!searchOrg && {!searchOrg &&
currentOrg?.members.map((member) => { members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return ( return (
<Tr key={member.tmbId}> <Tr key={member.tmbId}>
<Td> <Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} /> <MemberTag name={member.memberName} avatar={member.avatar} />
</Td> </Td>
<Td w={'6rem'}> <Td w={'6rem'}>
{isTeamAdmin && ( {isTeamAdmin && (
@ -333,14 +345,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
} }
}, },
label: t('account_team:delete_from_team', { label: t('account_team:delete_from_team', {
username: memberInfo.memberName username: member.memberName
}), }),
onClick: () => { onClick: () => {
openDeleteMemberFromTeamModal( openDeleteMemberFromTeamModal(
() => deleteMemberFromTeamReq(member.tmbId), () => deleteMemberFromTeamReq(member.tmbId),
undefined, undefined,
t('account_team:confirm_delete_from_team', { 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), deleteMemberReq(currentOrg._id, member.tmbId),
undefined, undefined,
t('account_team:confirm_delete_from_org', { t('account_team:confirm_delete_from_org', {
username: memberInfo.memberName username: member.memberName
}) })
)() )()
} }
@ -385,22 +397,29 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
{!isSyncMember && ( {!isSyncMember && (
<VStack w={'180px'} alignItems={'start'}> <VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}> <HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} /> <Avatar
src={currentOrg?.avatar || userInfo?.team.avatar}
w={'1rem'}
h={'1rem'}
rounded={'xs'}
/>
<Box fontWeight={500} color={'myGray.900'}> <Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name} {currentOrg?.name || userInfo?.team.teamName}
</Box> </Box>
{currentOrg?.path !== '' && ( {currentOrg && currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} /> <IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)} )}
</HStack> </HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box> {currentOrg && (
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
)}
<Divider my={'20px'} /> <Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900"> <Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')} {t('common:common.Action')}
</Box> </Box>
{currentOrg && isTeamAdmin && ( {isTeamAdmin && (
<VStack gap="13px" w="100%"> <VStack gap="13px" w="100%">
<ActionButton <ActionButton
icon="common/add2" icon="common/add2"
@ -408,16 +427,16 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onClick={() => { onClick={() => {
setEditOrg({ setEditOrg({
...defaultOrgForm, ...defaultOrgForm,
parentId: currentOrg?._id parentId: currentOrg?._id ?? rootOrg?._id
}); });
}} }}
/> />
<ActionButton <ActionButton
icon="common/administrator" icon="common/administrator"
text={t('account_team:manage_member')} text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)} onClick={() => setManageMemberOrg(currentOrg ?? rootOrg)}
/> />
{currentOrg?.path !== '' && ( {currentOrg && currentOrg?.path !== '' && (
<> <>
<ActionButton <ActionButton
icon="common/file/move" icon="common/file/move"

View File

@ -75,6 +75,7 @@ function PermissionManage({
const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), { const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), {
manual: false, manual: false,
throttleWait: 500, throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchKey] refreshDeps: [searchKey]
}); });

View File

@ -104,7 +104,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshList: refetchMemberList, refreshList: refetchMemberList,
ScrollData: MemberScrollData ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, { } = useScrollPagination(getTeamMembers, {
pageSize: 1000, pageSize: 20,
params: { params: {
withLeaved: true withLeaved: true
} }

View File

@ -48,7 +48,7 @@ const Team = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { setEditTeamData, isLoading, teamSize } = useContextSelector(TeamContext, (v) => v); const { setEditTeamData, teamSize } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo( const Tabs = useMemo(
() => ( () => (
@ -75,7 +75,7 @@ const Team = () => {
); );
return ( return (
<AccountContainer isLoading={isLoading}> <AccountContainer>
<Flex h={'100%'} flexDirection={'column'}> <Flex h={'100%'} flexDirection={'column'}>
{/* header */} {/* header */}
<Flex <Flex

View File

@ -105,6 +105,7 @@ export const GetSearchUserGroupOrg = (
orgs?: boolean; orgs?: boolean;
groups?: boolean; groups?: boolean;
} }
) => GET<SearchResult>('/proApi/support/user/search', { searchKey, ...options }); ) =>
GET<SearchResult>('/proApi/support/user/search', { searchKey, ...options }, { maxQuantity: 1 });
export const ExportMembers = () => GET<{ csv: string }>('/proApi/support/user/team/member/export'); export const ExportMembers = () => GET<{ csv: string }>('/proApi/support/user/team/member/export');

View File

@ -1,5 +1,8 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request'; 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 { import type {
postCreateGroupData, postCreateGroupData,
putUpdateGroupData putUpdateGroupData
@ -15,3 +18,9 @@ export const deleteGroup = (groupId: string) =>
export const putUpdateGroup = (data: putUpdateGroupData) => export const putUpdateGroup = (data: putUpdateGroupData) =>
PUT('/proApi/support/user/team/group/update', data); PUT('/proApi/support/user/team/group/update', data);
export const getGroupMembers = (groupId: string) =>
GET<GroupMemberItemType[]>(`/proApi/support/user/team/group/members`, { groupId });
export const putGroupChangeOwner = (groupId: string, tmbId: string) =>
PUT(`/proApi/support/user/team/group/changeOwner`, { groupId, tmbId });

View File

@ -6,8 +6,11 @@ import type {
} from '@fastgpt/global/support/user/team/org/api'; } from '@fastgpt/global/support/user/team/org/api';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { putMoveOrgType } from '@fastgpt/global/support/user/team/org/api'; 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<OrgType[]>('/proApi/support/user/team/org/list'); export const getOrgList = (path: string) =>
GET<OrgType[]>(`/proApi/support/user/team/org/list`, { orgPath: path });
export const postCreateOrg = (data: postCreateOrgData) => export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data); POST('/proApi/support/user/team/org/create', data);
@ -28,3 +31,6 @@ export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) => // export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
// PUT('/proApi/support/user/team/org/changeOwner', data); // PUT('/proApi/support/user/team/org/changeOwner', data);
export const getOrgMembers = (data: PaginationProps<{ orgId: string }>) =>
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/org/members`, data);

View File

@ -32,8 +32,6 @@ type State = {
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>; loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
teamOrgs: OrgType[]; teamOrgs: OrgType[];
myOrgs: OrgType[];
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
}; };
export const useUserStore = create<State>()( export const useUserStore = create<State>()(
@ -122,23 +120,6 @@ export const useUserStore = create<State>()(
); );
}); });
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; return res;
} }
})), })),