pref: member list (#4344)

* chore: search member new api

* chore: permission

* fix: ts error

* fix: member modal
This commit is contained in:
Finley Ge
2025-03-26 22:10:03 +08:00
committed by archer
parent 484b87478c
commit 2ebb2ccc9c
15 changed files with 508 additions and 548 deletions

View File

@@ -1,9 +1,8 @@
// orgId, pathid, path === null ===> root org
export type postCreateOrgData = { export type postCreateOrgData = {
name: string; name: string;
description?: string; description?: string;
avatar?: string; avatar?: string;
path?: string; orgId?: string;
}; };
export type putUpdateOrgMembersData = { export type putUpdateOrgMembersData = {

View File

@@ -70,7 +70,13 @@ export type TeamTmbItemType = {
permission: TeamPermission; permission: TeamPermission;
} & ThirdPartyAccountType; } & ThirdPartyAccountType;
export type TeamMemberItemType = { export type TeamMemberItemType<
Options extends {
withPermission?: boolean;
withOrgs?: boolean;
withGroupRole?: boolean;
} = { withPermission: true; withOrgs: true; withGroupRole: false }
> = {
userId: string; userId: string;
tmbId: string; tmbId: string;
teamId: string; teamId: string;
@@ -78,12 +84,24 @@ export type TeamMemberItemType = {
avatar: string; avatar: string;
role: `${TeamMemberRoleEnum}`; role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`; status: `${TeamMemberStatusEnum}`;
permission: TeamPermission;
contact?: string; contact?: string;
createTime: Date; createTime: Date;
updateTime?: Date; updateTime?: Date;
orgs?: string[]; // full path name, pattern: /teamName/orgname1/orgname2 } & (Options extends { withPermission: true }
}; ? {
permission: TeamPermission;
}
: {}) &
(Options extends { withOrgs: true }
? {
orgs?: string[]; // full path name, pattern: /teamName/orgname1/orgname2
}
: {}) &
(Options extends { withGroupRole: true }
? {
groupRole?: `${GroupMemberRole}`;
}
: {});
export type TeamTagItemType = { export type TeamTagItemType = {
label: string; label: string;

View File

@@ -19,7 +19,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import PermissionSelect from './PermissionSelect'; import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags'; import PermissionTags from './PermissionTags';
import { import {
@@ -39,6 +39,7 @@ import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import useOrg from '@/web/support/user/team/org/hooks/useOrg';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
import _ from 'lodash';
const HoverBoxStyle = { const HoverBoxStyle = {
bgColor: 'myGray.50', bgColor: 'myGray.50',
@@ -55,7 +56,6 @@ function MemberModal({
const { t } = useTranslation(); const { t } = useTranslation();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { const {
paths, paths,
@@ -63,18 +63,35 @@ function MemberModal({
members: orgMembers, members: orgMembers,
MemberScrollData: OrgMemberScrollData, MemberScrollData: OrgMemberScrollData,
onPathClick, onPathClick,
orgs orgs,
} = useOrg({ getPermission: false }); searchKey,
setSearchKey
} = useOrg({ withPermission: false });
const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, { const {
pageSize: 15 data: members,
ScrollData: TeamMemberScrollData,
refreshList
} = useScrollPagination(getTeamMembers, {
pageSize: 15,
params: {
withPermission: true,
withOrgs: true,
status: 'active',
searchKey
}
}); });
const { data: groups = [], loading: loadingGroupsAndOrgs } = useRequest2( const {
data: groups = [],
loading: loadingGroupsAndOrgs,
runAsync: refreshGroups
} = useRequest2(
async () => { async () => {
if (!userInfo?.team?.teamId) return []; if (!userInfo?.team?.teamId) return [];
return getGroupList<false>({ return getGroupList<false>({
withMembers: false withMembers: false,
searchKey
}); });
}, },
{ {
@@ -83,53 +100,20 @@ function MemberModal({
} }
); );
const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), { const search = _.debounce(() => {
manual: false, refreshList();
throttleWait: 500, refreshGroups();
debounceWait: 200, }, 200);
refreshDeps: [searchText]
}); useEffect(search, [searchKey]);
const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]); const [selectedOrgList, setSelectedOrgIdList] = useState<OrgListItemType[]>([]);
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)));
}
return orgs
.filter((org) => org.path !== '')
.map((org) => ({
...org,
count: org.total
}));
}, [searchText, orgs, searchedData]);
const [selectedMemberList, setSelectedMemberList] = useState< const [selectedMemberList, setSelectedMemberList] = useState<
Omit<TeamMemberItemType, 'permission' | 'teamId'>[] Omit<TeamMemberItemType, 'permission' | 'teamId'>[]
>([]); >([]);
const filterMembers = useMemo(() => {
if (searchText) {
return searchedData?.members || [];
}
return members;
}, [searchText, members, searchedData?.members]);
const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]); const [selectedGroupList, setSelectedGroupList] = useState<MemberGroupListItemType<false>[]>([]);
const filterGroups = useMemo(() => {
if (searchText) {
return searchedData?.groups.map((item) => ({
groupName: item.name,
_id: item.id,
...item
}));
}
if (!searchText && filterClass !== 'group') return [];
return groups;
}, [searchText, filterClass, groups, searchedData]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList); const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList); const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
const [selectedPermission, setSelectedPermission] = useState<number | undefined>( const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
@@ -225,12 +209,12 @@ function MemberModal({
<SearchInput <SearchInput
placeholder={t('user:search_group_org_user')} placeholder={t('user:search_group_org_user')}
bgColor="myGray.50" bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchKey(e.target.value)}
/> />
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}> <Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{/* Entry */} {/* Entry */}
{!searchText && !filterClass && ( {!searchKey && !filterClass && (
<> <>
{entryList.current.map((item) => { {entryList.current.map((item) => {
return ( return (
@@ -257,7 +241,7 @@ function MemberModal({
)} )}
{/* Path */} {/* Path */}
{!searchText && filterClass && ( {!searchKey && filterClass && (
<Box mb={1}> <Box mb={1}>
<Path <Path
paths={[ paths={[
@@ -291,9 +275,9 @@ function MemberModal({
/> />
</Box> </Box>
)} )}
{(filterClass === 'member' || (searchText && filterMembers.length > 0)) && {(filterClass === 'member' || searchKey) &&
(() => { (() => {
const members = filterMembers?.map((member) => { const Members = members?.map((member) => {
const onChange = () => { const onChange = () => {
setSelectedMemberList((state) => { setSelectedMemberList((state) => {
if (state.find((v) => v.tmbId === member.tmbId)) { if (state.find((v) => v.tmbId === member.tmbId)) {
@@ -315,8 +299,8 @@ function MemberModal({
/> />
); );
}); });
return searchText ? ( return searchKey ? (
members Members
) : ( ) : (
<TeamMemberScrollData <TeamMemberScrollData
flexDirection={'column'} flexDirection={'column'}
@@ -324,13 +308,13 @@ function MemberModal({
userSelect={'none'} userSelect={'none'}
height={'fit-content'} height={'fit-content'}
> >
{members} {Members}
</TeamMemberScrollData> </TeamMemberScrollData>
); );
})()} })()}
{(filterClass === 'org' || searchText) && {(filterClass === 'org' || searchKey) &&
(() => { (() => {
const orgs = filterOrgs?.map((org) => { const Orgs = orgs?.map((org) => {
const onChange = () => { const onChange = () => {
setSelectedOrgIdList((state) => { setSelectedOrgIdList((state) => {
if (state.find((v) => v._id === org._id)) { if (state.find((v) => v._id === org._id)) {
@@ -356,18 +340,18 @@ function MemberModal({
pointerEvents="none" pointerEvents="none"
/> />
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} /> <MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px"> <HStack w="full">
<Text>{org.name}</Text> <Text>{org.name}</Text>
{org.count && ( {org.total && (
<> <>
<Tag size="sm" my="auto"> <Tag size="sm" my="auto">
{org.count} {org.total}
</Tag> </Tag>
</> </>
)} )}
</HStack> </HStack>
<PermissionTags permission={collaborator?.permission.value} /> <PermissionTags permission={collaborator?.permission.value} />
{org.count && ( {org.total && (
<MyIcon <MyIcon
name="core/chat/chevronRight" name="core/chat/chevronRight"
w="16px" w="16px"
@@ -386,11 +370,11 @@ function MemberModal({
</HStack> </HStack>
); );
}); });
return searchText ? ( return searchKey ? (
orgs Orgs
) : ( ) : (
<OrgMemberScrollData> <OrgMemberScrollData>
{orgs} {Orgs}
{orgMembers.map((member) => { {orgMembers.map((member) => {
return ( return (
<MemberItemCard <MemberItemCard
@@ -413,29 +397,30 @@ function MemberModal({
</OrgMemberScrollData> </OrgMemberScrollData>
); );
})()} })()}
{filterGroups?.map((group) => { {(filterClass === 'group' || searchKey) &&
const onChange = () => { groups?.map((group) => {
setSelectedGroupList((state) => { const onChange = () => {
if (state.find((v) => v._id === group._id)) { setSelectedGroupList((state) => {
return state.filter((v) => v._id !== group._id); if (state.find((v) => v._id === group._id)) {
} return state.filter((v) => v._id !== group._id);
return [...state, group]; }
}); return [...state, group];
}; });
const collaborator = collaboratorList?.find((v) => v.groupId === group._id); };
return ( const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
<MemberItemCard return (
avatar={group.avatar} <MemberItemCard
key={group._id} avatar={group.avatar}
name={ key={group._id}
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name name={
} group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
permission={collaborator?.permission.value} }
onChange={onChange} permission={collaborator?.permission.value}
isChecked={!!selectedGroupList.find((v) => v._id === group._id)} onChange={onChange}
/> isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
); />
})} );
})}
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -1,34 +1,25 @@
import { import { Box, ModalBody, Flex, Button, ModalFooter, Grid, HStack } from '@chakra-ui/react';
Box,
ModalBody,
Flex,
Button,
ModalFooter,
Checkbox,
Grid,
HStack
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import Tag from '@fastgpt/web/components/common/Tag'; import Tag from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector'; import { putUpdateGroup } from '@/web/support/user/team/group/api';
import { TeamContext } from '../context';
import { getGroupMembers, putUpdateGroup } from '@/web/support/user/team/group/api';
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants'; import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
GroupMemberItemType, import { getTeamMembers } from '@/web/support/user/team/api';
MemberGroupListItemType import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
} from '@fastgpt/global/support/permission/memberGroup/type'; import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { useMount } from 'ahooks'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import _ from 'lodash';
import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
export type GroupFormType = { export type GroupFormType = {
members: { members: {
@@ -53,42 +44,65 @@ function GroupEditModal({
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { toast } = useToast(); const { toast } = useToast();
const allMembers = useContextSelector(TeamContext, (v) => v.members); const [searchKey, setSearchKey] = useState('');
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData); const [selected, setSelected] = useState<
const [hoveredMemberId, setHoveredMemberId] = useState<string>(); { name: string; tmbId: string; avatar: string; role: `${GroupMemberRole}` }[]
>([]);
const selectedMembersRef = useRef<HTMLDivElement>(null); const {
const [members, setMembers] = useState<GroupMemberItemType[]>([]); data: allMembers = [],
const editGroupId = useMemo(() => { ScrollData: MemberScrollData,
return group?._id; refreshList
}, [group]); } = useScrollPagination<
any,
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
>(getTeamMembers, {
pageSize: 20,
params: {
status: 'active',
withOrgs: true,
searchKey
}
});
const refetchMemberList = _.debounce(refreshList, 200);
useMount(async () => { useEffect(() => refetchMemberList, [searchKey]);
console.log('aaaaaa');
if (editGroupId) { const groupId = useMemo(() => String(group._id), [group._id]);
const data = await getGroupMembers(editGroupId);
console.log(data); const { data: groupMembers = [], ScrollData: GroupScrollData } = useScrollPagination<
setMembers(data); any,
PaginationResponse<
TeamMemberItemType<{ withOrgs: true; withPermission: true; withGroupRole: true }>
>
>(getTeamMembers, {
pageSize: 100000,
params: {
groupId: groupId
} }
}); });
const [searchKey, setSearchKey] = useState(''); useEffect(() => {
const filtered = useMemo(() => { if (!groupId) return;
return [ setSelected(
...allMembers.filter((member) => { groupMembers.map((item) => ({
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true; name: item.memberName,
return false; tmbId: item.tmbId,
}) avatar: item.avatar,
]; role: (item.groupRole ?? 'member') as `${GroupMemberRole}`
}, [searchKey, allMembers]); }))
);
}, [groupId, groupMembers]);
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2( const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
async () => { async () => {
if (!editGroupId || !members.length) return; if (!group._id || !groupMembers.length) return;
return putUpdateGroup({ return putUpdateGroup({
groupId: editGroupId, groupId: group._id,
memberList: members memberList: selected
}); });
}, },
{ {
@@ -97,18 +111,21 @@ function GroupEditModal({
); );
const isSelected = (memberId: string) => { const isSelected = (memberId: string) => {
return members.find((item) => item.tmbId === memberId); return selected.find((item) => item.tmbId === memberId);
}; };
const myRole = useMemo(() => { const myRole = useMemo(() => {
if (userInfo?.team.permission.hasManagePer) { if (userInfo?.team.permission.hasManagePer) {
return 'owner'; return 'owner';
} }
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member'; return groupMembers.find((item) => item.tmbId === userInfo?.team.tmbId)?.groupRole ?? 'member';
}, [members, userInfo]); }, [groupMembers, userInfo]);
const handleToggleSelect = (memberId: string) => { const handleToggleSelect = (memberId: string) => {
if (myRole === 'owner' && memberId === members.find((item) => item.role === 'owner')?.tmbId) { if (
myRole === 'owner' &&
memberId === groupMembers.find((item) => item.role === 'owner')?.tmbId
) {
toast({ toast({
title: t('user:team.group.toast.can_not_delete_owner'), title: t('user:team.group.toast.can_not_delete_owner'),
status: 'error' status: 'error'
@@ -118,18 +135,18 @@ function GroupEditModal({
if ( if (
myRole === 'admin' && myRole === 'admin' &&
members.find((item) => String(item.tmbId) === memberId)?.role !== 'member' selected.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
) { ) {
return; return;
} }
if (isSelected(memberId)) { if (isSelected(memberId)) {
setMembers(members.filter((item) => item.tmbId !== memberId)); setSelected(selected.filter((item) => item.tmbId !== memberId));
} else { } else {
const member = allMembers.find((m) => m.tmbId === memberId); const member = allMembers.find((m) => m.tmbId === memberId);
if (!member) return; if (!member) return;
setMembers([ setSelected([
...members, ...selected,
{ {
name: member.memberName, name: member.memberName,
avatar: member.avatar, avatar: member.avatar,
@@ -142,14 +159,14 @@ function GroupEditModal({
const handleToggleAdmin = (memberId: string) => { const handleToggleAdmin = (memberId: string) => {
if (myRole === 'owner' && isSelected(memberId)) { if (myRole === 'owner' && isSelected(memberId)) {
const oldRole = members.find((item) => item.tmbId === memberId)?.role; const oldRole = groupMembers.find((item) => item.tmbId === memberId)?.groupRole;
if (oldRole === 'admin') { if (oldRole === 'admin') {
setMembers( setSelected(
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item)) selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
); );
} else { } else {
setMembers( setSelected(
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item)) selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
); );
} }
} }
@@ -184,37 +201,24 @@ function GroupEditModal({
}} }}
/> />
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}> <MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
{filtered.map((member) => { {allMembers.map((member) => {
return ( return (
<HStack <MemberItemCard
py="2" avatar={member.avatar}
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId} key={member.tmbId}
cursor={'pointer'} name={member.memberName}
_hover={{ onChange={() => handleToggleSelect(member.tmbId)}
bg: 'myGray.50', isChecked={!!isSelected(member.tmbId)}
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {}) orgs={member.orgs}
}} />
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
); );
})} })}
</MemberScrollData> </MemberScrollData>
</Flex> </Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}> <Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box> <Box mt={2}>{t('common:chosen') + ': ' + selected.length}</Box>
<MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}> <GroupScrollData mt={3} flex={'1 0 0'} h={0}>
{members.map((member) => { {selected.map((member) => {
return ( return (
<HStack <HStack
onMouseEnter={() => setHoveredMemberId(member.tmbId)} onMouseEnter={() => setHoveredMemberId(member.tmbId)}
@@ -284,7 +288,7 @@ function GroupEditModal({
</HStack> </HStack>
); );
})} })}
</MemberScrollData> </GroupScrollData>
</Flex> </Flex>
</Grid> </Grid>
</ModalBody> </ModalBody>

View File

@@ -15,12 +15,16 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { TeamContext } from '../context'; import { TeamContext } from '../context';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { GetSearchUserGroupOrg } from '@/web/support/user/api';
import { Omit } from '@fastgpt/web/components/common/DndDrag'; import { Omit } from '@fastgpt/web/components/common/DndDrag';
import { getTeamMembers } from '@/web/support/user/team/api';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import _ from 'lodash';
export function ChangeOwnerModal({ export function ChangeOwnerModal({
group, group,
@@ -33,22 +37,24 @@ export function ChangeOwnerModal({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [inputValue, setInputValue] = React.useState(''); const [searchKey, setSearchKey] = React.useState('');
const { data: searchedData } = useRequest2(
async () => { const {
if (!inputValue) return; data: members = [],
return GetSearchUserGroupOrg(inputValue); ScrollData: MemberScrollData,
}, refreshList
} = useScrollPagination<any, PaginationResponse<TeamMemberItemType<{ withGroupRole: true }>>>(
getTeamMembers,
{ {
manual: false, pageSize: 20,
refreshDeps: [inputValue], params: {
throttleWait: 500, searchKey: searchKey
debounceWait: 200 }
} }
); );
const { members: allMembers } = useContextSelector(TeamContext, (v) => v); const search = _.debounce(refreshList, 500);
const memberList = searchedData ? searchedData.members : allMembers; useEffect(() => search, [searchKey]);
const { const {
isOpen: isOpenMemberListMenu, isOpen: isOpenMemberListMenu,
@@ -108,9 +114,9 @@ export function ChangeOwnerModal({
)} )}
<Input <Input
placeholder={t('common:permission.change_owner_placeholder')} placeholder={t('common:permission.change_owner_placeholder')}
value={inputValue} value={searchKey}
onChange={(e) => { onChange={(e) => {
setInputValue(e.target.value); setSearchKey(e.target.value);
setSelectedMember(null); setSelectedMember(null);
}} }}
onFocus={() => { onFocus={() => {
@@ -120,7 +126,7 @@ export function ChangeOwnerModal({
{...(selectedMember && { pl: '10' })} {...(selectedMember && { pl: '10' })}
/> />
</Flex> </Flex>
{isOpenMemberListMenu && memberList.length > 0 && ( {isOpenMemberListMenu && members.length > 0 && (
<Flex <Flex
mt={2} mt={2}
w={'100%'} w={'100%'}
@@ -134,26 +140,28 @@ export function ChangeOwnerModal({
maxH={'300px'} maxH={'300px'}
overflow={'auto'} overflow={'auto'}
> >
{memberList.map((item) => ( <MemberScrollData>
<Box {members.map((item) => (
key={item.tmbId} <Box
p="2" key={item.tmbId}
_hover={{ bg: 'myGray.100' }} p="2"
mx="1" _hover={{ bg: 'myGray.100' }}
borderRadius="md" mx="1"
cursor={'pointer'} borderRadius="md"
onClickCapture={() => { cursor={'pointer'}
setInputValue(item.memberName); onClickCapture={() => {
setSelectedMember(item); setSearchKey(item.memberName);
onCloseMemberListMenu(); setSelectedMember(item);
}} onCloseMemberListMenu();
> }}
<Flex align="center"> >
<Avatar src={item.avatar} w="1.25rem" /> <Flex align="center">
<Box ml="2">{item.memberName}</Box> <Avatar src={item.avatar} w="1.25rem" />
</Flex> <Box ml="2">{item.memberName}</Box>
</Box> </Flex>
))} </Box>
))}
</MemberScrollData>
</Flex> </Flex>
)} )}

View File

@@ -41,12 +41,15 @@ import {
import format from 'date-fns/format'; import format from 'date-fns/format';
import OrgTags from '@/components/support/user/team/OrgTags'; import OrgTags from '@/components/support/user/team/OrgTags';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils'; import { downloadFetch } from '@/web/common/system/utils';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import _ from 'lodash';
import MySelect from '@fastgpt/web/components/common/MySelect';
const InviteModal = dynamic(() => import('./Invite/InviteModal')); const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@@ -55,11 +58,26 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const statusOptions = [
{
label: t('common:common.All'),
value: undefined
},
{
label: t('common:user.team.member.active'),
value: 'active'
},
{
label: t('account_team:leave'),
value: 'inactive'
}
];
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs?.register_method?.includes('sync'); const isSyncMember = feConfigs?.register_method?.includes('sync');
const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v); const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
const [status, setStatus] = useState<string>();
const { const {
isOpen: isOpenTeamTagsAsync, isOpen: isOpenTeamTagsAsync,
@@ -68,35 +86,36 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
} = useDisclosure(); } = useDisclosure();
// member action // member action
const [searchKey, setSearchKey] = useState<string>('');
const { const {
data: members = [], data: members = [],
isLoading: loadingMembers, isLoading: loadingMembers,
refreshList: refetchMemberList, refreshList: refetchMemberList,
ScrollData: MemberScrollData ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, { } = useScrollPagination<
any,
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
>(getTeamMembers, {
pageSize: 20, pageSize: 20,
params: { params: {
withLeaved: true status,
withPermission: true,
withOrgs: true,
searchKey
} }
}); });
const [searchText, setSearchText] = useState<string>(''); const refreshList = _.debounce(() => {
const { data: searchMembersData, run: refreshSearchMembers } = useRequest2( refetchMemberList();
async () => { }, 200);
if (!searchText) return Promise.resolve();
return GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false }); useEffect(() => {
}, refreshList();
{ }, [searchKey, status]);
manual: false,
throttleWait: 500,
refreshDeps: [searchText]
}
);
const onRefreshMembers = useCallback(() => { const onRefreshMembers = useCallback(() => {
refetchMemberList(); refetchMemberList();
refreshSearchMembers(); }, [refetchMemberList]);
}, [refetchMemberList, refreshSearchMembers]);
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure(); const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
@@ -166,10 +185,13 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Flex justify={'space-between'} align={'center'} pb={'1rem'}> <Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs} {Tabs}
<HStack alignItems={'center'}> <HStack alignItems={'center'}>
<Box>
<MySelect list={statusOptions} value={status} onChange={(v) => setStatus(v)} />
</Box>
<Box width={'200px'}> <Box width={'200px'}>
<SearchInput <SearchInput
placeholder={t('account_team:search_member')} placeholder={t('account_team:search_member')}
onChange={(e) => setSearchText(e.target.value)} onChange={(e) => setSearchKey(e.target.value)}
/> />
</Box> </Box>
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && ( {userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
@@ -264,107 +286,104 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{(searchText && searchMembersData ? searchMembersData.members : members).map( {members.map((member) => (
(member) => ( <Tr key={member.tmbId} overflow={'unset'}>
<Tr key={member.tmbId} overflow={'unset'}> <Td>
<Td> <HStack>
<HStack> <Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} /> <Box className={'textEllipsis'}>
<Box className={'textEllipsis'}> {member.memberName}
{member.memberName} {member.status !== 'active' && (
{member.status !== 'active' && ( <Tag ml="2" colorSchema="gray" bg={'myGray.100'} color={'myGray.700'}>
<Tag ml="2" colorSchema="gray" bg={'myGray.100'} color={'myGray.700'}> {t('account_team:leave')}
{t('account_team:leave')} </Tag>
</Tag> )}
)} </Box>
</Box> </HStack>
</HStack> </Td>
</Td> <Td maxW={'300px'}>{member.contact || '-'}</Td>
<Td maxW={'300px'}>{member.contact || '-'}</Td> <Td maxWidth="300px">
<Td maxWidth="300px"> {(() => {
{(() => { return <OrgTags orgs={member.orgs || undefined} type="tag" />;
return <OrgTags orgs={member.orgs || undefined} type="tag" />; })()}
})()} </Td>
</Td> <Td maxW={'300px'}>
<Td maxW={'300px'}> <VStack gap={0}>
<VStack gap={0} alignItems="flex-start"> <Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box> <Box>
<Box> {member.updateTime
{member.updateTime ? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss')
? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss') : '-'}
: '-'} </Box>
</Box> </VStack>
</VStack> </Td>
</Td> <Td>
<Td> {userInfo?.team.permission.hasManagePer &&
{userInfo?.team.permission.hasManagePer && member.role !== TeamMemberRoleEnum.owner &&
member.role !== TeamMemberRoleEnum.owner && member.tmbId !== userInfo?.team.tmbId &&
member.tmbId !== userInfo?.team.tmbId && (member.status === TeamMemberStatusEnum.active ? (
(member.status === TeamMemberStatusEnum.active ? ( <>
<> {' '}
<Icon <Icon
name={'edit'} name={'edit'}
cursor={'pointer'} cursor={'pointer'}
w="1rem" w="1rem"
p="1" p="1"
borderRadius="sm" borderRadius="sm"
_hover={{ _hover={{
color: 'blue.600', color: 'blue.600',
bgColor: 'myGray.100' bgColor: 'myGray.100'
}} }}
onClick={() => onClick={() => handleEditMemberName(member.tmbId, member.memberName)}
handleEditMemberName(member.tmbId, member.memberName) />
} <Icon
/> name={'common/trash'}
<Icon cursor={'pointer'}
name={'common/trash'} w="1rem"
cursor={'pointer'} p="1"
w="1rem" borderRadius="sm"
p="1" _hover={{
borderRadius="sm" color: 'red.600',
_hover={{ bgColor: 'myGray.100'
color: 'red.600', }}
bgColor: 'myGray.100' onClick={() => {
}} openRemoveMember(
onClick={() => { () => onRemoveMember(member.tmbId),
openRemoveMember( undefined,
() => onRemoveMember(member.tmbId), t('account_team:remove_tip', {
undefined, username: member.memberName
t('account_team:remove_tip', { })
username: member.memberName )();
}) }}
)(); />
}} </>
/> ) : (
</> member.status === TeamMemberStatusEnum.forbidden && (
) : ( <Icon
member.status === TeamMemberStatusEnum.forbidden && ( name={'common/confirm/restoreTip'}
<Icon cursor={'pointer'}
name={'common/confirm/restoreTip'} w="1rem"
cursor={'pointer'} p="1"
w="1rem" borderRadius="sm"
p="1" _hover={{
borderRadius="sm" color: 'primary.500',
_hover={{ bgColor: 'myGray.100'
color: 'primary.500', }}
bgColor: 'myGray.100' onClick={() => {
}} openRestoreMember(
onClick={() => { () => onRestore(member.tmbId),
openRestoreMember( undefined,
() => onRestore(member.tmbId), t('account_team:restore_tip', {
undefined, username: member.memberName
t('account_team:restore_tip', { })
username: member.memberName )();
}) }}
)(); />
}} )
/> ))}
) </Td>
))} </Tr>
</Td> ))}
</Tr>
)
)}
</Tbody> </Tbody>
</Table> </Table>
<ConfirmRemoveMemberModal /> <ConfirmRemoveMemberModal />

View File

@@ -29,12 +29,14 @@ function OrgInfoModal({
editOrg, editOrg,
onClose, onClose,
onSuccess, onSuccess,
updateCurrentOrg updateCurrentOrg,
parentId
}: { }: {
editOrg: OrgFormType; editOrg: OrgFormType;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void; updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void;
parentId?: string;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -51,10 +53,11 @@ function OrgInfoModal({
const { run: onCreate, loading: isLoadingCreate } = useRequest2( const { run: onCreate, loading: isLoadingCreate } = useRequest2(
async (data: OrgFormType) => { async (data: OrgFormType) => {
if (parentId === undefined) return;
return postCreateOrg({ return postCreateOrg({
name: data.name, name: data.name,
avatar: data.avatar, avatar: data.avatar,
path: editOrg.path, orgId: parentId,
description: data.description description: data.description
}); });
}, },

View File

@@ -1,14 +1,5 @@
import { getOrgMembers, putUpdateOrgMembers } from '@/web/support/user/team/org/api'; import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
import { import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant'; import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -17,11 +8,11 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal'; import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { OrgListItemType } 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 { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { getTeamMembers } from '@/web/support/user/team/api'; import { getTeamMembers } from '@/web/support/user/team/api';
import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
export type GroupFormType = { export type GroupFormType = {
members: { members: {
@@ -30,16 +21,6 @@ export type GroupFormType = {
}[]; }[];
}; };
function CheckboxIcon({
name
}: {
isChecked?: boolean;
isIndeterminate?: boolean;
name: IconNameType;
}) {
return <MyIcon name={name} w="12px" />;
}
function OrgMemberManageModal({ function OrgMemberManageModal({
currentOrg, currentOrg,
refetchOrgs, refetchOrgs,
@@ -56,17 +37,24 @@ function OrgMemberManageModal({
ScrollData: MemberScrollData, ScrollData: MemberScrollData,
isLoading: isLoadingMembers isLoading: isLoadingMembers
} = useScrollPagination(getTeamMembers, { } = useScrollPagination(getTeamMembers, {
pageSize: 20 pageSize: 20,
params: {
withOrgs: true,
withPermission: false,
status: 'active'
}
}); });
const { const {
data: orgMembers, data: orgMembers,
ScrollData: OrgMemberScrollData, ScrollData: OrgMemberScrollData,
isLoading: isLoadingOrgMembers isLoading: isLoadingOrgMembers
} = useScrollPagination(getOrgMembers, { } = useScrollPagination(getTeamMembers, {
pageSize: 20, pageSize: 100000,
params: { params: {
orgPath: getOrgChildrenPath(currentOrg) orgId: currentOrg._id,
withOrgs: false,
withPermission: false
} }
}); });
@@ -83,11 +71,6 @@ function OrgMemberManageModal({
}, [orgMembers]); }, [orgMembers]);
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const filterMembers = useMemo(() => {
if (!searchKey) return allMembers;
const regx = new RegExp(searchKey, 'i');
return allMembers.filter((member) => regx.test(member.memberName));
}, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2( const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
() => { () => {
@@ -165,35 +148,20 @@ function OrgMemberManageModal({
}} }}
/> />
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'} isLoading={isLoadingMembers}> <MemberScrollData mt={3} flexGrow="1" overflow={'auto'} isLoading={isLoadingMembers}>
{filterMembers.map((member) => { {allMembers.map((member) => {
return ( return (
<HStack <MemberItemCard
py="2" avatar={member.avatar}
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId} key={member.tmbId}
cursor={'pointer'} name={member.memberName}
_hover={{ onChange={() => handleToggleSelect(member.tmbId)}
bg: 'myGray.50', isChecked={!!isSelected(member.tmbId)}
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {}) orgs={member.orgs}
}} />
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
); );
})} })}
</MemberScrollData> </MemberScrollData>
</Flex> </Flex>
{/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */}
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden"> <Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
<OrgMemberScrollData <OrgMemberScrollData
mt={3} mt={3}

View File

@@ -20,8 +20,6 @@ function OrgMoveModal({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedOrg, setSelectedOrg] = useState<OrgListItemType>(); const [selectedOrg, setSelectedOrg] = useState<OrgListItemType>();
const { userInfo } = useUserStore();
const team = userInfo?.team!;
const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, { const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, {
onSuccess: () => { onSuccess: () => {
@@ -39,7 +37,7 @@ function OrgMoveModal({
iconColor="primary.600" iconColor="primary.600"
> >
<ModalBody> <ModalBody>
<OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} /> <OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} movingOrg={movingOrg} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button

View File

@@ -14,17 +14,19 @@ function OrgTreeNode({
org, org,
selectedOrg, selectedOrg,
setSelectedOrg, setSelectedOrg,
index = 0 index = 0,
movingOrg
}: { }: {
org: OrgListItemType; org: OrgListItemType;
selectedOrg?: OrgListItemType; selectedOrg?: OrgListItemType;
setSelectedOrg: (org?: OrgListItemType) => void; setSelectedOrg: (org?: OrgListItemType) => void;
index?: number; index?: number;
movingOrg: OrgListItemType;
}) { }) {
const [isExpanded, toggleIsExpanded] = useToggle(index === 0); const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
const [canBeExpanded, setCanBeExpanded] = useState(true); const [canBeExpanded, setCanBeExpanded] = useState(true);
const { data: orgs = [], runAsync: getOrgs } = useRequest2(() => const { data: orgs = [], runAsync: getOrgs } = useRequest2(() =>
getOrgList({ orgPath: getOrgChildrenPath(org) }) getOrgList({ orgId: org._id, withPermission: false })
); );
const onClickExpand = async () => { const onClickExpand = async () => {
const data = await getOrgs(); const data = await getOrgs();
@@ -34,6 +36,9 @@ function OrgTreeNode({
toggleIsExpanded.toggle(); toggleIsExpanded.toggle();
}; };
if (org._id === movingOrg._id) {
return <></>;
}
return ( return (
<Box userSelect={'none'}> <Box userSelect={'none'}>
<HStack <HStack
@@ -78,6 +83,7 @@ function OrgTreeNode({
orgs.map((child) => ( orgs.map((child) => (
<Box key={child._id} mt={0.5}> <Box key={child._id} mt={0.5}>
<OrgTreeNode <OrgTreeNode
movingOrg={movingOrg}
org={child} org={child}
index={index + 1} index={index + 1}
selectedOrg={selectedOrg} selectedOrg={selectedOrg}
@@ -91,10 +97,12 @@ function OrgTreeNode({
function OrgTree({ function OrgTree({
selectedOrg, selectedOrg,
setSelectedOrg setSelectedOrg,
movingOrg
}: { }: {
selectedOrg?: OrgListItemType; selectedOrg?: OrgListItemType;
setSelectedOrg: (org?: OrgListItemType) => void; setSelectedOrg: (org?: OrgListItemType) => void;
movingOrg: OrgListItemType;
}) { }) {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const root: OrgListItemType = { const root: OrgListItemType = {
@@ -107,6 +115,7 @@ function OrgTree({
return ( return (
<OrgTreeNode <OrgTreeNode
movingOrg={movingOrg}
key={'root'} key={'root'}
org={root} org={root}
selectedOrg={selectedOrg} selectedOrg={selectedOrg}

View File

@@ -78,9 +78,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const [editOrg, setEditOrg] = useState<OrgFormType>(); const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>(); const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>();
const [movingOrg, setMovingOrg] = useState<OrgListItemType>(); const [movingOrg, setMovingOrg] = useState<OrgListItemType>();
const [searchOrg, setSearchOrg] = useState('');
const { const {
currentOrg, currentOrg,
orgs, orgs,
@@ -91,7 +88,9 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
MemberScrollData, MemberScrollData,
onPathClick, onPathClick,
refresh, refresh,
updateCurrentOrg updateCurrentOrg,
setSearchKey,
searchKey
} = useOrg(); } = useOrg();
// Delete org // Delete org
@@ -123,12 +122,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onSuccess: refresh onSuccess: refresh
}); });
const searchedOrgs = useMemo(() => {
if (!searchOrg) return [];
return orgs.filter((org) => org.name.includes(searchOrg));
}, [orgs, searchOrg]);
return ( return (
<> <>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}> <Flex justify={'space-between'} align={'center'} pb={'1rem'}>
@@ -136,8 +129,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
<Box w="200px"> <Box w="200px">
<SearchInput <SearchInput
placeholder={t('account_team:search_org')} placeholder={t('account_team:search_org')}
value={searchOrg} value={searchKey}
onChange={(e) => setSearchOrg(e.target.value)} onChange={(e) => setSearchKey(e.target.value)}
/> />
</Box> </Box>
</Flex> </Flex>
@@ -166,102 +159,55 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{searchedOrgs.map((org) => ( {orgs
<Tr key={org._id} overflow={'unset'} onClick={() => onClickOrg(org)}> .filter((org) => org.path !== '')
<Td> .map((org) => (
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}> <Tr key={org._id} overflow={'unset'}>
<MemberTag name={org.name} avatar={org.avatar!} /> <Td>
<Tag size="sm">{org.total}</Tag> <HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
<MyIcon <MemberTag name={org.name} avatar={org.avatar} />
name="core/chat/chevronRight" <Tag size="sm">{org.total}</Tag>
w={'1rem'} <MyIcon
h={'1rem'} name="core/chat/chevronRight"
color={'myGray.500'} w={'1rem'}
/> h={'1rem'}
</HStack> color={'myGray.500'}
</Td> />
{isTeamAdmin && !isSyncMember && ( </HStack>
<Td w={'6rem'}>
<MyMenu
trigger="hover"
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td> </Td>
)} {isTeamAdmin && !isSyncMember && (
</Tr> <Td w={'6rem'}>
))} <MyMenu
{!searchOrg && trigger="hover"
orgs Button={<IconButton name="more" />}
.filter((org) => org.path !== '') menuList={[
.map((org) => ( {
<Tr key={org._id} overflow={'unset'}> children: [
<Td> {
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}> icon: 'edit',
<MemberTag name={org.name} avatar={org.avatar} /> label: t('account_team:edit_info'),
<Tag size="sm">{org.total}</Tag> onClick: () => setEditOrg(org)
<MyIcon },
name="core/chat/chevronRight" {
w={'1rem'} icon: 'common/file/move',
h={'1rem'} label: t('common:Move'),
color={'myGray.500'} onClick: () => setMovingOrg(org)
/> },
</HStack> {
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td> </Td>
{isTeamAdmin && !isSyncMember && ( )}
<Td w={'6rem'}> </Tr>
<MyMenu ))}
trigger="hover" {!searchKey &&
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'edit',
label: t('account_team:edit_info'),
onClick: () => setEditOrg(org)
},
{
icon: 'common/file/move',
label: t('common:Move'),
onClick: () => setMovingOrg(org)
},
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
</Td>
)}
</Tr>
))}
{!searchOrg &&
members.map((member) => { members.map((member) => {
return ( return (
<Tr key={member.tmbId}> <Tr key={member.tmbId}>
@@ -403,6 +349,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
onClose={() => setEditOrg(undefined)} onClose={() => setEditOrg(undefined)}
onSuccess={refresh} onSuccess={refresh}
updateCurrentOrg={updateCurrentOrg} updateCurrentOrg={updateCurrentOrg}
parentId={currentOrg._id}
/> />
)} )}
{!!movingOrg && ( {!!movingOrg && (

View File

@@ -20,20 +20,17 @@ const EditInfoModal = dynamic(() => import('./EditInfoModal'));
type TeamModalContextType = { type TeamModalContextType = {
myTeams: TeamTmbItemType[]; myTeams: TeamTmbItemType[];
members: TeamMemberItemType[];
isLoading: boolean; isLoading: boolean;
onSwitchTeam: (teamId: string) => void; onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>; setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
refetchMembers: () => void; refetchTeamSize: () => void;
refetchTeams: () => void; refetchTeams: () => void;
teamSize: number; teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
}; };
export const TeamContext = createContext<TeamModalContextType>({ export const TeamContext = createContext<TeamModalContextType>({
myTeams: [], myTeams: [],
members: [],
isLoading: false, isLoading: false,
onSwitchTeam: function (_teamId: string): void { onSwitchTeam: function (_teamId: string): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
@@ -44,11 +41,10 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchTeams: function (): void { refetchTeams: function (): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
refetchMembers: function (): void { refetchTeamSize: function (): void {
throw new Error('Function not implemented.'); throw new Error('Function not implemented.');
}, },
teamSize: 0, teamSize: 0
MemberScrollData: () => <></>
}); });
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => { export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
@@ -67,32 +63,11 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshDeps: [userInfo?._id] refreshDeps: [userInfo?._id]
}); });
const { data: teamMemberCountData, refresh: refetchTeamMemberCount } = useRequest2( const { data: teamMemberCountData, refresh: refetchTeamSize } = useRequest2(getTeamMemberCount, {
getTeamMemberCount, manual: false,
{ refreshDeps: [userInfo?.team?.teamId]
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
// member action
const {
data: members = [],
isLoading: loadingMembers,
refreshList: refetchMemberList,
ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, {
pageSize: 20,
params: {
withLeaved: true
}
}); });
const refetchMembers = useCallback(() => {
refetchTeamMemberCount();
refetchMemberList();
}, [refetchTeamMemberCount, refetchMemberList]);
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2( const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => { async (teamId: string) => {
await putSwitchTeam(teamId); await putSwitchTeam(teamId);
@@ -106,7 +81,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
} }
); );
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers; const isLoading = isLoadingTeams || isSwitchingTeam;
const contextValue = { const contextValue = {
myTeams, myTeams,
@@ -116,10 +91,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
// create | update team // create | update team
setEditTeamData, setEditTeamData,
members,
refetchMembers,
teamSize: teamMemberCountData?.count || 0, teamSize: teamMemberCountData?.count || 0,
MemberScrollData refetchTeamSize
}; };
return ( return (

View File

@@ -34,8 +34,16 @@ export const putSwitchTeam = (teamId: string) =>
PUT<string>(`/proApi/support/user/team/switch`, { teamId }); PUT<string>(`/proApi/support/user/team/switch`, { teamId });
/* --------------- team member ---------------- */ /* --------------- team member ---------------- */
export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) => export const getTeamMembers = (
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props); props: PaginationProps<{
status?: 'active' | 'inactive';
withOrgs?: boolean;
withPermission?: boolean;
searchKey?: string;
orgId?: string;
groupId?: string;
}>
) => POST<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
export const getTeamMemberCount = () => export const getTeamMemberCount = () =>
GET<{ count: number }>(`/proApi/support/user/team/member/count`); GET<{ count: number }>(`/proApi/support/user/team/member/count`);

View File

@@ -10,8 +10,11 @@ import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/t
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
export const getOrgList = (params: { orgPath: string; getPermission?: boolean }) => export const getOrgList = (params: {
GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params); orgId: string;
withPermission?: boolean;
searchKey?: string;
}) => POST<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
export const postCreateOrg = (data: postCreateOrgData) => export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data); POST('/proApi/support/user/team/org/create', data);

View File

@@ -1,14 +1,17 @@
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant'; import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type';
import { memo, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { useUserStore } from '../../../useUserStore'; import { useUserStore } from '../../../useUserStore';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getOrgList, getOrgMembers } from '../api'; import { getOrgList, getOrgMembers } from '../api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '../../api';
import _ from 'lodash';
function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) { function useOrg({ withPermission = true }: { withPermission?: boolean } = {}) {
const [orgStack, setOrgStack] = useState<OrgListItemType[]>([]); const [orgStack, setOrgStack] = useState<OrgListItemType[]>([]);
const [searchKey, setSearchKey] = useState('');
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
@@ -34,10 +37,18 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
data: orgs = [], data: orgs = [],
loading: isLoadingOrgs, loading: isLoadingOrgs,
refresh: refetchOrgs refresh: refetchOrgs
} = useRequest2(() => getOrgList({ orgPath: path, getPermission }), { } = useRequest2(
manual: false, () => getOrgList({ orgId: currentOrg._id, withPermission: withPermission, searchKey }),
refreshDeps: [userInfo?.team?.teamId, path] {
}); manual: false,
refreshDeps: [userInfo?.team?.teamId, path, currentOrg._id]
}
);
const search = _.debounce(() => {
if (!searchKey) return;
refetchOrgs();
}, 200);
useEffect(() => search, [searchKey]);
const paths = useMemo(() => { const paths = useMemo(() => {
if (!currentOrg) return []; if (!currentOrg) return [];
@@ -53,16 +64,20 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
const onClickOrg = (org: OrgListItemType) => { const onClickOrg = (org: OrgListItemType) => {
setOrgStack([...orgStack, org]); setOrgStack([...orgStack, org]);
setSearchKey('');
}; };
const { const {
data: members = [], data: members = [],
ScrollData: MemberScrollData, ScrollData: MemberScrollData,
refreshList: refetchMembers refreshList: refetchMembers
} = useScrollPagination(getOrgMembers, { } = useScrollPagination(getTeamMembers, {
pageSize: 20, pageSize: 20,
params: { params: {
orgPath: path orgId: currentOrg._id,
withOrgs: false,
withPermission: true,
status: 'active'
}, },
refreshDeps: [path] refreshDeps: [path]
}); });
@@ -70,6 +85,7 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
const onPathClick = (path: string) => { const onPathClick = (path: string) => {
const pathIds = path.split('/'); const pathIds = path.split('/');
setOrgStack(orgStack.filter((org) => pathIds.includes(org.pathId))); setOrgStack(orgStack.filter((org) => pathIds.includes(org.pathId)));
setSearchKey('');
}; };
const refresh = () => { const refresh = () => {
@@ -101,7 +117,9 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
MemberScrollData, MemberScrollData,
onPathClick, onPathClick,
refresh, refresh,
updateCurrentOrg updateCurrentOrg,
searchKey,
setSearchKey
}; };
} }