mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
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:
@@ -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,
|
||||
|
@@ -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) => {
|
||||
|
@@ -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,
|
||||
|
@@ -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]);
|
||||
|
@@ -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),
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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
|
||||
};
|
||||
|
@@ -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 });
|
||||
|
108
projects/app/src/web/support/user/team/org/hooks/useOrg.tsx
Normal file
108
projects/app/src/web/support/user/team/org/hooks/useOrg.tsx
Normal 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;
|
Reference in New Issue
Block a user