chore: team, orgs, search and so on (#3807)

* feat: clb search support username, memberName, contacts

* feat: popup org names

* feat: update team member table

* feat: restore the member

* feat: search user in team member table

* feat: bind contact

* feat: export members

* feat: org tab could delete member

* feat: org table search

* feat: team notification account bind

* feat: permission tab search

* fix: wecom sso

* chore(init): copy notificationAccount to user.contact

* chore: adjust

* fix: ts error

* fix: useConfirm iconColor customization

* pref: fe

* fix: style

* fix: fix team member manage

* pref: enlarge team member pagesize

* pref: initv4822

* fix: pageSize

* pref: initscritpt
This commit is contained in:
Finley Ge
2025-02-19 17:27:19 +08:00
committed by GitHub
parent 5fd520c794
commit 206325bc5f
35 changed files with 867 additions and 349 deletions

View File

@@ -2,18 +2,30 @@ import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
import {
Box,
Button,
Flex,
HStack,
Input,
ModalBody,
ModalFooter,
useDisclosure
} from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
import Icon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'));
export type EditTeamFormDataType = CreateTeamProps & {
id?: string;
notificationAccount?: string;
};
export const defaultForm = {
@@ -31,12 +43,12 @@ function EditModal({
onSuccess: () => void;
}) {
const { t } = useTranslation();
const { toast } = useToast();
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
defaultValues: defaultData
});
const avatar = watch('avatar');
const notificationAccount = watch('notificationAccount');
const {
File,
@@ -74,6 +86,8 @@ function EditModal({
errorToast: t('common:common.Update Failed')
});
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
return (
<MyModal
isOpen
@@ -84,7 +98,7 @@ function EditModal({
>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('user:team.Set Name')}
{t('account_team:set_name_avatar')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common:common.Set Avatar')}>
@@ -110,6 +124,38 @@ function EditModal({
})}
/>
</Flex>
<Box color={'myGray.800'} fontWeight={'bold'} mt={4}>
{t('account_team:notification_recieve')}
</Box>
<HStack w="full" justifyContent={'space-between'}>
{(() => {
return notificationAccount ? (
<Box width="full">{notificationAccount}</Box>
) : (
<HStack
px="3"
py="1"
color="red.600"
bgColor="red.50"
borderRadius="md"
fontSize={'sm'}
width={'fit-content'}
>
<Icon name="common/info" w="1rem" />
<Box width="fit-content">{t('account_info:please_bind_contact')}</Box>
</HStack>
);
})()}
<Button
variant={'whiteBase'}
leftIcon={<Icon name="common/setting" w="1rem" />}
onClick={() => {
onOpenContact();
}}
>
{t('common:common.Setting')}
</Button>
</HStack>
</ModalBody>
<ModalFooter>
@@ -142,6 +188,14 @@ function EditModal({
})
}
/>
{isOpenContact && (
<UpdateContact
onClose={() => {
onCloseContact();
}}
mode="notification_account"
/>
)}
</MyModal>
);
}

View File

@@ -148,7 +148,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" p="4">
<Flex flexDirection="column" p="4" overflowY={'auto'} overflowX={'hidden'}>
<SearchInput
placeholder={t('user:search_user')}
fontSize="sm"
@@ -157,7 +157,7 @@ function GroupEditModal({ onClose, editGroupId }: { onClose: () => void; editGro
setSearchKey(e.target.value);
}}
/>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
{filtered.map((member) => {
return (
<HStack

View File

@@ -11,15 +11,15 @@ import {
Th,
Thead,
Tr,
useDisclosure
useDisclosure,
VStack
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { delRemoveMember } from '@/web/support/user/team/api';
import { delRemoveMember, updateStatus } from '@/web/support/user/team/api';
import Tag from '@fastgpt/web/components/common/Tag';
import Icon from '@fastgpt/web/components/common/Icon';
import GroupTags from '@/components/support/permission/Group/GroupTags';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from './context';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -29,9 +29,17 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { postSyncMembers } from '@/web/support/user/api';
import { GetSearchUserGroupOrg, postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import {
TeamMemberRoleEnum,
TeamMemberStatusEnum
} from '@fastgpt/global/support/user/team/constant';
import format from 'date-fns/format';
import OrgTags from '@/components/support/user/team/OrgTags';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils';
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@@ -43,14 +51,14 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const {
groups,
refetchGroups,
myTeams,
refetchTeams,
members,
refetchMembers,
onSwitchTeam,
MemberScrollData
MemberScrollData,
orgs
} = useContextSelector(TeamContext, (v) => v);
const {
@@ -64,8 +72,25 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
type: 'delete'
});
const { ConfirmModal: ConfirmRestoreMemberModal, openConfirm: openRestoreMember } = useConfirm({
type: 'common',
title: t('account_team:restore_tip_title'),
iconSrc: 'common/confirm/restoreTip',
iconColor: 'primary.500'
});
const [searchText, setSearchText] = useState<string>('');
const isSyncMember = feConfigs.register_method?.includes('sync');
const { data: searchMembersData } = useRequest2(
() => GetSearchUserGroupOrg(searchText, { members: true, orgs: false, groups: false }),
{
manual: false,
throttleWait: 500,
refreshDeps: [searchText]
}
);
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
@@ -92,12 +117,28 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
errorToast: t('account_team:sync_member_failed')
});
const { runAsync: onRestore, loading: isUpdateInvite } = useRequest2(updateStatus, {
onSuccess() {
refetchMembers();
},
successToast: t('common:user.team.invite.Accepted'),
errorToast: t('common:user.team.invite.Reject')
});
const isLoading = isUpdateInvite || isSyncing;
return (
<>
{isSyncing && <MyLoading />}
{isLoading && <MyLoading />}
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack>
<HStack alignItems={'center'}>
<Box width={'200px'}>
<SearchInput
placeholder={t('account_team:search_member')}
onChange={(e) => setSearchText(e.target.value)}
/>
</Box>
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
@@ -165,6 +206,23 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
{t('account_team:user_team_leave_team')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="export" w={'16px'} />}
onClick={() => {
downloadFetch({
url: '/api/proApi/support/user/team/member/export',
filename: `${userInfo.team.teamName}-${format(new Date(), 'yyyyMMddHHmmss')}.csv`
});
}}
>
{t('account_team:export_members')}
</Button>
)}
</HStack>
</Flex>
@@ -177,45 +235,66 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
{!isSyncMember && (
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
)}
<Th bgColor="myGray.100">{t('account_team:contact')}</Th>
<Th bgColor="myGray.100">{t('account_team:org')}</Th>
<Th bgColor="myGray.100">{t('account_team:join_update_time')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) =>
group.members.map((m) => m.tmbId).includes(item.tmbId)
)
.map((g) => g.name)}
max={3}
/>
</Td>
{!isSyncMember && (
{(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 === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
{member.status === 'leave' && (
<Tag ml="2" colorSchema="gray">
{t('account_team:leave')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>{member.contact || '-'}</Td>
<Td maxWidth="300px">
{(() => {
const memberOrgs = orgs.filter((org) =>
org.members.find((v) => String(v.tmbId) === String(member.tmbId))
);
const memberPathIds = memberOrgs.map((org) =>
(org.path + '/' + org.pathId).split('/').slice(0)
);
const memberOrgNames = memberPathIds.map((pathIds) =>
pathIds.map((id) => orgs.find((v) => v.pathId === id)?.name).join('/')
);
return <OrgTags orgs={memberOrgNames} 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 &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
member.role !== TeamMemberRoleEnum.owner &&
member.tmbId !== userInfo?.team.tmbId &&
(member.status !== TeamMemberStatusEnum.leave ? (
<Icon
name={'common/trash'}
cursor={'pointer'}
@@ -229,24 +308,50 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
delRemoveMember(member.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
username: member.memberName
})
)();
}}
/>
)}
) : (
<Icon
name={'common/confirm/restoreTip'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'primary.500',
bgColor: 'myGray.100'
}}
onClick={() => {
openRestoreMember(
() =>
onRestore({
tmbId: member.tmbId,
status: TeamMemberStatusEnum.active
}),
undefined,
t('account_team:restore_tip', {
username: member.memberName
})
)();
}}
/>
))}
</Td>
)}
</Tr>
))}
</Tr>
)
)}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
<ConfirmRestoreMemberModal />
</TableContainer>
</MemberScrollData>
</Box>

