Commit 5a47af6f by Finley Ge Committed by archer

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
parent 6ea57e46
......@@ -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}`;
};
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
};
......@@ -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 = {
......
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';
......
......@@ -90,6 +90,6 @@ export async function createRootOrg({
path: ''
}
],
{ session }
{ session, ordered: true }
);
}
......@@ -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>
......
......@@ -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>
);
}
......
......@@ -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">
{(() => {
......
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) {
......
......@@ -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>
......
......@@ -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'}>
......
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>
......
......@@ -75,6 +75,7 @@ function PermissionManage({
const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), {
manual: false,
throttleWait: 500,
debounceWait: 200,
refreshDeps: [searchKey]
});
......
......@@ -104,7 +104,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshList: refetchMemberList,
ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, {
pageSize: 1000,
pageSize: 20,
params: {
withLeaved: true
}
......
......@@ -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
......
......@@ -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');
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 });
......@@ -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);
......@@ -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>()(
......@@ -123,23 +121,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;
}
})),
{
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment