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

View File

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

View File

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

View File

@@ -24,7 +24,10 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
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';
export type GroupFormType = {
@@ -37,13 +40,21 @@ export type GroupFormType = {
// 1. Owner can not be deleted, toast
// 2. Owner/Admin can manage members
// 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 { userInfo } = useUserStore();
const { toast } = useToast();
const groups = useContextSelector(TeamContext, (v) => v.groups);
const refetchGroups = useContextSelector(TeamContext, (v) => v.refetchGroups);
const group = useMemo(() => {
return groups.find((item) => item._id === editGroupId);
}, [editGroupId, groups]);

View File

@@ -18,25 +18,43 @@ import { useTranslation } from 'next-i18next';
import React, { useMemo, useState } from 'react';
import { TeamContext } from '../context';
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 = {
groupId: string;
groups: MemberGroupListType;
refetchGroups: () => void;
};
export function ChangeOwnerModal({
onClose,
groupId
groupId,
groups,
refetchGroups
}: ChangeOwnerModalProps & { onClose: () => void }) {
const { t } = useTranslation();
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(() => {
return groups.find((item) => item._id === groupId);
}, [groupId, groups]);
const memberList = allMembers.filter((item) => {
return item.memberName.toLowerCase().includes(inputValue.toLowerCase());
});
const memberList = searchedData ? searchedData.members : allMembers;
const [keepAdmin, setKeepAdmin] = useState(true);
@@ -46,7 +64,10 @@ export function ChangeOwnerModal({
onOpen: onOpenMemberListMenu
} = useDisclosure();
const [selectedMember, setSelectedMember] = useState<TeamMemberItemType | null>(null);
const [selectedMember, setSelectedMember] = useState<Omit<
TeamMemberItemType,
'permission' | 'teamId'
> | null>(null);
const { runAsync, loading } = useRequest2(
(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 { useUserStore } from '@/web/support/user/useUserStore';
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 MemberTag from '../../../../components/support/user/team/Info/MemberTag';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -39,9 +39,19 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
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 {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
@@ -106,7 +116,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
)}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'}>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingGroups}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
@@ -230,10 +240,17 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroup && (
<ChangeOwnerModal groupId={editGroup._id} onClose={onCloseChangeOwner} />
<ChangeOwnerModal
groupId={editGroup._id}
onClose={onCloseChangeOwner}
groups={groups}
refetchGroups={refetchGroups}
/>
)}
{isOpenGroupInfo && (
<GroupInfoModal
groups={groups}
refetchGroups={refetchGroups}
onClose={() => {
onCloseGroupInfo();
setEditGroup(undefined);
@@ -243,6 +260,8 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
)}
{isOpenManageGroupMember && editGroup && (
<GroupManageMember
groups={groups}
refetchGroups={refetchGroups}
onClose={() => {
onCloseManageGroupMember();
setEditGroup(undefined);

View File

@@ -44,24 +44,20 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { toast } = useToast();
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const {
refetchGroups,
myTeams,
refetchTeams,
members,
refetchMembers,
onSwitchTeam,
MemberScrollData
} = useContextSelector(TeamContext, (v) => v);
const { myTeams, refetchTeams, members, refetchMembers, onSwitchTeam, MemberScrollData } =
useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
@@ -85,7 +81,10 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const isSyncMember = feConfigs.register_method?.includes('sync');
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,
throttleWait: 500,
@@ -137,13 +136,12 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
canEmpty: false,
rows: 1
});
const handleEditMemberName = (tmbId: string, memberName: string) => {
openEditMemberName({
defaultVal: memberName,
onSuccess: (newName: string) => {
return putUpdateMemberNameByManager(tmbId, newName).then(() => {
Promise.all([refetchGroups(), refetchMembers()]);
refetchMembers();
});
},
onError: (err) => {
@@ -323,10 +321,27 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(member.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
() => delRemoveMember(member.tmbId).then(refetchMembers),
undefined,
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,
t('account_team:remove_tip', {
username: member.memberName

View File

@@ -14,8 +14,7 @@ export type OrgFormType = {
avatar: string;
description?: string;
name: string;
path: string;
parentId?: string;
path?: string;
};
export const defaultOrgForm: OrgFormType = {
@@ -29,11 +28,13 @@ export const defaultOrgForm: OrgFormType = {
function OrgInfoModal({
editOrg,
onClose,
onSuccess
onSuccess,
updateCurrentOrg
}: {
editOrg: OrgFormType;
onClose: () => void;
onSuccess: () => void;
updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void;
}) {
const { t } = useTranslation();
@@ -50,11 +51,10 @@ function OrgInfoModal({
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg.parentId) return;
return postCreateOrg({
name: data.name,
avatar: data.avatar,
parentId: editOrg.parentId,
path: editOrg.path,
description: data.description
});
},
@@ -67,7 +67,7 @@ function OrgInfoModal({
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg._id) return;
return putUpdateOrg({
@@ -144,7 +144,9 @@ function OrgInfoModal({
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (isEdit) {
onUpdate(data);
onUpdate(data).then(() => {
updateCurrentOrg(data);
});
} else {
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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
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 = {
members: {
@@ -51,20 +50,39 @@ function OrgMemberManageModal({
onClose: () => void;
}) {
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,
params: {
orgId: currentOrg?._id ?? ''
withLeaved: false
}
});
const [selectedMembers, setSelectedMembers] = useState<string[]>(
orgMembers.map((item) => item.tmbId)
);
const {
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(() => {
setSelectedMembers(orgMembers.map((item) => item.tmbId));
setSelected(
orgMembers.map((item) => ({
name: item.memberName,
tmbId: item.tmbId,
avatar: item.avatar
}))
);
}, [orgMembers]);
const [searchKey, setSearchKey] = useState('');
@@ -78,8 +96,8 @@ function OrgMemberManageModal({
() => {
return putUpdateOrgMembers({
orgId: currentOrg._id,
members: selectedMembers.map((tmbId) => ({
tmbId
members: selected.map((member) => ({
tmbId: member.tmbId
}))
});
},
@@ -92,15 +110,25 @@ function OrgMemberManageModal({
}
);
const isSelected = (memberId: string) => {
return selectedMembers.find((tmbId) => tmbId === memberId);
const isSelected = (tmbId: string) => {
return selected.find((tmb) => tmb.tmbId === tmbId);
};
const handleToggleSelect = (memberId: string) => {
if (isSelected(memberId)) {
setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId));
const handleToggleSelect = (tmbId: string) => {
if (isSelected(tmbId)) {
setSelected((state) => state.filter((tmb) => tmb.tmbId !== tmbId));
// setSelectedTmbIds((state) => state.filter((tmbId) => tmbId !== memberId));
} 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"
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
placeholder={t('user:search_user')}
fontSize="sm"
@@ -132,7 +167,7 @@ function OrgMemberManageModal({
setSearchKey(e.target.value);
}}
/>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'} isLoading={isLoadingMembers}>
{filterMembers.map((member) => {
return (
<HStack
@@ -163,30 +198,34 @@ function OrgMemberManageModal({
</Flex>
{/* <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)!;
<OrgMemberScrollData
mt={3}
flexGrow="1"
overflow={'auto'}
isLoading={isLoadingOrgMembers}
>
<Box mt={2}>{`${t('common:chosen')}:${selected.length}`}</Box>
{selected.map((member) => {
return (
<HStack
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={tmbId}
key={member.tmbId}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<HStack>
<Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} />
<Box>{member?.memberName}</Box>
<Box>{member?.name}</Box>
</HStack>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(tmbId)}
onClick={() => handleToggleSelect(member.tmbId)}
/>
</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 type { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type';
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 { useMemo, useState } from 'react';
import OrgTree from './OrgTree';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/web/support/user/useUserStore';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
function OrgMoveModal({
movingOrg,
orgs,
onClose,
onSuccess
}: {
movingOrg: OrgListItemType;
orgs: OrgListItemType[];
onClose: () => void;
onSuccess: () => void;
}) {
@@ -32,11 +30,6 @@ function OrgMoveModal({
}
});
const filterMovingOrgs = useMemo(
() => orgs.filter((org) => org._id !== movingOrg._id),
[movingOrg._id, orgs]
);
return (
<MyModal
isOpen
@@ -46,11 +39,7 @@ function OrgMoveModal({
iconColor="primary.600"
>
<ModalBody>
<OrgTree
orgs={filterMovingOrgs}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
<OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} />
</ModalBody>
<ModalFooter>
<Button

View File

@@ -1,29 +1,38 @@
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 { useToggle } from 'ahooks';
import { useMemo } from 'react';
import { useState } from 'react';
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';
function OrgTreeNode({
org,
list,
selectedOrg,
setSelectedOrg,
index = 0
}: {
org: OrgType;
list: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
org: OrgListItemType;
selectedOrg?: OrgListItemType;
setSelectedOrg: (org?: OrgListItemType) => void;
index?: number;
}) {
const children = useMemo(
() => list.filter((item) => item.path === getOrgChildrenPath(org)),
[org, list]
);
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 (
<Box userSelect={'none'}>
@@ -34,7 +43,7 @@ function OrgTreeNode({
pr={2}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
cursor={'pointer'}
{...(selectedOrg === org
{...(selectedOrg?._id === org._id
? {
bg: 'primary.50 !important',
onClick: () => setSelectedOrg(undefined)
@@ -43,19 +52,17 @@ function OrgTreeNode({
onClick: () => setSelectedOrg(org)
})}
>
{index > 0 && (
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
color={'myGray.500'}
p={0}
w={'1.25rem'}
visibility={children.length > 0 ? 'visible' : 'hidden'}
onClick={(e) => {
e.stopPropagation();
toggleIsExpanded.toggle();
}}
/>
)}
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
color={'myGray.500'}
p={0}
w={'1.25rem'}
visibility={canBeExpanded ? 'visible' : 'hidden'}
onClick={(e) => {
onClickExpand();
e.stopPropagation();
}}
/>
<HStack
flex={'1 0 0'}
onClick={() => setSelectedOrg(org)}
@@ -67,13 +74,12 @@ function OrgTreeNode({
</HStack>
</HStack>
{isExpanded &&
children.length > 0 &&
children.map((child) => (
orgs.length > 0 &&
orgs.map((child) => (
<Box key={child._id} mt={0.5}>
<OrgTreeNode
org={child}
index={index + 1}
list={list}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
@@ -84,19 +90,29 @@ function OrgTreeNode({
}
function OrgTree({
orgs,
selectedOrg,
setSelectedOrg
}: {
orgs: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
selectedOrg?: OrgListItemType;
setSelectedOrg: (org?: OrgListItemType) => void;
}) {
const root = orgs[0];
if (!root) return;
const { userInfo } = useUserStore();
const root: OrgListItemType = {
_id: '',
path: '',
pathId: '',
name: userInfo?.team.teamName || '',
avatar: userInfo?.team.avatar || ''
} as any;
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 { useMemo, useState } from 'react';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import {
deleteOrg,
deleteOrgMember,
getOrgList,
getOrgMembers
} from '@/web/support/user/team/org/api';
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
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 SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
@@ -82,51 +78,25 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { userInfo, isTeamAdmin } = useUserStore();
const { feConfigs } = useSystemStore();
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 [manageMemberOrg, setManageMemberOrg] = 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
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
@@ -134,17 +104,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
const { data: members = [], ScrollData: MemberScrollData } = useScrollPagination(getOrgMembers, {
pageSize: 20,
params: {
orgId: currentOrg?._id
},
refreshDeps: [currentOrg?._id]
onSuccess: refresh
});
// Delete member
@@ -159,15 +119,11 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
onSuccess: refresh
});
const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, {
onSuccess: () => {
refetchOrgs();
}
onSuccess: refresh
});
const searchedOrgs = useMemo(() => {
@@ -196,7 +152,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
isLoading={isLoadingOrgs}
>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} />
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={onPathClick} />
</Box>
<Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
<MemberScrollData flex="1">
@@ -356,8 +312,14 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
label: t('account_team:delete_from_org'),
onClick: () =>
openDeleteMemberFromOrgModal(
() =>
deleteMemberReq(currentOrg._id, member.tmbId),
() => {
if (currentOrg) {
return deleteMemberReq(
currentOrg._id,
member.tmbId
);
}
},
undefined,
t('account_team:confirm_delete_from_org', {
username: member.memberName
@@ -383,20 +345,15 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
{!isSyncMember && (
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar
src={currentOrg?.avatar || userInfo?.team.avatar}
w={'1rem'}
h={'1rem'}
rounded={'xs'}
/>
<Avatar src={currentOrg.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name || userInfo?.team.teamName}
{currentOrg.name}
</Box>
{currentOrg && currentOrg?.path !== '' && (
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
{currentOrg && (
{currentOrg?.path !== '' && (
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
)}
@@ -413,7 +370,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
path: currentOrg.path
});
}}
/>
@@ -422,7 +379,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg && currentOrg?.path !== '' && (
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
@@ -447,21 +404,21 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
onSuccess={refresh}
updateCurrentOrg={updateCurrentOrg}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
onSuccess={refresh}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
refetchOrgs={refresh}
onClose={() => setManageMemberOrg(undefined)}
/>
)}

View File

@@ -23,21 +23,18 @@ const EditInfoModal = dynamic(() => import('./EditInfoModal'));
type TeamModalContextType = {
myTeams: TeamTmbItemType[];
members: TeamMemberItemType[];
groups: MemberGroupListType;
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
};
export const TeamContext = createContext<TeamModalContextType>({
myTeams: [],
groups: [],
members: [],
isLoading: false,
onSwitchTeam: function (_teamId: string): void {
@@ -52,9 +49,6 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchMembers: function (): void {
throw new Error('Function not implemented.');
},
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
teamSize: 0,
MemberScrollData: () => <></>
});
@@ -110,16 +104,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
}
);
const {
data: groups = [],
loading: isLoadingGroups,
refresh: refetchGroups
} = useRequest2(getGroupList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers;
const contextValue = {
myTeams,
@@ -131,8 +116,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
setEditTeamData,
members,
refetchMembers,
groups,
refetchGroups,
teamSize: teamMemberCountData?.count || 0,
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 { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
export const getOrgList = (parentId: ParentIdType) =>
GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, { parentId });
export const getOrgList = (params: { orgPath: string; getPermission?: boolean }) =>
GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data);
@@ -19,19 +19,17 @@ export const postCreateOrg = (data: postCreateOrgData) =>
export const deleteOrg = (orgId: string) =>
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 putUpdateOrg = (data: putUpdateOrgData) =>
PUT('/proApi/support/user/team/org/update', data);
// org members
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
PUT('/proApi/support/user/team/org/updateMembers', data);
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
// PUT('/proApi/support/user/team/org/changeOwner', data);
export const getOrgMembers = (data: PaginationProps<{ orgId: string }>) =>
export const getOrgMembers = (data: PaginationProps<{ orgPath?: string }>) =>
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;