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
21 changed files with 413 additions and 364 deletions

View File

@@ -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}`;
};

View File

@@ -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<OrgSchemaType, 'avatar'> & {
avatar: string;
permission: TeamPermission;
members: OrgMemberSchemaType[];
total: number; // members + children orgs
};

View File

@@ -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 = {

View File

@@ -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';

View File

@@ -90,6 +90,6 @@ export async function createRootOrg({
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
* @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 (
<Flex position="relative">
{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 && (
<Box
position="relative"
left={`${(max - 1) * 15}px`}
left={`${(max - 1) * 15 + 15}px`}
w={'24px'}
h={'24px'}
borderRadius="50%"
@@ -37,7 +46,7 @@ function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] })
fontSize="sm"
color="myGray.500"
>
+{avatars.length - max}
+{String(remain)}
</Box>
)}
</Flex>

View File

@@ -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<string>('');
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
});
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 () => {
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<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(() => {
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<string[]>([]);
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<string[]>([]);
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')}
/>
</Box>
)}
{(filterClass === 'org' || filterClass === 'member') && (
<ScrollData
{(filterClass === 'member' || (searchText && filterMembers.length > 0)) && (
<TeamMemberScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
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 = () => {
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();
}}
/>
)}
</HStack>
);
})}
{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 (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={selectedMemberIdList.includes(member.tmbId)}
orgs={memberOrgNames}
/>
);
})}
</ScrollData>
)}
});
return searchText ? (
orgs
) : (
<OrgMemberScrollData>
{orgs}
{orgMembers.map((member) => {
return (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
onChange={() => {
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}
/>
);
})}
</OrgMemberScrollData>
);
})()}
{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}
/>
);
})}

View File

@@ -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 ? (
<MyTooltip
label={
<VStack gap="1" alignItems={'start'}>
@@ -39,6 +39,10 @@ function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' |
</Flex>
)}
</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 { 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<string>();
const selectedMembersRef = useRef<HTMLDivElement>(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
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
<MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}>
{members.map((member) => {
{members?.map((member) => {
return (
<HStack
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
@@ -202,14 +223,8 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
_notLast={{ mb: 2 }}
>
<HStack>
<Avatar
src={allMembers.find((item) => item.tmbId === member.tmbId)?.avatar}
w="1.5rem"
borderRadius={'md'}
/>
<Box>
{allMembers.find((item) => item.tmbId === member.tmbId)?.memberName}
</Box>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'md'} />
<Box>{member.name}</Box>
</HStack>
<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 {
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<TeamMemberItemType | null>(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) {

View File

@@ -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<MemberGroupType>();
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}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
<Box>({group.name === DefaultGroupName ? teamSize : group.count})</Box>
</HStack>
</Td>
<Td>
@@ -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
}
/>
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} />
<AvatarGroup avatars={members.map((v) => v.avatar)} total={teamSize} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group)}>
@@ -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}
/>
</Box>
</MyTooltip>
@@ -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}
/>
)}
</Td>

View File

@@ -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 }) {
<Td maxW={'300px'}>{member.contact || '-'}</Td>
<Td maxWidth="300px">
{(() => {
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 <OrgTags orgs={memberOrgNames} type="tag" />;
return <OrgTags orgs={member.orgs || undefined} type="tag" />;
})()}
</Td>
<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 {
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<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 filterMembers = useMemo(() => {
if (!searchKey) return allMembers;
@@ -150,9 +161,10 @@ function OrgMemberManageModal({
})}
</MemberScrollData>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
{/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */}
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
<OrgMemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box>
{selectedMembers.map((tmbId) => {
const member = allMembers.find((item) => item.tmbId === tmbId)!;
return (
@@ -179,7 +191,7 @@ function OrgMemberManageModal({
</HStack>
);
})}
</Flex>
</OrgMemberScrollData>
</Flex>
</Grid>
</ModalBody>

View File

@@ -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<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 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<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
@@ -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}
>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setPath} />
</Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData h={'100%'} fontSize={'sm'} flexGrow={1}>
{/* Table */}
<MemberScrollData flex="1">
<TableContainer>
<Table>
<Thead>
@@ -229,21 +228,11 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
</Thead>
<Tbody>
{searchedOrgs.map((org) => (
<Tr
key={org._id}
overflow={'unset'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<Tr key={org._id} overflow={'unset'} onClick={() => onClickOrg(org)}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
setSearchOrg('');
}}
>
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<Tag size="sm">{org.total}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
@@ -252,70 +241,93 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
/>
</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>
)}
</Tr>
))}
{!searchOrg &&
currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
setSearchOrg('');
}}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
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)
}
]
}
]}
/>
orgs
.filter((org) => org.path !== '')
.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.total}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
)}
</Tr>
))}
{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>
))}
{!searchOrg &&
currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
members.map((member) => {
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
<MemberTag name={member.memberName} avatar={member.avatar} />
</Td>
<Td w={'6rem'}>
{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 && (
<VStack w={'180px'} alignItems={'start'}>
<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'}>
{currentOrg?.name}
{currentOrg?.name || userInfo?.team.teamName}
</Box>
{currentOrg?.path !== '' && (
{currentOrg && currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</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'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
{isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
@@ -408,16 +427,16 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
parentId: currentOrg?._id ?? rootOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
onClick={() => setManageMemberOrg(currentOrg ?? rootOrg)}
/>
{currentOrg?.path !== '' && (
{currentOrg && currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"

View File

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

View File

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

View File

@@ -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 (
<AccountContainer isLoading={isLoading}>
<AccountContainer>
<Flex h={'100%'} flexDirection={'column'}>
{/* header */}
<Flex

View File

@@ -105,6 +105,7 @@ export const GetSearchUserGroupOrg = (
orgs?: 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');

View File

@@ -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<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';
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<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) =>
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<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/org/members`, data);

View File

@@ -32,8 +32,6 @@ type State = {
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
teamOrgs: OrgType[];
myOrgs: OrgType[];
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
};
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;
}
})),