pref: member/group/org (#4316)

* feat: change group owner api

* pref: member/org/group

* fix: member modal select clb

* fix: search member when change owner
This commit is contained in:
Finley Ge
2025-03-25 21:08:51 +08:00
committed by archer
parent ff64a3c039
commit 1fdf947a13
20 changed files with 496 additions and 350 deletions

View File

@@ -1,12 +1,13 @@
// orgId, pathid, path === null ===> root org
export type postCreateOrgData = { export type postCreateOrgData = {
name: string; name: string;
parentId: string;
description?: string; description?: string;
avatar?: string; avatar?: string;
path?: string;
}; };
export type putUpdateOrgMembersData = { export type putUpdateOrgMembersData = {
orgId: string; orgId?: string;
members: { members: {
tmbId: string; tmbId: string;
// role: `${OrgMemberRole}`; // role: `${OrgMemberRole}`;
@@ -14,7 +15,7 @@ export type putUpdateOrgMembersData = {
}; };
export type putUpdateOrgData = { export type putUpdateOrgData = {
orgId: string; orgId: string; // can not be undefined because can not uppdate root org
name?: string; name?: string;
avatar?: string; avatar?: string;
description?: string; description?: string;
@@ -22,7 +23,7 @@ export type putUpdateOrgData = {
export type putMoveOrgType = { export type putMoveOrgType = {
orgId: string; orgId: string;
targetOrgId: string; targetOrgId?: string; // '' ===> move to root org
}; };
// type putChnageOrgOwnerData = { // type putChnageOrgOwnerData = {

View File

@@ -3,7 +3,10 @@ import { OrgSchemaType } from './type';
export const OrgCollectionName = 'team_orgs'; export const OrgCollectionName = 'team_orgs';
export const OrgMemberCollectionName = 'team_org_members'; export const OrgMemberCollectionName = 'team_org_members';
export const getOrgChildrenPath = (org: OrgSchemaType) => `${org.path}/${org.pathId}`; export const getOrgChildrenPath = (org: OrgSchemaType) => {
if (org.path === '' && org.pathId === '') return '';
return `${org.path ?? ''}/${org.pathId}`;
};
export enum SyncOrgSourceEnum { export enum SyncOrgSourceEnum {
wecom = 'wecom' wecom = 'wecom'

View File

@@ -21,7 +21,7 @@ type OrgMemberSchemaType = {
}; };
export type OrgListItemType = OrgSchemaType & { export type OrgListItemType = OrgSchemaType & {
permission: TeamPermission; permission?: TeamPermission;
total: number; // members + children orgs total: number; // members + children orgs
}; };

View File

@@ -11,7 +11,7 @@ export const authOrgMember = async ({
orgIds, orgIds,
...props ...props
}: { }: {
orgIds: string | string[]; orgIds?: string | string[];
} & AuthModeType): Promise<AuthResponseType> => { } & AuthModeType): Promise<AuthResponseType> => {
const result = await authUserPer({ const result = await authUserPer({
...props, ...props,

View File

@@ -19,7 +19,7 @@ function AvatarGroup({
avatars: string[]; avatars: string[];
total?: number; total?: number;
}) { }) {
const remain = total ?? avatars.length - max; const remain = (total ?? avatars.length) - max;
return ( return (
<Flex position="relative"> <Flex position="relative">
{avatars.slice(0, max).map((avatar, index) => ( {avatars.slice(0, max).map((avatar, index) => (

View File

@@ -1,3 +1,4 @@
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
import { import {
Box, Box,
@@ -38,16 +39,29 @@ export function ChangeOwnerModal({
pageSize: 15 pageSize: 15
}); });
const memberList = teamMembers.filter((item) => { const { data: searchedData } = useRequest2(
return item.memberName.includes(inputValue); async () => {
}); if (!inputValue) return;
return GetSearchUserGroupOrg(inputValue);
},
{
manual: false,
refreshDeps: [inputValue],
throttleWait: 500,
debounceWait: 200
}
);
const memberList = searchedData ? searchedData.members : teamMembers;
const { const {
isOpen: isOpenMemberListMenu, isOpen: isOpenMemberListMenu,
onClose: onCloseMemberListMenu, onClose: onCloseMemberListMenu,
onOpen: onOpenMemberListMenu onOpen: onOpenMemberListMenu
} = useDisclosure(); } = useDisclosure();
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null); const [selectedMember, setSelectedMember] = useState<Omit<
TeamMemberItemType,
'permission' | 'teamId'
> | null>(null);
const { runAsync, loading } = useRequest2(onChangeOwner, { const { runAsync, loading } = useRequest2(onChangeOwner, {
onSuccess: onClose, onSuccess: onClose,

View File

@@ -30,7 +30,7 @@ import {
import Path from '@/components/common/folder/Path'; import Path from '@/components/common/folder/Path';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant'; import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type'; import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context'; import { CollaboratorContext } from './context';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
@@ -39,6 +39,9 @@ import { getOrgList, getOrgMembers } from '@/web/support/user/team/org/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MemberItemCard from './MemberItemCard'; import MemberItemCard from './MemberItemCard';
import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
const HoverBoxStyle = { const HoverBoxStyle = {
bgColor: 'myGray.50', bgColor: 'myGray.50',
@@ -57,48 +60,21 @@ function MemberModal({
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const [path, setPath] = useState(''); const {
const [orgStack, setOrgStack] = useState<OrgType[]>([]); paths,
const currentOrg = useMemo(() => orgStack[orgStack.length - 1], [orgStack]); onClickOrg,
members: orgMembers,
MemberScrollData: OrgMemberScrollData,
onPathClick,
refresh,
updateCurrentOrg,
orgs
} = useOrg({ getPermission: false });
const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, { const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, {
pageSize: 15 pageSize: 15
}); });
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( const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2(
async () => { async () => {
if (!userInfo?.team?.teamId) return []; if (!userInfo?.team?.teamId) return [];
@@ -117,21 +93,9 @@ function MemberModal({
refreshDeps: [searchText] refreshDeps: [searchText]
}); });
const paths = useMemo(() => { const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
return orgStack
.map((org) => {
if (org?.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [orgStack]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]); const filterOrgs: (OrgListItemType & { count?: number })[] = useMemo(() => {
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
if (searchText && searchedData) { if (searchText && searchedData) {
const orgids = searchedData.orgs.map((item) => item._id); const orgids = searchedData.orgs.map((item) => item._id);
return orgs.filter((org) => orgids.includes(String(org._id))); return orgs.filter((org) => orgids.includes(String(org._id)));
@@ -144,7 +108,9 @@ function MemberModal({
})); }));
}, [searchText, orgs, searchedData]); }, [searchText, orgs, searchedData]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]); const [selectedMemberList, setSelectedMemberList] = useState<
Omit<TeamMemberItemType, 'permission' | 'teamId'>[]
>([]);
const filterMembers = useMemo(() => { const filterMembers = useMemo(() => {
if (searchText) { if (searchText) {
return searchedData?.members || []; return searchedData?.members || [];
@@ -152,9 +118,8 @@ function MemberModal({
return members; return members;
}, [searchText, members, searchedData?.members]); }, [searchText, members, searchedData?.members]);
console.log(filterMembers);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]); const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListType>([]);
const filterGroups = useMemo(() => { const filterGroups = useMemo(() => {
if (searchText) { if (searchText) {
return searchedData?.groups.map((item) => ({ return searchedData?.groups.map((item) => ({
@@ -186,9 +151,9 @@ function MemberModal({
const { runAsync: onConfirm, loading: isUpdating } = useRequest2( const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() => () =>
onUpdateCollaborators({ onUpdateCollaborators({
members: selectedMemberIdList, members: selectedMemberList.map((item) => item.tmbId),
groups: selectedGroupIdList, groups: selectedGroupList.map((item) => item._id),
orgs: selectedOrgIdList, orgs: selectedOrgList.map((item) => item._id),
permission: selectedPermission! permission: selectedPermission!
}), }),
{ {
@@ -206,42 +171,31 @@ function MemberModal({
]); ]);
const selectedList = useMemo(() => { const selectedList = useMemo(() => {
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
return [ return [
...selectedOrgs.map((item) => ({ ...selectedOrgList.map((item) => ({
id: `org-${item._id}`, id: `org-${item._id}`,
avatar: item.avatar, avatar: item.avatar,
name: item.name, name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id)), onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)),
orgs: undefined orgs: undefined
})), })),
...selectedGroups.map((item) => ({ ...selectedGroupList.map((item) => ({
id: `group-${item._id}`, id: `group-${item._id}`,
avatar: item.avatar, avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id)), onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)),
orgs: undefined orgs: undefined
})), })),
...selectedMembers.map((item) => ({ ...selectedMemberList.map((item) => ({
id: `member-${item.tmbId}`, id: `member-${item.tmbId}`,
avatar: item.avatar, avatar: item.avatar,
name: item.memberName, name: item.memberName,
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId)), onDelete: () =>
setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)),
orgs: item.orgs orgs: item.orgs
})) }))
]; ];
}, [ }, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]);
orgs,
groups,
members,
selectedOrgIdList,
selectedGroupIdList,
selectedMemberIdList,
userInfo?.team.teamName
]);
return ( return (
<MyModal <MyModal
@@ -324,36 +278,31 @@ function MemberModal({
onClick={(parentId) => { onClick={(parentId) => {
if (parentId === '') { if (parentId === '') {
setFilterClass(undefined); setFilterClass(undefined);
setPath(''); onPathClick('');
} else if ( } else if (
parentId === 'member' || parentId === 'member' ||
parentId === 'org' || parentId === 'org' ||
parentId === 'group' parentId === 'group'
) { ) {
setFilterClass(parentId); setFilterClass(parentId);
setPath(''); onPathClick('');
} else { } else {
setPath(parentId); onPathClick(parentId);
} }
}} }}
rootName={t('common:common.Team')} rootName={t('common:common.Team')}
/> />
</Box> </Box>
)} )}
{(filterClass === 'member' || (searchText && filterMembers.length > 0)) && ( {(filterClass === 'member' || (searchText && filterMembers.length > 0)) &&
<TeamMemberScrollData (() => {
flexDirection={'column'} const members = filterMembers?.map((member) => {
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{filterMembers?.map((member) => {
const onChange = () => { const onChange = () => {
setSelectedMembers((state) => { setSelectedMemberList((state) => {
if (state.includes(member.tmbId)) { if (state.find((v) => v.tmbId === member.tmbId)) {
return state.filter((v) => v !== member.tmbId); return state.filter((v) => v.tmbId !== member.tmbId);
} }
return [...state, member.tmbId]; return [...state, member];
}); });
}; };
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
@@ -364,23 +313,33 @@ function MemberModal({
name={member.memberName} name={member.memberName}
permission={collaborator?.permission.value} permission={collaborator?.permission.value}
onChange={onChange} onChange={onChange}
isChecked={selectedMemberIdList.includes(member.tmbId)} isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
orgs={member.orgs} orgs={member.orgs}
/> />
); );
})} });
</TeamMemberScrollData> return searchText ? (
)} members
) : (
<TeamMemberScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{members}
</TeamMemberScrollData>
);
})()}
{(filterClass === 'org' || searchText) && {(filterClass === 'org' || searchText) &&
(() => { (() => {
const orgs = filterOrgs?.map((org) => { const orgs = filterOrgs?.map((org) => {
const onChange = () => { const onChange = () => {
setSelectedOrgIdList((state) => { setSelectedOrgIdList((state) => {
if (state.includes(org._id)) { if (state.find((v) => v._id === org._id)) {
return state.filter((v) => v !== org._id); return state.filter((v) => v._id !== org._id);
} }
return [...state, org._id]; return [...state, org];
}); });
}; };
const collaborator = collaboratorList?.find((v) => v.orgId === org._id); const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
@@ -396,7 +355,7 @@ function MemberModal({
onClick={onChange} onClick={onChange}
> >
<Checkbox <Checkbox
isChecked={selectedOrgIdList.includes(org._id)} isChecked={!!selectedOrgList.find((v) => v._id === org._id)}
pointerEvents="none" pointerEvents="none"
/> />
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} /> <MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
@@ -442,14 +401,14 @@ function MemberModal({
key={member.tmbId} key={member.tmbId}
name={member.memberName} name={member.memberName}
onChange={() => { onChange={() => {
setSelectedMembers((state) => { setSelectedMemberList((state) => {
if (state.includes(member.tmbId)) { if (state.find((v) => v.tmbId === member.tmbId)) {
return state.filter((v) => v !== member.tmbId); return state.filter((v) => v.tmbId !== member.tmbId);
} }
return [...state, member.tmbId]; return [...state, member];
}); });
}} }}
isChecked={selectedMemberIdList.includes(member.tmbId)} isChecked={!!selectedMemberList.find((v) => v.tmbId === member.tmbId)}
orgs={member.orgs} orgs={member.orgs}
/> />
); );
@@ -459,11 +418,11 @@ function MemberModal({
})()} })()}
{filterGroups?.map((group) => { {filterGroups?.map((group) => {
const onChange = () => { const onChange = () => {
setSelectedGroupIdList((state) => { setSelectedGroupList((state) => {
if (state.includes(group._id)) { if (state.find((v) => v._id === group._id)) {
return state.filter((v) => v !== group._id); return state.filter((v) => v._id !== group._id);
} }
return [...state, group._id]; return [...state, group];
}); });
}; };
const collaborator = collaboratorList?.find((v) => v.groupId === group._id); const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
@@ -476,7 +435,7 @@ function MemberModal({
} }
permission={collaborator?.permission.value} permission={collaborator?.permission.value}
onChange={onChange} onChange={onChange}
isChecked={selectedGroupIdList.includes(group._id)} isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
/> />
); );
})} })}
@@ -486,7 +445,7 @@ function MemberModal({
<Flex h={'100%'} p="4" flexDirection="column"> <Flex h={'100%'} p="4" flexDirection="column">
<Box> <Box>
{`${t('user:has_chosen')}: `} {`${t('user:has_chosen')}: `}
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length} {selectedMemberList.length + selectedGroupList.length + selectedOrgList.length}
</Box> </Box>
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}> <Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => { {selectedList.map((item) => {

View File

@@ -12,14 +12,25 @@ import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api'; import { postCreateGroup, putUpdateGroup } from '@/web/support/user/team/group/api';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
export type GroupFormType = { export type GroupFormType = {
avatar: string; avatar: string;
name: string; name: string;
}; };
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { function GroupInfoModal({
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v); onClose,
editGroupId,
groups,
refetchGroups
}: {
onClose: () => void;
editGroupId?: string;
groups: MemberGroupListType;
refetchGroups: () => void;
}) {
const { refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
File: AvatarSelect, File: AvatarSelect,

View File

@@ -24,7 +24,10 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { GroupMemberItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import {
GroupMemberItemType,
MemberGroupListType
} from '@fastgpt/global/support/permission/memberGroup/type';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
export type GroupFormType = { export type GroupFormType = {
@@ -37,13 +40,21 @@ export type GroupFormType = {
// 1. Owner can not be deleted, toast // 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members // 2. Owner/Admin can manage members
// 3. Owner can add/remove admins // 3. Owner can add/remove admins
function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) { function GroupEditModal({
onClose,
editGroupId,
groups,
refetchGroups
}: {
onClose: () => void;
editGroupId?: string;
groups: MemberGroupListType;
refetchGroups: () => void;
}) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { toast } = useToast(); const { toast } = useToast();
const groups = useContextSelector(TeamContext, (v) => v.groups);
const refetchGroups = useContextSelector(TeamContext, (v) => v.refetchGroups);
const group = useMemo(() => { const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId); return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]); }, [editGroupId, groups]);

View File

@@ -18,25 +18,43 @@ import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import { Omit } from '@fastgpt/web/components/common/DndDrag';
export type ChangeOwnerModalProps = { export type ChangeOwnerModalProps = {
groupId: string; groupId: string;
groups: MemberGroupListType;
refetchGroups: () => void;
}; };
export function ChangeOwnerModal({ export function ChangeOwnerModal({
onClose, onClose,
groupId groupId,
groups,
refetchGroups
}: ChangeOwnerModalProps & { onClose: () => void }) { }: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState(''); const [inputValue, setInputValue] = React.useState('');
const { members: allMembers, groups, refetchGroups } = useContextSelector(TeamContext, (v) => v); const { data: searchedData } = useRequest2(
async () => {
if (!inputValue) return;
return GetSearchUserGroupOrg(inputValue);
},
{
manual: false,
refreshDeps: [inputValue],
throttleWait: 500,
debounceWait: 200
}
);
const { members: allMembers } = useContextSelector(TeamContext, (v) => v);
const group = useMemo(() => { const group = useMemo(() => {
return groups.find((item) => item._id === groupId); return groups.find((item) => item._id === groupId);
}, [groupId, groups]); }, [groupId, groups]);
const memberList = allMembers.filter((item) => { const memberList = searchedData ? searchedData.members : allMembers;
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
});
const [keepAdmin, setKeepAdmin] = useState(true); const [keepAdmin, setKeepAdmin] = useState(true);
@@ -46,7 +64,10 @@ export function ChangeOwnerModal({
onOpen: onOpenMemberListMenu onOpen: onOpenMemberListMenu
} = useDisclosure(); } = useDisclosure();
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null); const [selectedMember, setSelectedMember] = useState<Omit<
TeamMemberItemType,
'permission' | 'teamId'
> | null>(null);
const { runAsync, loading } = useRequest2( const { runAsync, loading } = useRequest2(
(tmbId: string) => putGroupChangeOwner(groupId, tmbId), (tmbId: string) => putGroupChangeOwner(groupId, tmbId),

View File

@@ -22,7 +22,7 @@ import MyMenu, { MenuItemType } from '@fastgpt/web/components/common/MyMenu';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { deleteGroup, getGroupMembers } from '@/web/support/user/team/group/api'; import { deleteGroup, getGroupList, getGroupMembers } from '@/web/support/user/team/group/api';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MemberTag from '../../../../components/support/user/team/Info/MemberTag'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -39,9 +39,19 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { groups, refetchGroups, members, teamSize } = useContextSelector(TeamContext, (v) => v); const { members, teamSize } = useContextSelector(TeamContext, (v) => v);
const {
data: groups = [],
loading: isLoadingGroups,
refresh: refetchGroups
} = useRequest2(getGroupList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const [editGroup, setEditGroup] = useState<MemberGroupType>(); const [editGroup, setEditGroup] = useState<MemberGroupType>();
const { const {
isOpen: isOpenGroupInfo, isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo, onOpen: onOpenGroupInfo,
@@ -106,7 +116,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
)} )}
</Flex> </Flex>
<MyBox flex={'1 0 0'} overflow={'auto'}> <MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingGroups}>
<TableContainer overflow={'unset'} fontSize={'sm'}> <TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}> <Table overflow={'unset'}>
<Thead> <Thead>
@@ -230,10 +240,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<ConfirmDeleteGroupModal /> <ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroup && ( {isOpenChangeOwner && editGroup && (
<ChangeOwnerModal groupId={editGroup._id} onClose={onCloseChangeOwner} /> <ChangeOwnerModal
groupId={editGroup._id}
onClose={onCloseChangeOwner}
groups={groups}
refetchGroups={refetchGroups}
/>
)} )}
{isOpenGroupInfo && ( {isOpenGroupInfo && (
<GroupInfoModal <GroupInfoModal
groups={groups}
refetchGroups={refetchGroups}
onClose={() => { onClose={() => {
onCloseGroupInfo(); onCloseGroupInfo();
setEditGroup(undefined); setEditGroup(undefined);
@@ -243,6 +260,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
)} )}
{isOpenManageGroupMember && editGroup && ( {isOpenManageGroupMember && editGroup && (
<GroupManageMember <GroupManageMember
groups={groups}
refetchGroups={refetchGroups}
onClose={() => { onClose={() => {
onCloseManageGroupMember(); onCloseManageGroupMember();
setEditGroup(undefined); setEditGroup(undefined);

View File

@@ -44,24 +44,20 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react'; import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils'; import { downloadFetch } from '@/web/common/system/utils';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InviteModal = dynamic(() => import('./Invite/InviteModal')); const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) { function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { const { myTeams, refetchTeams, members, refetchMembers, onSwitchTeam, MemberScrollData } =
refetchGroups, useContextSelector(TeamContext, (v) => v);
myTeams,
refetchTeams,
members,
refetchMembers,
onSwitchTeam,
MemberScrollData
} = useContextSelector(TeamContext, (v) => v);
const { const {
isOpen: isOpenTeamTagsAsync, isOpen: isOpenTeamTagsAsync,
@@ -85,7 +81,10 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const isSyncMember = feConfigs.register_method?.includes('sync'); const isSyncMember = feConfigs.register_method?.includes('sync');
const { data: searchMembersData } = useRequest2( const { data: searchMembersData } = useRequest2(
() => GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false }), async () => {
if (!searchText) return Promise.resolve();
return GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false });
},
{ {
manual: false, manual: false,
throttleWait: 500, throttleWait: 500,
@@ -137,13 +136,12 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
canEmpty: false, canEmpty: false,
rows: 1 rows: 1
}); });
const handleEditMemberName = (tmbId: string, memberName: string) => { const handleEditMemberName = (tmbId: string, memberName: string) => {
openEditMemberName({ openEditMemberName({
defaultVal: memberName, defaultVal: memberName,
onSuccess: (newName: string) => { onSuccess: (newName: string) => {
return putUpdateMemberNameByManager(tmbId, newName).then(() => { return putUpdateMemberNameByManager(tmbId, newName).then(() => {
Promise.all([refetchGroups(), refetchMembers()]); refetchMembers();
}); });
}, },
onError: (err) => { onError: (err) => {
@@ -323,10 +321,27 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}} }}
onClick={() => { onClick={() => {
openRemoveMember( openRemoveMember(
() => () => delRemoveMember(member.tmbId).then(refetchMembers),
delRemoveMember(member.tmbId).then(() => undefined,
Promise.all([refetchGroups(), refetchMembers()]) t('account_team:remove_tip', {
), username: member.memberName
})
)();
}}
/>
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() => delRemoveMember(member.tmbId).then(refetchMembers),
undefined, undefined,
t('account_team:remove_tip', { t('account_team:remove_tip', {
username: member.memberName username: member.memberName

View File

@@ -14,8 +14,7 @@ export type OrgFormType = {
avatar: string; avatar: string;
description?: string; description?: string;
name: string; name: string;
path: string; path?: string;
parentId?: string;
}; };
export const defaultOrgForm: OrgFormType = { export const defaultOrgForm: OrgFormType = {
@@ -29,11 +28,13 @@ export const defaultOrgForm: OrgFormType = {
function OrgInfoModal({ function OrgInfoModal({
editOrg, editOrg,
onClose, onClose,
onSuccess onSuccess,
updateCurrentOrg
}: { }: {
editOrg: OrgFormType; editOrg: OrgFormType;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,11 +51,10 @@ function OrgInfoModal({
const { run: onCreate, loading: isLoadingCreate } = useRequest2( const { run: onCreate, loading: isLoadingCreate } = useRequest2(
async (data: OrgFormType) => { async (data: OrgFormType) => {
if (!editOrg.parentId) return;
return postCreateOrg({ return postCreateOrg({
name: data.name, name: data.name,
avatar: data.avatar, avatar: data.avatar,
parentId: editOrg.parentId, path: editOrg.path,
description: data.description description: data.description
}); });
}, },
@@ -67,7 +67,7 @@ function OrgInfoModal({
} }
); );
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: OrgFormType) => { async (data: OrgFormType) => {
if (!editOrg._id) return; if (!editOrg._id) return;
return putUpdateOrg({ return putUpdateOrg({
@@ -144,7 +144,9 @@ function OrgInfoModal({
isLoading={isLoading} isLoading={isLoading}
onClick={handleSubmit((data) => { onClick={handleSubmit((data) => {
if (isEdit) { if (isEdit) {
onUpdate(data); onUpdate(data).then(() => {
updateCurrentOrg(data);
});
} else { } else {
onCreate(data); onCreate(data);
} }

View File

@@ -17,12 +17,11 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector'; import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import { TeamContext } from '../context';
import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { getTeamMembers } from '@/web/support/user/team/api';
export type GroupFormType = { export type GroupFormType = {
members: { members: {
@@ -51,20 +50,39 @@ function OrgMemberManageModal({
onClose: () => void; onClose: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { members: allMembers, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const { data: orgMembers, ScrollData: OrgMemberScrollData } = useScrollPagination(getOrgMembers, { const {
data: allMembers,
ScrollData: MemberScrollData,
isLoading: isLoadingMembers
} = useScrollPagination(getTeamMembers, {
pageSize: 20, pageSize: 20,
params: { params: {
orgId: currentOrg?._id ?? '' withLeaved: false
} }
}); });
const [selectedMembers, setSelectedMembers] = useState<string[]>( const {
orgMembers.map((item) => item.tmbId) data: orgMembers,
); ScrollData: OrgMemberScrollData,
isLoading: isLoadingOrgMembers
} = useScrollPagination(getOrgMembers, {
pageSize: 20,
params: {
orgPath: getOrgChildrenPath(currentOrg)
}
});
const [selected, setSelected] = useState<{ name: string; tmbId: string; avatar: string }[]>([]);
useEffect(() => { useEffect(() => {
setSelectedMembers(orgMembers.map((item) => item.tmbId)); setSelected(
orgMembers.map((item) => ({
name: item.memberName,
tmbId: item.tmbId,
avatar: item.avatar
}))
);
}, [orgMembers]); }, [orgMembers]);
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
@@ -78,8 +96,8 @@ function OrgMemberManageModal({
() => { () => {
return putUpdateOrgMembers({ return putUpdateOrgMembers({
orgId: currentOrg._id, orgId: currentOrg._id,
members: selectedMembers.map((tmbId) => ({ members: selected.map((member) => ({
tmbId tmbId: member.tmbId
})) }))
}); });
}, },
@@ -92,15 +110,25 @@ function OrgMemberManageModal({
} }
); );
const isSelected = (memberId: string) => { const isSelected = (tmbId: string) => {
return selectedMembers.find((tmbId) => tmbId === memberId); return selected.find((tmb) => tmb.tmbId === tmbId);
}; };
const handleToggleSelect = (memberId: string) => { const handleToggleSelect = (tmbId: string) => {
if (isSelected(memberId)) { if (isSelected(tmbId)) {
setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId)); setSelected((state) => state.filter((tmb) => tmb.tmbId !== tmbId));
// setSelectedTmbIds((state) => state.filter((tmbId) => tmbId !== memberId));
} else { } else {
setSelectedMembers((state) => [...state, memberId]); // setSelectedTmbIds((state) => [...state, memberId]);
const member = allMembers.find((item) => item.tmbId === tmbId)!;
setSelected((state) => [
...state,
{
name: member.memberName,
tmbId,
avatar: member.avatar
}
]);
} }
}; };
@@ -123,7 +151,14 @@ function OrgMemberManageModal({
gridTemplateColumns="1fr 1fr" gridTemplateColumns="1fr 1fr"
h={'100%'} h={'100%'}
> >
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden"> <Flex
flexDirection="column"
p="4"
overflowY="auto"
overflowX="hidden"
borderRight={'1px solid'}
borderColor={'myGray.200'}
>
<SearchInput <SearchInput
placeholder={t('user:search_user')} placeholder={t('user:search_user')}
fontSize="sm" fontSize="sm"
@@ -132,7 +167,7 @@ function OrgMemberManageModal({
setSearchKey(e.target.value); setSearchKey(e.target.value);
}} }}
/> />
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}> <MemberScrollData mt={3} flexGrow="1" overflow={'auto'} isLoading={isLoadingMembers}>
{filterMembers.map((member) => { {filterMembers.map((member) => {
return ( return (
<HStack <HStack
@@ -163,30 +198,34 @@ function OrgMemberManageModal({
</Flex> </Flex>
{/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */} {/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */}
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden"> <Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
<OrgMemberScrollData mt={3} flexGrow="1" overflow={'auto'}> <OrgMemberScrollData
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box> mt={3}
{selectedMembers.map((tmbId) => { flexGrow="1"
const member = allMembers.find((item) => item.tmbId === tmbId)!; overflow={'auto'}
isLoading={isLoadingOrgMembers}
>
<Box mt={2}>{`${t('common:chosen')}:${selected.length}`}</Box>
{selected.map((member) => {
return ( return (
<HStack <HStack
justifyContent="space-between" justifyContent="space-between"
py="2" py="2"
px={3} px={3}
borderRadius={'md'} borderRadius={'md'}
key={tmbId} key={member.tmbId}
_hover={{ bg: 'myGray.50' }} _hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }} _notLast={{ mb: 2 }}
> >
<HStack> <HStack>
<Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} /> <Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} />
<Box>{member?.memberName}</Box> <Box>{member?.name}</Box>
</HStack> </HStack>
<MyIcon <MyIcon
name={'common/closeLight'} name={'common/closeLight'}
w={'1rem'} w={'1rem'}
cursor={'pointer'} cursor={'pointer'}
_hover={{ color: 'red.600' }} _hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(tmbId)} onClick={() => handleToggleSelect(member.tmbId)}
/> />
</HStack> </HStack>
); );

View File

@@ -1,4 +1,4 @@
import { putMoveOrg } from '@/web/support/user/team/org/api'; import { getOrgList, putMoveOrg } from '@/web/support/user/team/org/api';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react'; import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type'; import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
@@ -6,17 +6,15 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import OrgTree from './OrgTree'; import OrgTree from './OrgTree';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
function OrgMoveModal({ function OrgMoveModal({
movingOrg, movingOrg,
orgs,
onClose, onClose,
onSuccess onSuccess
}: { }: {
movingOrg: OrgListItemType; movingOrg: OrgListItemType;
orgs: OrgListItemType[];
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
}) { }) {
@@ -32,11 +30,6 @@ function OrgMoveModal({
} }
}); });
const filterMovingOrgs = useMemo(
() => orgs.filter((org) => org._id !== movingOrg._id),
[movingOrg._id, orgs]
);
return ( return (
<MyModal <MyModal
isOpen isOpen
@@ -46,11 +39,7 @@ function OrgMoveModal({
iconColor="primary.600" iconColor="primary.600"
> >
<ModalBody> <ModalBody>
<OrgTree <OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} />
orgs={filterMovingOrgs}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button

View File

@@ -1,29 +1,38 @@
import { Box, HStack, VStack } from '@chakra-ui/react'; import { Box, HStack, VStack } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type'; import type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToggle } from 'ahooks'; import { useToggle } from 'ahooks';
import { useMemo } from 'react'; import { useState } from 'react';
import IconButton from './IconButton'; import IconButton from './IconButton';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getOrgList } from '@/web/support/user/team/org/api';
import { getChildrenByOrg } from '@fastgpt/service/support/permission/org/controllers';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant'; import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
function OrgTreeNode({ function OrgTreeNode({
org, org,
list,
selectedOrg, selectedOrg,
setSelectedOrg, setSelectedOrg,
index = 0 index = 0
}: { }: {
org: OrgType; org: OrgListItemType;
list: OrgType[]; selectedOrg?: OrgListItemType;
selectedOrg?: OrgType; setSelectedOrg: (org?: OrgListItemType) => void;
setSelectedOrg: (org?: OrgType) => void;
index?: number; index?: number;
}) { }) {
const children = useMemo(
() => list.filter((item) => item.path === getOrgChildrenPath(org)),
[org, list]
);
const [isExpanded, toggleIsExpanded] = useToggle(index === 0); const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
const [canBeExpanded, setCanBeExpanded] = useState(true);
const { data: orgs = [], runAsync: getOrgs } = useRequest2(() =>
getOrgList({ orgPath: getOrgChildrenPath(org) })
);
const onClickExpand = async () => {
const data = await getOrgs();
if (data.length < 1) {
setCanBeExpanded(false);
}
toggleIsExpanded.toggle();
};
return ( return (
<Box userSelect={'none'}> <Box userSelect={'none'}>
@@ -34,7 +43,7 @@ function OrgTreeNode({
pr={2} pr={2}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`} pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
cursor={'pointer'} cursor={'pointer'}
{...(selectedOrg === org {...(selectedOrg?._id === org._id
? { ? {
bg: 'primary.50 !important', bg: 'primary.50 !important',
onClick: () => setSelectedOrg(undefined) onClick: () => setSelectedOrg(undefined)
@@ -43,19 +52,17 @@ function OrgTreeNode({
onClick: () => setSelectedOrg(org) onClick: () => setSelectedOrg(org)
})} })}
> >
{index > 0 && ( <IconButton
<IconButton name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'} color={'myGray.500'}
color={'myGray.500'} p={0}
p={0} w={'1.25rem'}
w={'1.25rem'} visibility={canBeExpanded ? 'visible' : 'hidden'}
visibility={children.length > 0 ? 'visible' : 'hidden'} onClick={(e) => {
onClick={(e) => { onClickExpand();
e.stopPropagation(); e.stopPropagation();
toggleIsExpanded.toggle(); }}
}} />
/>
)}
<HStack <HStack
flex={'1 0 0'} flex={'1 0 0'}
onClick={() => setSelectedOrg(org)} onClick={() => setSelectedOrg(org)}
@@ -67,13 +74,12 @@ function OrgTreeNode({
</HStack> </HStack>
</HStack> </HStack>
{isExpanded && {isExpanded &&
children.length > 0 && orgs.length > 0 &&
children.map((child) => ( orgs.map((child) => (
<Box key={child._id} mt={0.5}> <Box key={child._id} mt={0.5}>
<OrgTreeNode <OrgTreeNode
org={child} org={child}
index={index + 1} index={index + 1}
list={list}
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg} setSelectedOrg={setSelectedOrg}
/> />
@@ -84,19 +90,29 @@ function OrgTreeNode({
} }
function OrgTree({ function OrgTree({
orgs,
selectedOrg, selectedOrg,
setSelectedOrg setSelectedOrg
}: { }: {
orgs: OrgType[]; selectedOrg?: OrgListItemType;
selectedOrg?: OrgType; setSelectedOrg: (org?: OrgListItemType) => void;
setSelectedOrg: (org?: OrgType) => void;
}) { }) {
const root = orgs[0]; const { userInfo } = useUserStore();
if (!root) return; const root: OrgListItemType = {
_id: '',
path: '',
pathId: '',
name: userInfo?.team.teamName || '',
avatar: userInfo?.team.avatar || ''
} as any;
return ( return (
<OrgTreeNode org={root} list={orgs} setSelectedOrg={setSelectedOrg} selectedOrg={selectedOrg} /> <OrgTreeNode
key={'root'}
org={root}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
index={1}
/>
); );
} }

View File

@@ -24,12 +24,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import MemberTag from '@/components/support/user/team/Info/MemberTag'; import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
deleteOrg,
deleteOrgMember,
getOrgList,
getOrgMembers
} from '@/web/support/user/team/org/api';
import IconButton from './IconButton'; import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal'; import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
@@ -43,6 +38,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { delRemoveMember } from '@/web/support/user/team/api'; import { delRemoveMember } from '@/web/support/user/team/api';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal')); const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal')); const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
@@ -82,51 +78,25 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo, isTeamAdmin } = useUserStore(); const { userInfo, isTeamAdmin } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs.register_method?.includes('sync'); const isSyncMember = feConfigs.register_method?.includes('sync');
const [searchOrg, setSearchOrg] = useState('');
const [parentId, setParentId] = useState<ParentIdType>();
const [currentOrg, setCurrentOrg] = useState<OrgListItemType>();
// 用于 org 层级
const [orgStack, setOrgStack] = useState<OrgListItemType[]>([]);
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(
() => {
return getOrgList(parentId);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId, parentId]
}
);
const paths = useMemo(() => {
if (!currentOrg) return [];
return orgStack
.map((org) => {
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [currentOrg, orgStack]);
const onClickOrg = (org: OrgListItemType) => {
setParentId(currentOrg?._id);
setOrgStack([...orgStack, org]);
setCurrentOrg(org);
};
const [editOrg, setEditOrg] = useState<OrgFormType>(); const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>(); const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>();
const [movingOrg, setMovingOrg] = useState<OrgListItemType>(); const [movingOrg, setMovingOrg] = useState<OrgListItemType>();
const [searchOrg, setSearchOrg] = useState('');
const {
currentOrg,
orgs,
isLoadingOrgs,
paths,
onClickOrg,
members,
MemberScrollData,
onPathClick,
refresh,
updateCurrentOrg
} = useOrg();
// Delete org // Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({ const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete', type: 'delete',
@@ -134,17 +104,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
}); });
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))(); const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, { const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => { onSuccess: refresh
refetchOrgs();
}
});
const { data: members = [], ScrollData: MemberScrollData } = useScrollPagination(getOrgMembers, {
pageSize: 20,
params: {
orgId: currentOrg?._id
},
refreshDeps: [currentOrg?._id]
}); });
// Delete member // Delete member
@@ -159,15 +119,11 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
}); });
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, { const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => { onSuccess: refresh
refetchOrgs();
}
}); });
const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, { const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, {
onSuccess: () => { onSuccess: refresh
refetchOrgs();
}
}); });
const searchedOrgs = useMemo(() => { const searchedOrgs = useMemo(() => {
@@ -196,7 +152,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
isLoading={isLoadingOrgs} isLoading={isLoadingOrgs}
> >
<Box mb={3}> <Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} /> <Path paths={paths} rootName={userInfo?.team?.teamName} onClick={onPathClick} />
</Box> </Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}> <Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData flex="1"> <MemberScrollData flex="1">
@@ -356,8 +312,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:delete_from_org'), label: t('account_team:delete_from_org'),
onClick: () => onClick: () =>
openDeleteMemberFromOrgModal( openDeleteMemberFromOrgModal(
() => () => {
deleteMemberReq(currentOrg._id, member.tmbId), if (currentOrg) {
return deleteMemberReq(
currentOrg._id,
member.tmbId
);
}
},
undefined, undefined,
t('account_team:confirm_delete_from_org', { t('account_team:confirm_delete_from_org', {
username: member.memberName username: member.memberName
@@ -383,20 +345,15 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
{!isSyncMember && ( {!isSyncMember && (
<VStack w={'180px'} alignItems={'start'}> <VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}> <HStack gap={'6px'}>
<Avatar <Avatar src={currentOrg.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
src={currentOrg?.avatar || userInfo?.team.avatar}
w={'1rem'}
h={'1rem'}
rounded={'xs'}
/>
<Box fontWeight={500} color={'myGray.900'}> <Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name || userInfo?.team.teamName} {currentOrg.name}
</Box> </Box>
{currentOrg && currentOrg?.path !== '' && ( {currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} /> <IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)} )}
</HStack> </HStack>
{currentOrg && ( {currentOrg?.path !== '' && (
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box> <Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
)} )}
@@ -413,7 +370,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onClick={() => { onClick={() => {
setEditOrg({ setEditOrg({
...defaultOrgForm, ...defaultOrgForm,
parentId: currentOrg?._id path: currentOrg.path
}); });
}} }}
/> />
@@ -422,7 +379,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
text={t('account_team:manage_member')} text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)} onClick={() => setManageMemberOrg(currentOrg)}
/> />
{currentOrg && currentOrg?.path !== '' && ( {currentOrg?.path !== '' && (
<> <>
<ActionButton <ActionButton
icon="common/file/move" icon="common/file/move"
@@ -447,21 +404,21 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
<OrgInfoModal <OrgInfoModal
editOrg={editOrg} editOrg={editOrg}
onClose={() => setEditOrg(undefined)} onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs} onSuccess={refresh}
updateCurrentOrg={updateCurrentOrg}
/> />
)} )}
{!!movingOrg && ( {!!movingOrg && (
<OrgMoveModal <OrgMoveModal
orgs={orgs}
movingOrg={movingOrg} movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)} onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs} onSuccess={refresh}
/> />
)} )}
{!!manageMemberOrg && ( {!!manageMemberOrg && (
<OrgMemberManageModal <OrgMemberManageModal
currentOrg={manageMemberOrg} currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs} refetchOrgs={refresh}
onClose={() => setManageMemberOrg(undefined)} onClose={() => setManageMemberOrg(undefined)}
/> />
)} )}

View File

@@ -23,21 +23,18 @@ const EditInfoModal = dynamic(() => import('./EditInfoModal'));
type TeamModalContextType = { type TeamModalContextType = {
myTeams: TeamTmbItemType[]; myTeams: TeamTmbItemType[];
members: TeamMemberItemType[]; members: TeamMemberItemType[];
groups: MemberGroupListType;
isLoading: boolean; isLoading: boolean;
onSwitchTeam: (teamId: string) => void; onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>; setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
refetchMembers: () => void; refetchMembers: () => void;
refetchTeams: () => void; refetchTeams: () => void;
refetchGroups: () => void;
teamSize: number; teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData']; MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
}; };
export const TeamContext = createContext<TeamModalContextType>({ export const TeamContext = createContext<TeamModalContextType>({
myTeams: [], myTeams: [],
groups: [],
members: [], members: [],
isLoading: false, isLoading: false,
onSwitchTeam: function (_teamId: string): void { onSwitchTeam: function (_teamId: string): void {
@@ -52,9 +49,6 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchMembers: function (): void { refetchMembers: function (): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
teamSize: 0, teamSize: 0,
MemberScrollData: () => <></> MemberScrollData: () => <></>
}); });
@@ -110,16 +104,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
} }
); );
const { const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers;
data: groups = [],
loading: isLoadingGroups,
refresh: refetchGroups
} = useRequest2(getGroupList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const contextValue = { const contextValue = {
myTeams, myTeams,
@@ -131,8 +116,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
setEditTeamData, setEditTeamData,
members, members,
refetchMembers, refetchMembers,
groups,
refetchGroups,
teamSize: teamMemberCountData?.count || 0, teamSize: teamMemberCountData?.count || 0,
MemberScrollData MemberScrollData
}; };

View File

@@ -10,8 +10,8 @@ import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/t
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
export const getOrgList = (parentId: ParentIdType) => export const getOrgList = (params: { orgPath: string; getPermission?: boolean }) =>
GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, { parentId }); GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
export const postCreateOrg = (data: postCreateOrgData) => export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data); POST('/proApi/support/user/team/org/create', data);
@@ -19,19 +19,17 @@ export const postCreateOrg = (data: postCreateOrgData) =>
export const deleteOrg = (orgId: string) => export const deleteOrg = (orgId: string) =>
DELETE('/proApi/support/user/team/org/delete', { orgId }); DELETE('/proApi/support/user/team/org/delete', { orgId });
export const deleteOrgMember = (orgId: string, tmbId: string) =>
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
export const putMoveOrg = (data: putMoveOrgType) => PUT('/proApi/support/user/team/org/move', data); export const putMoveOrg = (data: putMoveOrgType) => PUT('/proApi/support/user/team/org/move', data);
export const putUpdateOrg = (data: putUpdateOrgData) => export const putUpdateOrg = (data: putUpdateOrgData) =>
PUT('/proApi/support/user/team/org/update', data); PUT('/proApi/support/user/team/org/update', data);
// org members
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) => export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
PUT('/proApi/support/user/team/org/updateMembers', data); PUT('/proApi/support/user/team/org/updateMembers', data);
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) => export const getOrgMembers = (data: PaginationProps<{ orgPath?: string }>) =>
// 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); GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/org/members`, data);
export const deleteOrgMember = (orgId: string, tmbId: string) =>
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });

View File

@@ -0,0 +1,108 @@
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import { memo, useMemo, useState } from 'react';
import { useUserStore } from '../../../useUserStore';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getOrgList, getOrgMembers } from '../api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
const [orgStack, setOrgStack] = useState<OrgListItemType[]>([]);
const { userInfo } = useUserStore();
const path = useMemo(
() => (orgStack.length ? getOrgChildrenPath(orgStack[orgStack.length - 1]) : ''),
[orgStack]
);
const currentOrg = useMemo(() => {
return (
orgStack.at(-1) ??
({
_id: '',
path: '',
pathId: '',
avatar: userInfo?.team.avatar,
name: userInfo?.team.teamName
} as OrgListItemType) // root org
);
}, [orgStack, userInfo?.team.avatar, userInfo?.team.teamName]);
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(() => getOrgList({ orgPath: path, getPermission }), {
manual: false,
refreshDeps: [userInfo?.team?.teamId, path]
});
const paths = useMemo(() => {
if (!currentOrg) return [];
return orgStack
.map((org) => {
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [currentOrg, orgStack]);
const onClickOrg = (org: OrgListItemType) => {
setOrgStack([...orgStack, org]);
};
const {
data: members = [],
ScrollData: MemberScrollData,
refreshList: refetchMembers
} = useScrollPagination(getOrgMembers, {
pageSize: 20,
params: {
orgPath: path
},
refreshDeps: [path]
});
const onPathClick = (path: string) => {
const pathIds = path.split('/');
setOrgStack(orgStack.filter((org) => pathIds.includes(org.pathId)));
};
const refresh = () => {
refetchOrgs();
refetchMembers();
};
const updateCurrentOrg = (data: { name?: string; description?: string; avatar?: string }) => {
if (currentOrg.path === '') return;
setOrgStack([
...orgStack.slice(0, -1),
{
...currentOrg,
name: data.name || currentOrg.name,
description: data.description || currentOrg.description,
avatar: data.avatar || currentOrg.avatar
}
]);
};
return {
orgStack,
currentOrg,
orgs,
isLoadingOrgs,
paths,
onClickOrg,
members,
MemberScrollData,
onPathClick,
refresh,
updateCurrentOrg
};
}
export default useOrg;