View File

@@ -112,7 +112,7 @@ function OrgMemberManageModal({
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" p="4">
<Flex flexDirection="column" p="4" overflowY="auto" overflowX="hidden">
<SearchInput
placeholder={t('user:search_user')}
fontSize="sm"
@@ -121,7 +121,7 @@ function OrgMemberManageModal({
setSearchKey(e.target.value);
}}
/>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
<MemberScrollData mt={3} flexGrow="1" overflow={'auto'}>
{filterMembers.map((member) => {
return (
<HStack

View File

@@ -37,6 +37,8 @@ import Path from '@/components/common/folder/Path';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { delRemoveMember } from '@/web/support/user/team/api';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
@@ -74,8 +76,9 @@ function ActionButton({
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const [searchOrg, setSearchOrg] = useState('');
const { members, MemberScrollData } = useContextSelector(TeamContext, (v) => v);
const { members, MemberScrollData, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { feConfigs } = useSystemStore();
const isSyncMember = feConfigs.register_method?.includes('sync');
@@ -88,6 +91,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
@@ -107,6 +111,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
};
});
}, [orgs, parentPath]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
@@ -121,7 +126,7 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
if (org?.path === '') return;
return {
parentId: getOrgChildrenPath(org),
@@ -148,20 +153,52 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { ConfirmModal: ConfirmDeleteMemberFromOrg, openConfirm: openDeleteMemberFromOrgModal } =
useConfirm({
type: 'delete'
});
const { ConfirmModal: ConfirmDeleteMemberFromTeam, openConfirm: openDeleteMemberFromTeamModal } =
useConfirm({
type: 'delete'
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
const { runAsync: deleteMemberFromTeamReq } = useRequest2(delRemoveMember, {
onSuccess: () => {
refetchOrgs();
refetchMembers();
}
});
const searchedOrgs = useMemo(() => {
if (!searchOrg) return [];
return orgs
.filter((org) => org.name.includes(searchOrg))
.map((org) => ({
...org,
count:
org.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(org)).length
}));
}, [orgs, searchOrg]);
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<Box w="200px">
<SearchInput
placeholder={t('account_team:search_org')}
value={searchOrg}
onChange={(e) => setSearchOrg(e.target.value)}
/>
</Box>
</Flex>
<MyBox
flex={'1 0 0'}
@@ -183,20 +220,25 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
{!isSyncMember && (
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
)}
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
{searchedOrgs.map((org) => (
<Tr
key={org._id}
overflow={'unset'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
setSearchOrg('');
}}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
@@ -208,73 +250,130 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
/>
</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)
}
]
}
]}
/>
</Td>
)}
</Tr>
))}
{currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
{!searchOrg &&
currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
<HStack
cursor={'pointer'}
onClick={() => {
setParentPath(getOrgChildrenPath(org));
setSearchOrg('');
}}
>
<MemberTag name={org.name} avatar={org.avatar} />
<Tag size="sm">{org.count}</Tag>
<MyIcon
name="core/chat/chevronRight"
w={'1rem'}
h={'1rem'}
color={'myGray.500'}
/>
</HStack>
</Td>
<Td w={'6rem'}>
{isTeamAdmin && !isSyncMember && (
{isTeamAdmin && !isSyncMember && (
<Td w={'6rem'}>
<MyMenu
trigger={'hover'}
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: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
onClick: () => deleteOrgHandler(org._id)
}
]
}
]}
/>
)}
</Td>
</Td>
)}
</Tr>
);
})}
))}
{!searchOrg &&
currentOrg?.members.map((member) => {
const memberInfo = members.find((m) => m.tmbId === member.tmbId);
if (!memberInfo) return null;
return (
<Tr key={member.tmbId}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
menuItemStyles: {
_hover: {
color: 'red.600',
backgroundColor: 'red.50'
}
},
label: t('account_team:delete_from_team', {
username: memberInfo.memberName
}),
onClick: () => {
openDeleteMemberFromTeamModal(
() => deleteMemberFromTeamReq(member.tmbId),
undefined,
t('account_team:confirm_delete_from_team', {
username: memberInfo.memberName
})
)();
}
},
...(isSyncMember
? []
: [
{
menuItemStyles: {
_hover: {
color: 'red.600',
bgColor: 'red.50'
}
},
label: t('account_team:delete_from_org'),
onClick: () =>
openDeleteMemberFromOrgModal(
() =>
deleteMemberReq(currentOrg._id, member.tmbId),
undefined,
t('account_team:confirm_delete_from_org', {
username: memberInfo.memberName
})
)()
}
])
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
@@ -361,7 +460,8 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
<ConfirmDeleteMemberFromOrg />
<ConfirmDeleteMemberFromTeam />
</>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState } from 'react';
import {
Box,
Checkbox,
@@ -40,7 +40,8 @@ import CollaboratorContextProvider, {
} from '@/components/support/permission/MemberManager/context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
function PermissionManage({
Tabs,
@@ -69,27 +70,37 @@ function PermissionManage({
const [isExpandGroup, setExpandGroup] = useToggle(true);
const [isExpandOrg, setExpandOrg] = useToggle(true);
const { tmbList, groupList, orgList } = useMemo(() => {
const tmbList: CollaboratorItemType[] = [];
const groupList: CollaboratorItemType[] = [];
const orgList: CollaboratorItemType[] = [];
const [searchKey, setSearchKey] = useState('');
collaboratorList.forEach((item) => {
if (item.tmbId) {
tmbList.push(item);
} else if (item.groupId) {
groupList.push(item);
} else if (item.orgId) {
orgList.push(item);
}
});
const { data: searchResult } = useRequest2(() => GetSearchUserGroupOrg(searchKey), {
manual: false,
throttleWait: 500,
refreshDeps: [searchKey]
});
const { tmbList, groupList, orgList } = useMemo(() => {
const tmbList = collaboratorList.filter(
(item) =>
Object.keys(item).includes('tmbId') &&
(!searchKey || searchResult?.members.find((member) => member.tmbId === item.tmbId))
);
const groupList = collaboratorList.filter(
(item) =>
Object.keys(item).includes('groupId') &&
(!searchKey || searchResult?.groups.find((group) => group.groupId === item.groupId))
);
const orgList = collaboratorList.filter(
(item) =>
Object.keys(item).includes('orgId') &&
(!searchKey || searchResult?.orgs.find((org) => org._id === item.orgId))
);
return {
tmbList,
groupList,
orgList
};
}, [collaboratorList]);
}, [collaboratorList, searchResult, searchKey]);
const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2(
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => {
@@ -131,12 +142,12 @@ function PermissionManage({
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<Box ml="auto">
{/* <SearchInput
<SearchInput
placeholder={t('user:search_group_org_user')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/> */}
/>
</Box>
{userInfo?.team.permission.hasManagePer && (
<Button

View File

@@ -11,6 +11,8 @@ import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getOrgList } from '@/web/support/user/team/org/api';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -18,6 +20,7 @@ type TeamModalContextType = {
myTeams: TeamTmbItemType[];
members: TeamMemberItemType[];
groups: MemberGroupListType;
orgs: OrgType[];
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
@@ -25,6 +28,7 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
refetchOrgs: () => void;
teamSize: number;
MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
};
@@ -33,6 +37,7 @@ export const TeamContext = createContext<TeamModalContextType>({
myTeams: [],
groups: [],
members: [],
orgs: [],
isLoading: false,
onSwitchTeam: function (_teamId: string): void {
throw new Error('Function not implemented.');
@@ -49,7 +54,9 @@ export const TeamContext = createContext<TeamModalContextType>({
refetchGroups: function (): void {
throw new Error('Function not implemented.');
},
refetchOrgs: function (): void {
throw new Error('Function not implemented.');
},
teamSize: 0,
MemberScrollData: () => <></>
});
@@ -68,6 +75,15 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshDeps: [userInfo?._id]
});
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
// member action
const {
data: members = [],
@@ -75,7 +91,12 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshList: refetchMembers,
total: memberTotal,
ScrollData: MemberScrollData
} = useScrollPagination(getTeamMembers, {});
} = useScrollPagination(getTeamMembers, {
pageSize: 20,
params: {
withLeaved: true
}
});
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
@@ -96,13 +117,16 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refreshDeps: [userInfo?.team?.teamId]
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups;
const isLoading =
isLoadingTeams || isSwitchingTeam || loadingMembers || isLoadingGroups || isLoadingOrgs;
const contextValue = {
myTeams,
refetchTeams,
isLoading,
onSwitchTeam,
orgs,
refetchOrgs,
// create | update team
setEditTeamData,

View File

@@ -111,7 +111,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
provider: OAuthEnum.wecom,
icon: 'common/wecom',
redirectUrl: isWecomWorkTerminal
? `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${feConfigs?.oauth?.wecom?.corpid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&agentid=${feConfigs?.oauth?.wecom?.agentid}&state=${state.current}#wechat_redirect`
? `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${feConfigs?.oauth?.wecom?.corpid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_privateinfo&agentid=${feConfigs?.oauth?.wecom?.agentid}&state=${state.current}#wechat_redirect`
: `https://login.work.weixin.qq.com/wwlogin/sso/login?login_type=CorpApp&appid=${feConfigs?.oauth?.wecom?.corpid}&agentid=${feConfigs?.oauth?.wecom?.agentid}&redirect_uri=${redirectUri}&state=${state.current}`
}
]