mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
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:
@@ -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}`;
|
||||
};
|
||||
|
@@ -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
|
||||
};
|
||||
|
1
packages/global/support/user/team/type.d.ts
vendored
1
packages/global/support/user/team/type.d.ts
vendored
@@ -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 = {
|
||||
|
@@ -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';
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@@ -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">
|
||||
{(() => {
|
||||
|
@@ -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) {
|
||||
|
@@ -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'}>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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');
|
||||
|
@@ -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 });
|
||||
|
@@ -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>()(
|
||||
@@ -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;
|
||||
}
|
||||
})),
|
||||
|
Reference in New Issue
Block a user