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

@@ -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)}
/>
)}