mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 04:06:18 +00:00
pref: member list (#4344)
* chore: search member new api * chore: permission * fix: ts error * fix: member modal
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
// orgId, pathid, path === null ===> root org
|
||||
export type postCreateOrgData = {
|
||||
name: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
path?: string;
|
||||
orgId?: string;
|
||||
};
|
||||
|
||||
export type putUpdateOrgMembersData = {
|
||||
|
26
packages/global/support/user/team/type.d.ts
vendored
26
packages/global/support/user/team/type.d.ts
vendored
@@ -70,7 +70,13 @@ export type TeamTmbItemType = {
|
||||
permission: TeamPermission;
|
||||
} & ThirdPartyAccountType;
|
||||
|
||||
export type TeamMemberItemType = {
|
||||
export type TeamMemberItemType<
|
||||
Options extends {
|
||||
withPermission?: boolean;
|
||||
withOrgs?: boolean;
|
||||
withGroupRole?: boolean;
|
||||
} = { withPermission: true; withOrgs: true; withGroupRole: false }
|
||||
> = {
|
||||
userId: string;
|
||||
tmbId: string;
|
||||
teamId: string;
|
||||
@@ -78,12 +84,24 @@ export type TeamMemberItemType = {
|
||||
avatar: string;
|
||||
role: `${TeamMemberRoleEnum}`;
|
||||
status: `${TeamMemberStatusEnum}`;
|
||||
permission: TeamPermission;
|
||||
contact?: string;
|
||||
createTime: 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 = {
|
||||
label: string;
|
||||
|
@@ -19,7 +19,7 @@ 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 { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PermissionSelect from './PermissionSelect';
|
||||
import PermissionTags from './PermissionTags';
|
||||
import {
|
||||
@@ -39,6 +39,7 @@ import { GetSearchUserGroupOrg } from '@/web/support/user/api';
|
||||
import useOrg from '@/web/support/user/team/org/hooks/useOrg';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import _ from 'lodash';
|
||||
|
||||
const HoverBoxStyle = {
|
||||
bgColor: 'myGray.50',
|
||||
@@ -55,7 +56,6 @@ function MemberModal({
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
|
||||
const {
|
||||
paths,
|
||||
@@ -63,18 +63,35 @@ function MemberModal({
|
||||
members: orgMembers,
|
||||
MemberScrollData: OrgMemberScrollData,
|
||||
onPathClick,
|
||||
orgs
|
||||
} = useOrg({ getPermission: false });
|
||||
orgs,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
} = useOrg({ withPermission: false });
|
||||
|
||||
const { data: members, ScrollData: TeamMemberScrollData } = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 15
|
||||
const {
|
||||
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 () => {
|
||||
if (!userInfo?.team?.teamId) return [];
|
||||
return getGroupList<false>({
|
||||
withMembers: false
|
||||
withMembers: false,
|
||||
searchKey
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -83,53 +100,20 @@ function MemberModal({
|
||||
}
|
||||
);
|
||||
|
||||
const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), {
|
||||
manual: false,
|
||||
throttleWait: 500,
|
||||
debounceWait: 200,
|
||||
refreshDeps: [searchText]
|
||||
});
|
||||
const search = _.debounce(() => {
|
||||
refreshList();
|
||||
refreshGroups();
|
||||
}, 200);
|
||||
|
||||
useEffect(search, [searchKey]);
|
||||
|
||||
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<
|
||||
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 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 getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
|
||||
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
|
||||
@@ -225,12 +209,12 @@ function MemberModal({
|
||||
<SearchInput
|
||||
placeholder={t('user:search_group_org_user')}
|
||||
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}>
|
||||
{/* Entry */}
|
||||
{!searchText && !filterClass && (
|
||||
{!searchKey && !filterClass && (
|
||||
<>
|
||||
{entryList.current.map((item) => {
|
||||
return (
|
||||
@@ -257,7 +241,7 @@ function MemberModal({
|
||||
)}
|
||||
|
||||
{/* Path */}
|
||||
{!searchText && filterClass && (
|
||||
{!searchKey && filterClass && (
|
||||
<Box mb={1}>
|
||||
<Path
|
||||
paths={[
|
||||
@@ -291,9 +275,9 @@ function MemberModal({
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{(filterClass === 'member' || (searchText && filterMembers.length > 0)) &&
|
||||
{(filterClass === 'member' || searchKey) &&
|
||||
(() => {
|
||||
const members = filterMembers?.map((member) => {
|
||||
const Members = members?.map((member) => {
|
||||
const onChange = () => {
|
||||
setSelectedMemberList((state) => {
|
||||
if (state.find((v) => v.tmbId === member.tmbId)) {
|
||||
@@ -315,8 +299,8 @@ function MemberModal({
|
||||
/>
|
||||
);
|
||||
});
|
||||
return searchText ? (
|
||||
members
|
||||
return searchKey ? (
|
||||
Members
|
||||
) : (
|
||||
<TeamMemberScrollData
|
||||
flexDirection={'column'}
|
||||
@@ -324,13 +308,13 @@ function MemberModal({
|
||||
userSelect={'none'}
|
||||
height={'fit-content'}
|
||||
>
|
||||
{members}
|
||||
{Members}
|
||||
</TeamMemberScrollData>
|
||||
);
|
||||
})()}
|
||||
{(filterClass === 'org' || searchText) &&
|
||||
{(filterClass === 'org' || searchKey) &&
|
||||
(() => {
|
||||
const orgs = filterOrgs?.map((org) => {
|
||||
const Orgs = orgs?.map((org) => {
|
||||
const onChange = () => {
|
||||
setSelectedOrgIdList((state) => {
|
||||
if (state.find((v) => v._id === org._id)) {
|
||||
@@ -356,18 +340,18 @@ function MemberModal({
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
|
||||
<HStack ml="2" w="full" gap="5px">
|
||||
<HStack w="full">
|
||||
<Text>{org.name}</Text>
|
||||
{org.count && (
|
||||
{org.total && (
|
||||
<>
|
||||
<Tag size="sm" my="auto">
|
||||
{org.count}
|
||||
{org.total}
|
||||
</Tag>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
<PermissionTags permission={collaborator?.permission.value} />
|
||||
{org.count && (
|
||||
{org.total && (
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w="16px"
|
||||
@@ -386,11 +370,11 @@ function MemberModal({
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
return searchText ? (
|
||||
orgs
|
||||
return searchKey ? (
|
||||
Orgs
|
||||
) : (
|
||||
<OrgMemberScrollData>
|
||||
{orgs}
|
||||
{Orgs}
|
||||
{orgMembers.map((member) => {
|
||||
return (
|
||||
<MemberItemCard
|
||||
@@ -413,29 +397,30 @@ function MemberModal({
|
||||
</OrgMemberScrollData>
|
||||
);
|
||||
})()}
|
||||
{filterGroups?.map((group) => {
|
||||
const onChange = () => {
|
||||
setSelectedGroupList((state) => {
|
||||
if (state.find((v) => v._id === group._id)) {
|
||||
return state.filter((v) => v._id !== group._id);
|
||||
}
|
||||
return [...state, group];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={group.avatar}
|
||||
key={group._id}
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{(filterClass === 'group' || searchKey) &&
|
||||
groups?.map((group) => {
|
||||
const onChange = () => {
|
||||
setSelectedGroupList((state) => {
|
||||
if (state.find((v) => v._id === group._id)) {
|
||||
return state.filter((v) => v._id !== group._id);
|
||||
}
|
||||
return [...state, group];
|
||||
});
|
||||
};
|
||||
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
|
||||
return (
|
||||
<MemberItemCard
|
||||
avatar={group.avatar}
|
||||
key={group._id}
|
||||
name={
|
||||
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
|
||||
}
|
||||
permission={collaborator?.permission.value}
|
||||
onChange={onChange}
|
||||
isChecked={!!selectedGroupList.find((v) => v._id === group._id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
@@ -1,34 +1,25 @@
|
||||
import {
|
||||
Box,
|
||||
ModalBody,
|
||||
Flex,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Checkbox,
|
||||
Grid,
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, ModalBody, Flex, Button, ModalFooter, Grid, HStack } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
|
||||
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 { useContextSelector } from 'use-context-selector';
|
||||
import { TeamContext } from '../context';
|
||||
import { getGroupMembers, putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { putUpdateGroup } from '@/web/support/user/team/group/api';
|
||||
import { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
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,
|
||||
MemberGroupListItemType
|
||||
} from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { useMount } from 'ahooks';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { getTeamMembers } from '@/web/support/user/team/api';
|
||||
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import _ from 'lodash';
|
||||
import MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
|
||||
|
||||
export type GroupFormType = {
|
||||
members: {
|
||||
@@ -53,42 +44,65 @@ function GroupEditModal({
|
||||
const { userInfo } = useUserStore();
|
||||
const { toast } = useToast();
|
||||
|
||||
const allMembers = useContextSelector(TeamContext, (v) => v.members);
|
||||
const MemberScrollData = useContextSelector(TeamContext, (v) => v.MemberScrollData);
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selected, setSelected] = useState<
|
||||
{ name: string; tmbId: string; avatar: string; role: `${GroupMemberRole}` }[]
|
||||
>([]);
|
||||
|
||||
const selectedMembersRef = useRef<HTMLDivElement>(null);
|
||||
const [members, setMembers] = useState<GroupMemberItemType[]>([]);
|
||||
const editGroupId = useMemo(() => {
|
||||
return group?._id;
|
||||
}, [group]);
|
||||
const {
|
||||
data: allMembers = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList
|
||||
} = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
status: 'active',
|
||||
withOrgs: true,
|
||||
searchKey
|
||||
}
|
||||
});
|
||||
const refetchMemberList = _.debounce(refreshList, 200);
|
||||
|
||||
useMount(async () => {
|
||||
console.log('aaaaaa');
|
||||
if (editGroupId) {
|
||||
const data = await getGroupMembers(editGroupId);
|
||||
console.log(data);
|
||||
setMembers(data);
|
||||
useEffect(() => refetchMemberList, [searchKey]);
|
||||
|
||||
const groupId = useMemo(() => String(group._id), [group._id]);
|
||||
|
||||
const { data: groupMembers = [], ScrollData: GroupScrollData } = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<
|
||||
TeamMemberItemType<{ withOrgs: true; withPermission: true; withGroupRole: true }>
|
||||
>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 100000,
|
||||
params: {
|
||||
groupId: groupId
|
||||
}
|
||||
});
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const filtered = useMemo(() => {
|
||||
return [
|
||||
...allMembers.filter((member) => {
|
||||
if (member.memberName.toLowerCase().includes(searchKey.toLowerCase())) return true;
|
||||
return false;
|
||||
})
|
||||
];
|
||||
}, [searchKey, allMembers]);
|
||||
useEffect(() => {
|
||||
if (!groupId) return;
|
||||
setSelected(
|
||||
groupMembers.map((item) => ({
|
||||
name: item.memberName,
|
||||
tmbId: item.tmbId,
|
||||
avatar: item.avatar,
|
||||
role: (item.groupRole ?? 'member') as `${GroupMemberRole}`
|
||||
}))
|
||||
);
|
||||
}, [groupId, groupMembers]);
|
||||
|
||||
const [hoveredMemberId, setHoveredMemberId] = useState<string>();
|
||||
|
||||
const { runAsync: onUpdate, loading: isLoadingUpdate } = useRequest2(
|
||||
async () => {
|
||||
if (!editGroupId || !members.length) return;
|
||||
if (!group._id || !groupMembers.length) return;
|
||||
|
||||
return putUpdateGroup({
|
||||
groupId: editGroupId,
|
||||
memberList: members
|
||||
groupId: group._id,
|
||||
memberList: selected
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -97,18 +111,21 @@ function GroupEditModal({
|
||||
);
|
||||
|
||||
const isSelected = (memberId: string) => {
|
||||
return members.find((item) => item.tmbId === memberId);
|
||||
return selected.find((item) => item.tmbId === memberId);
|
||||
};
|
||||
|
||||
const myRole = useMemo(() => {
|
||||
if (userInfo?.team.permission.hasManagePer) {
|
||||
return 'owner';
|
||||
}
|
||||
return members.find((item) => item.tmbId === userInfo?.team.tmbId)?.role ?? 'member';
|
||||
}, [members, userInfo]);
|
||||
return groupMembers.find((item) => item.tmbId === userInfo?.team.tmbId)?.groupRole ?? 'member';
|
||||
}, [groupMembers, userInfo]);
|
||||
|
||||
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({
|
||||
title: t('user:team.group.toast.can_not_delete_owner'),
|
||||
status: 'error'
|
||||
@@ -118,18 +135,18 @@ function GroupEditModal({
|
||||
|
||||
if (
|
||||
myRole === 'admin' &&
|
||||
members.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
selected.find((item) => String(item.tmbId) === memberId)?.role !== 'member'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected(memberId)) {
|
||||
setMembers(members.filter((item) => item.tmbId !== memberId));
|
||||
setSelected(selected.filter((item) => item.tmbId !== memberId));
|
||||
} else {
|
||||
const member = allMembers.find((m) => m.tmbId === memberId);
|
||||
if (!member) return;
|
||||
setMembers([
|
||||
...members,
|
||||
setSelected([
|
||||
...selected,
|
||||
{
|
||||
name: member.memberName,
|
||||
avatar: member.avatar,
|
||||
@@ -142,14 +159,14 @@ function GroupEditModal({
|
||||
|
||||
const handleToggleAdmin = (memberId: string) => {
|
||||
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') {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
setSelected(
|
||||
selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'member' } : item))
|
||||
);
|
||||
} else {
|
||||
setMembers(
|
||||
members.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
setSelected(
|
||||
selected.map((item) => (item.tmbId === memberId ? { ...item, role: 'admin' } : item))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -184,37 +201,24 @@ function GroupEditModal({
|
||||
}}
|
||||
/>
|
||||
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
|
||||
{filtered.map((member) => {
|
||||
{allMembers.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_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>
|
||||
name={member.memberName}
|
||||
onChange={() => handleToggleSelect(member.tmbId)}
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + members.length}</Box>
|
||||
<MemberScrollData ScrollContainerRef={selectedMembersRef} mt={3} flex={'1 0 0'} h={0}>
|
||||
{members.map((member) => {
|
||||
<Box mt={2}>{t('common:chosen') + ': ' + selected.length}</Box>
|
||||
<GroupScrollData mt={3} flex={'1 0 0'} h={0}>
|
||||
{selected.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
onMouseEnter={() => setHoveredMemberId(member.tmbId)}
|
||||
@@ -284,7 +288,7 @@ function GroupEditModal({
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</GroupScrollData>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
|
@@ -15,12 +15,16 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TeamContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type';
|
||||
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
|
||||
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({
|
||||
group,
|
||||
@@ -33,22 +37,24 @@ export function ChangeOwnerModal({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const { data: searchedData } = useRequest2(
|
||||
async () => {
|
||||
if (!inputValue) return;
|
||||
return GetSearchUserGroupOrg(inputValue);
|
||||
},
|
||||
const [searchKey, setSearchKey] = React.useState('');
|
||||
|
||||
const {
|
||||
data: members = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList
|
||||
} = useScrollPagination<any, PaginationResponse<TeamMemberItemType<{ withGroupRole: true }>>>(
|
||||
getTeamMembers,
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [inputValue],
|
||||
throttleWait: 500,
|
||||
debounceWait: 200
|
||||
pageSize: 20,
|
||||
params: {
|
||||
searchKey: searchKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { members: allMembers } = useContextSelector(TeamContext, (v) => v);
|
||||
const memberList = searchedData ? searchedData.members : allMembers;
|
||||
const search = _.debounce(refreshList, 500);
|
||||
useEffect(() => search, [searchKey]);
|
||||
|
||||
const {
|
||||
isOpen: isOpenMemberListMenu,
|
||||
@@ -108,9 +114,9 @@ export function ChangeOwnerModal({
|
||||
)}
|
||||
<Input
|
||||
placeholder={t('common:permission.change_owner_placeholder')}
|
||||
value={inputValue}
|
||||
value={searchKey}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
setSearchKey(e.target.value);
|
||||
setSelectedMember(null);
|
||||
}}
|
||||
onFocus={() => {
|
||||
@@ -120,7 +126,7 @@ export function ChangeOwnerModal({
|
||||
{...(selectedMember && { pl: '10' })}
|
||||
/>
|
||||
</Flex>
|
||||
{isOpenMemberListMenu && memberList.length > 0 && (
|
||||
{isOpenMemberListMenu && members.length > 0 && (
|
||||
<Flex
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
@@ -134,26 +140,28 @@ export function ChangeOwnerModal({
|
||||
maxH={'300px'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
{memberList.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setInputValue(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
<MemberScrollData>
|
||||
{members.map((item) => (
|
||||
<Box
|
||||
key={item.tmbId}
|
||||
p="2"
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
mx="1"
|
||||
borderRadius="md"
|
||||
cursor={'pointer'}
|
||||
onClickCapture={() => {
|
||||
setSearchKey(item.memberName);
|
||||
setSelectedMember(item);
|
||||
onCloseMemberListMenu();
|
||||
}}
|
||||
>
|
||||
<Flex align="center">
|
||||
<Avatar src={item.avatar} w="1.25rem" />
|
||||
<Box ml="2">{item.memberName}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
|
@@ -41,12 +41,15 @@ import {
|
||||
import format from 'date-fns/format';
|
||||
import OrgTags from '@/components/support/user/team/OrgTags';
|
||||
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 { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
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 TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
|
||||
@@ -55,11 +58,26 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
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 { feConfigs } = useSystemStore();
|
||||
const isSyncMember = feConfigs?.register_method?.includes('sync');
|
||||
|
||||
const { myTeams, onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
|
||||
const [status, setStatus] = useState<string>();
|
||||
|
||||
const {
|
||||
isOpen: isOpenTeamTagsAsync,
|
||||
@@ -68,35 +86,36 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
} = useDisclosure();
|
||||
|
||||
// member action
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
const {
|
||||
data: members = [],
|
||||
isLoading: loadingMembers,
|
||||
refreshList: refetchMemberList,
|
||||
ScrollData: MemberScrollData
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
} = useScrollPagination<
|
||||
any,
|
||||
PaginationResponse<TeamMemberItemType<{ withOrgs: true; withPermission: true }>>
|
||||
>(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
withLeaved: true
|
||||
status,
|
||||
withPermission: true,
|
||||
withOrgs: true,
|
||||
searchKey
|
||||
}
|
||||
});
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const { data: searchMembersData, run: refreshSearchMembers } = useRequest2(
|
||||
async () => {
|
||||
if (!searchText) return Promise.resolve();
|
||||
return GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false });
|
||||
},
|
||||
{
|
||||
manual: false,
|
||||
throttleWait: 500,
|
||||
refreshDeps: [searchText]
|
||||
}
|
||||
);
|
||||
const refreshList = _.debounce(() => {
|
||||
refetchMemberList();
|
||||
}, 200);
|
||||
|
||||
useEffect(() => {
|
||||
refreshList();
|
||||
}, [searchKey, status]);
|
||||
|
||||
const onRefreshMembers = useCallback(() => {
|
||||
refetchMemberList();
|
||||
refreshSearchMembers();
|
||||
}, [refetchMemberList, refreshSearchMembers]);
|
||||
}, [refetchMemberList]);
|
||||
|
||||
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'}>
|
||||
{Tabs}
|
||||
<HStack alignItems={'center'}>
|
||||
<Box>
|
||||
<MySelect list={statusOptions} value={status} onChange={(v) => setStatus(v)} />
|
||||
</Box>
|
||||
<Box width={'200px'}>
|
||||
<SearchInput
|
||||
placeholder={t('account_team:search_member')}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
|
||||
@@ -264,107 +286,104 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{(searchText && searchMembersData ? searchMembersData.members : members).map(
|
||||
(member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{member.memberName}
|
||||
{member.status !== 'active' && (
|
||||
<Tag ml="2" colorSchema="gray" bg={'myGray.100'} color={'myGray.700'}>
|
||||
{t('account_team:leave')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td maxW={'300px'}>{member.contact || '-'}</Td>
|
||||
<Td maxWidth="300px">
|
||||
{(() => {
|
||||
return <OrgTags orgs={member.orgs || undefined} type="tag" />;
|
||||
})()}
|
||||
</Td>
|
||||
<Td maxW={'300px'}>
|
||||
<VStack gap={0} alignItems="flex-start">
|
||||
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
|
||||
<Box>
|
||||
{member.updateTime
|
||||
? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss')
|
||||
: '-'}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
member.role !== TeamMemberRoleEnum.owner &&
|
||||
member.tmbId !== userInfo?.team.tmbId &&
|
||||
(member.status === TeamMemberStatusEnum.active ? (
|
||||
<>
|
||||
<Icon
|
||||
name={'edit'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'blue.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() =>
|
||||
handleEditMemberName(member.tmbId, member.memberName)
|
||||
}
|
||||
/>
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() => onRemoveMember(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:remove_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
member.status === TeamMemberStatusEnum.forbidden && (
|
||||
<Icon
|
||||
name={'common/confirm/restoreTip'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'primary.500',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRestoreMember(
|
||||
() => onRestore(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:restore_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
{members.map((member) => (
|
||||
<Tr key={member.tmbId} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Avatar src={member.avatar} w={['18px', '22px']} borderRadius={'50%'} />
|
||||
<Box className={'textEllipsis'}>
|
||||
{member.memberName}
|
||||
{member.status !== 'active' && (
|
||||
<Tag ml="2" colorSchema="gray" bg={'myGray.100'} color={'myGray.700'}>
|
||||
{t('account_team:leave')}
|
||||
</Tag>
|
||||
)}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Td>
|
||||
<Td maxW={'300px'}>{member.contact || '-'}</Td>
|
||||
<Td maxWidth="300px">
|
||||
{(() => {
|
||||
return <OrgTags orgs={member.orgs || undefined} type="tag" />;
|
||||
})()}
|
||||
</Td>
|
||||
<Td maxW={'300px'}>
|
||||
<VStack gap={0}>
|
||||
<Box>{format(new Date(member.createTime), 'yyyy-MM-dd HH:mm:ss')}</Box>
|
||||
<Box>
|
||||
{member.updateTime
|
||||
? format(new Date(member.updateTime), 'yyyy-MM-dd HH:mm:ss')
|
||||
: '-'}
|
||||
</Box>
|
||||
</VStack>
|
||||
</Td>
|
||||
<Td>
|
||||
{userInfo?.team.permission.hasManagePer &&
|
||||
member.role !== TeamMemberRoleEnum.owner &&
|
||||
member.tmbId !== userInfo?.team.tmbId &&
|
||||
(member.status === TeamMemberStatusEnum.active ? (
|
||||
<>
|
||||
{' '}
|
||||
<Icon
|
||||
name={'edit'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'blue.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => handleEditMemberName(member.tmbId, member.memberName)}
|
||||
/>
|
||||
<Icon
|
||||
name={'common/trash'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'red.600',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRemoveMember(
|
||||
() => onRemoveMember(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:remove_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
member.status === TeamMemberStatusEnum.forbidden && (
|
||||
<Icon
|
||||
name={'common/confirm/restoreTip'}
|
||||
cursor={'pointer'}
|
||||
w="1rem"
|
||||
p="1"
|
||||
borderRadius="sm"
|
||||
_hover={{
|
||||
color: 'primary.500',
|
||||
bgColor: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
openRestoreMember(
|
||||
() => onRestore(member.tmbId),
|
||||
undefined,
|
||||
t('account_team:restore_tip', {
|
||||
username: member.memberName
|
||||
})
|
||||
)();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<ConfirmRemoveMemberModal />
|
||||
|
@@ -29,12 +29,14 @@ function OrgInfoModal({
|
||||
editOrg,
|
||||
onClose,
|
||||
onSuccess,
|
||||
updateCurrentOrg
|
||||
updateCurrentOrg,
|
||||
parentId
|
||||
}: {
|
||||
editOrg: OrgFormType;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
updateCurrentOrg: (data: { name?: string; avatar?: string; description?: string }) => void;
|
||||
parentId?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -51,10 +53,11 @@ function OrgInfoModal({
|
||||
|
||||
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
|
||||
async (data: OrgFormType) => {
|
||||
if (parentId === undefined) return;
|
||||
return postCreateOrg({
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
path: editOrg.path,
|
||||
orgId: parentId,
|
||||
description: data.description
|
||||
});
|
||||
},
|
||||
|
@@ -1,14 +1,5 @@
|
||||
import { getOrgMembers, putUpdateOrgMembers } from '@/web/support/user/team/org/api';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
ModalBody,
|
||||
ModalFooter
|
||||
} from '@chakra-ui/react';
|
||||
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
|
||||
import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
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 { 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 MemberItemCard from '@/components/support/permission/MemberManager/MemberItemCard';
|
||||
|
||||
export type GroupFormType = {
|
||||
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({
|
||||
currentOrg,
|
||||
refetchOrgs,
|
||||
@@ -56,17 +37,24 @@ function OrgMemberManageModal({
|
||||
ScrollData: MemberScrollData,
|
||||
isLoading: isLoadingMembers
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 20
|
||||
pageSize: 20,
|
||||
params: {
|
||||
withOrgs: true,
|
||||
withPermission: false,
|
||||
status: 'active'
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: orgMembers,
|
||||
ScrollData: OrgMemberScrollData,
|
||||
isLoading: isLoadingOrgMembers
|
||||
} = useScrollPagination(getOrgMembers, {
|
||||
pageSize: 20,
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 100000,
|
||||
params: {
|
||||
orgPath: getOrgChildrenPath(currentOrg)
|
||||
orgId: currentOrg._id,
|
||||
withOrgs: false,
|
||||
withPermission: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83,11 +71,6 @@ function OrgMemberManageModal({
|
||||
}, [orgMembers]);
|
||||
|
||||
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(
|
||||
() => {
|
||||
@@ -165,35 +148,20 @@ function OrgMemberManageModal({
|
||||
}}
|
||||
/>
|
||||
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'} isLoading={isLoadingMembers}>
|
||||
{filterMembers.map((member) => {
|
||||
{allMembers.map((member) => {
|
||||
return (
|
||||
<HStack
|
||||
py="2"
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
alignItems="center"
|
||||
<MemberItemCard
|
||||
avatar={member.avatar}
|
||||
key={member.tmbId}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.50',
|
||||
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
|
||||
}}
|
||||
_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>
|
||||
name={member.memberName}
|
||||
onChange={() => handleToggleSelect(member.tmbId)}
|
||||
isChecked={!!isSelected(member.tmbId)}
|
||||
orgs={member.orgs}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MemberScrollData>
|
||||
</Flex>
|
||||
{/* <Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'100%'}> */}
|
||||
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
|
||||
<OrgMemberScrollData
|
||||
mt={3}
|
||||
|
@@ -20,8 +20,6 @@ function OrgMoveModal({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedOrg, setSelectedOrg] = useState<OrgListItemType>();
|
||||
const { userInfo } = useUserStore();
|
||||
const team = userInfo?.team!;
|
||||
|
||||
const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, {
|
||||
onSuccess: () => {
|
||||
@@ -39,7 +37,7 @@ function OrgMoveModal({
|
||||
iconColor="primary.600"
|
||||
>
|
||||
<ModalBody>
|
||||
<OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} />
|
||||
<OrgTree selectedOrg={selectedOrg} setSelectedOrg={setSelectedOrg} movingOrg={movingOrg} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
|
@@ -14,17 +14,19 @@ function OrgTreeNode({
|
||||
org,
|
||||
selectedOrg,
|
||||
setSelectedOrg,
|
||||
index = 0
|
||||
index = 0,
|
||||
movingOrg
|
||||
}: {
|
||||
org: OrgListItemType;
|
||||
selectedOrg?: OrgListItemType;
|
||||
setSelectedOrg: (org?: OrgListItemType) => void;
|
||||
index?: number;
|
||||
movingOrg: OrgListItemType;
|
||||
}) {
|
||||
const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
|
||||
const [canBeExpanded, setCanBeExpanded] = useState(true);
|
||||
const { data: orgs = [], runAsync: getOrgs } = useRequest2(() =>
|
||||
getOrgList({ orgPath: getOrgChildrenPath(org) })
|
||||
getOrgList({ orgId: org._id, withPermission: false })
|
||||
);
|
||||
const onClickExpand = async () => {
|
||||
const data = await getOrgs();
|
||||
@@ -34,6 +36,9 @@ function OrgTreeNode({
|
||||
toggleIsExpanded.toggle();
|
||||
};
|
||||
|
||||
if (org._id === movingOrg._id) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box userSelect={'none'}>
|
||||
<HStack
|
||||
@@ -78,6 +83,7 @@ function OrgTreeNode({
|
||||
orgs.map((child) => (
|
||||
<Box key={child._id} mt={0.5}>
|
||||
<OrgTreeNode
|
||||
movingOrg={movingOrg}
|
||||
org={child}
|
||||
index={index + 1}
|
||||
selectedOrg={selectedOrg}
|
||||
@@ -91,10 +97,12 @@ function OrgTreeNode({
|
||||
|
||||
function OrgTree({
|
||||
selectedOrg,
|
||||
setSelectedOrg
|
||||
setSelectedOrg,
|
||||
movingOrg
|
||||
}: {
|
||||
selectedOrg?: OrgListItemType;
|
||||
setSelectedOrg: (org?: OrgListItemType) => void;
|
||||
movingOrg: OrgListItemType;
|
||||
}) {
|
||||
const { userInfo } = useUserStore();
|
||||
const root: OrgListItemType = {
|
||||
@@ -107,6 +115,7 @@ function OrgTree({
|
||||
|
||||
return (
|
||||
<OrgTreeNode
|
||||
movingOrg={movingOrg}
|
||||
key={'root'}
|
||||
org={root}
|
||||
selectedOrg={selectedOrg}
|
||||
|
@@ -78,9 +78,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
const [editOrg, setEditOrg] = useState<OrgFormType>();
|
||||
const [manageMemberOrg, setManageMemberOrg] = useState<OrgListItemType>();
|
||||
const [movingOrg, setMovingOrg] = useState<OrgListItemType>();
|
||||
|
||||
const [searchOrg, setSearchOrg] = useState('');
|
||||
|
||||
const {
|
||||
currentOrg,
|
||||
orgs,
|
||||
@@ -91,7 +88,9 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
MemberScrollData,
|
||||
onPathClick,
|
||||
refresh,
|
||||
updateCurrentOrg
|
||||
updateCurrentOrg,
|
||||
setSearchKey,
|
||||
searchKey
|
||||
} = useOrg();
|
||||
|
||||
// Delete org
|
||||
@@ -123,12 +122,6 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
onSuccess: refresh
|
||||
});
|
||||
|
||||
const searchedOrgs = useMemo(() => {
|
||||
if (!searchOrg) return [];
|
||||
|
||||
return orgs.filter((org) => org.name.includes(searchOrg));
|
||||
}, [orgs, searchOrg]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
|
||||
@@ -136,8 +129,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
<Box w="200px">
|
||||
<SearchInput
|
||||
placeholder={t('account_team:search_org')}
|
||||
value={searchOrg}
|
||||
onChange={(e) => setSearchOrg(e.target.value)}
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
@@ -166,102 +159,55 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{searchedOrgs.map((org) => (
|
||||
<Tr key={org._id} overflow={'unset'} onClick={() => onClickOrg(org)}>
|
||||
<Td>
|
||||
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
|
||||
<MemberTag name={org.name} avatar={org.avatar!} />
|
||||
<Tag size="sm">{org.total}</Tag>
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w={'1rem'}
|
||||
h={'1rem'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
{isTeamAdmin && !isSyncMember && (
|
||||
<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)
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{orgs
|
||||
.filter((org) => org.path !== '')
|
||||
.map((org) => (
|
||||
<Tr key={org._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
|
||||
<MemberTag name={org.name} avatar={org.avatar} />
|
||||
<Tag size="sm">{org.total}</Tag>
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w={'1rem'}
|
||||
h={'1rem'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
{!searchOrg &&
|
||||
orgs
|
||||
.filter((org) => org.path !== '')
|
||||
.map((org) => (
|
||||
<Tr key={org._id} overflow={'unset'}>
|
||||
<Td>
|
||||
<HStack cursor={'pointer'} onClick={() => onClickOrg(org)}>
|
||||
<MemberTag name={org.name} avatar={org.avatar} />
|
||||
<Tag size="sm">{org.total}</Tag>
|
||||
<MyIcon
|
||||
name="core/chat/chevronRight"
|
||||
w={'1rem'}
|
||||
h={'1rem'}
|
||||
color={'myGray.500'}
|
||||
/>
|
||||
</HStack>
|
||||
{isTeamAdmin && !isSyncMember && (
|
||||
<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>
|
||||
{isTeamAdmin && !isSyncMember && (
|
||||
<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>
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
{!searchOrg &&
|
||||
)}
|
||||
</Tr>
|
||||
))}
|
||||
{!searchKey &&
|
||||
members.map((member) => {
|
||||
return (
|
||||
<Tr key={member.tmbId}>
|
||||
@@ -403,6 +349,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
|
||||
onClose={() => setEditOrg(undefined)}
|
||||
onSuccess={refresh}
|
||||
updateCurrentOrg={updateCurrentOrg}
|
||||
parentId={currentOrg._id}
|
||||
/>
|
||||
)}
|
||||
{!!movingOrg && (
|
||||
|
@@ -20,20 +20,17 @@ const EditInfoModal = dynamic(() => import('./EditInfoModal'));
|
||||
|
||||
type TeamModalContextType = {
|
||||
myTeams: TeamTmbItemType[];
|
||||
members: TeamMemberItemType[];
|
||||
isLoading: boolean;
|
||||
onSwitchTeam: (teamId: string) => void;
|
||||
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
|
||||
|
||||
refetchMembers: () => void;
|
||||
refetchTeamSize: () => void;
|
||||
refetchTeams: () => void;
|
||||
teamSize: number;
|
||||
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
|
||||
};
|
||||
|
||||
export const TeamContext = createContext<TeamModalContextType>({
|
||||
myTeams: [],
|
||||
members: [],
|
||||
isLoading: false,
|
||||
onSwitchTeam: function (_teamId: string): void {
|
||||
throw new Error('Function not implemented.');
|
||||
@@ -44,11 +41,10 @@ export const TeamContext = createContext<TeamModalContextType>({
|
||||
refetchTeams: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
refetchMembers: function (): void {
|
||||
refetchTeamSize: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
teamSize: 0,
|
||||
MemberScrollData: () => <></>
|
||||
teamSize: 0
|
||||
});
|
||||
|
||||
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
@@ -67,32 +63,11 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
||||
refreshDeps: [userInfo?._id]
|
||||
});
|
||||
|
||||
const { data: teamMemberCountData, refresh: refetchTeamMemberCount } = useRequest2(
|
||||
getTeamMemberCount,
|
||||
{
|
||||
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 { data: teamMemberCountData, refresh: refetchTeamSize } = useRequest2(getTeamMemberCount, {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId]
|
||||
});
|
||||
|
||||
const refetchMembers = useCallback(() => {
|
||||
refetchTeamMemberCount();
|
||||
refetchMemberList();
|
||||
}, [refetchTeamMemberCount, refetchMemberList]);
|
||||
|
||||
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
|
||||
async (teamId: string) => {
|
||||
await putSwitchTeam(teamId);
|
||||
@@ -106,7 +81,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers;
|
||||
const isLoading = isLoadingTeams || isSwitchingTeam;
|
||||
|
||||
const contextValue = {
|
||||
myTeams,
|
||||
@@ -116,10 +91,8 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
|
||||
|
||||
// create | update team
|
||||
setEditTeamData,
|
||||
members,
|
||||
refetchMembers,
|
||||
teamSize: teamMemberCountData?.count || 0,
|
||||
MemberScrollData
|
||||
refetchTeamSize
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -34,8 +34,16 @@ export const putSwitchTeam = (teamId: string) =>
|
||||
PUT<string>(`/proApi/support/user/team/switch`, { teamId });
|
||||
|
||||
/* --------------- team member ---------------- */
|
||||
export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) =>
|
||||
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
|
||||
export const getTeamMembers = (
|
||||
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 = () =>
|
||||
GET<{ count: number }>(`/proApi/support/user/team/member/count`);
|
||||
|
||||
|
@@ -10,8 +10,11 @@ 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 = (params: { orgPath: string; getPermission?: boolean }) =>
|
||||
GET<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
|
||||
export const getOrgList = (params: {
|
||||
orgId: string;
|
||||
withPermission?: boolean;
|
||||
searchKey?: string;
|
||||
}) => POST<OrgListItemType[]>(`/proApi/support/user/team/org/list`, params);
|
||||
|
||||
export const postCreateOrg = (data: postCreateOrgData) =>
|
||||
POST('/proApi/support/user/team/org/create', data);
|
||||
|
@@ -1,14 +1,17 @@
|
||||
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 { memo, useEffect, 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';
|
||||
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 [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
@@ -34,10 +37,18 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
|
||||
data: orgs = [],
|
||||
loading: isLoadingOrgs,
|
||||
refresh: refetchOrgs
|
||||
} = useRequest2(() => getOrgList({ orgPath: path, getPermission }), {
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId, path]
|
||||
});
|
||||
} = useRequest2(
|
||||
() => getOrgList({ orgId: currentOrg._id, withPermission: withPermission, searchKey }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [userInfo?.team?.teamId, path, currentOrg._id]
|
||||
}
|
||||
);
|
||||
const search = _.debounce(() => {
|
||||
if (!searchKey) return;
|
||||
refetchOrgs();
|
||||
}, 200);
|
||||
useEffect(() => search, [searchKey]);
|
||||
|
||||
const paths = useMemo(() => {
|
||||
if (!currentOrg) return [];
|
||||
@@ -53,16 +64,20 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
|
||||
|
||||
const onClickOrg = (org: OrgListItemType) => {
|
||||
setOrgStack([...orgStack, org]);
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const {
|
||||
data: members = [],
|
||||
ScrollData: MemberScrollData,
|
||||
refreshList: refetchMembers
|
||||
} = useScrollPagination(getOrgMembers, {
|
||||
} = useScrollPagination(getTeamMembers, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
orgPath: path
|
||||
orgId: currentOrg._id,
|
||||
withOrgs: false,
|
||||
withPermission: true,
|
||||
status: 'active'
|
||||
},
|
||||
refreshDeps: [path]
|
||||
});
|
||||
@@ -70,6 +85,7 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
|
||||
const onPathClick = (path: string) => {
|
||||
const pathIds = path.split('/');
|
||||
setOrgStack(orgStack.filter((org) => pathIds.includes(org.pathId)));
|
||||
setSearchKey('');
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
@@ -101,7 +117,9 @@ function useOrg({ getPermission = true }: { getPermission?: boolean } = {}) {
|
||||
MemberScrollData,
|
||||
onPathClick,
|
||||
refresh,
|
||||
updateCurrentOrg
|
||||
updateCurrentOrg,
|
||||
searchKey,
|
||||
setSearchKey
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user