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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user