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

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.8.21",
"version": "4.8.22",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -21,9 +21,7 @@ const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/U
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'));
const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
@@ -156,7 +154,7 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
{!!userInfo && <SystemMsgModal />}
{showUpdateNotification && (
<UpdateNotification onClose={() => setIsUpdateNotification(false)} />
<UpdateContact onClose={() => setIsUpdateNotification(false)} mode="contact" />
)}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import PermissionTags from './PermissionTags';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import OrgTags from '../../user/team/OrgTags';
function MemberItemCard({
avatar,
key,
onChange,
isChecked,
onDelete,
name,
permission,
orgs
}: {
avatar: string;
key: string;
onChange: () => void;
isChecked?: boolean;
onDelete?: () => void;
name: string;
permission?: PermissionValueType;
orgs?: string[];
}) {
return (
<>
<HStack
justifyContent="space-between"
alignItems="center"
key={key}
px="3"
py="2"
borderRadius="sm"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
onClick={onChange}
>
{isChecked !== undefined && <Checkbox isChecked={isChecked} pointerEvents="none" />}
<Avatar src={avatar} w="1.5rem" borderRadius={'50%'} />
<VStack w="full" gap={0}>
<Box w="full">{name}</Box>
<Box w="full">{orgs && orgs.length > 0 && <OrgTags orgs={orgs} />}</Box>
</VStack>
{permission && <PermissionTags permission={permission} />}
{onDelete !== undefined && (
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={onDelete}
/>
)}
</HStack>
</>
);
}
export default MemberItemCard;

View File

@@ -37,6 +37,8 @@ import { getTeamMembers } from '@/web/support/user/team/api';
import { getGroupList } from '@/web/support/user/team/group/api';
import { getOrgList } from '@/web/support/user/team/org/api';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import MemberItemCard from './MemberItemCard';
import { GetSearchUserGroupOrg } from '@/web/support/user/api';
const HoverBoxStyle = {
bgColor: 'myGray.50',
@@ -72,6 +74,12 @@ function MemberModal({
const [parentPath, setParentPath] = useState('');
const { data: searchedData } = useRequest2(() => GetSearchUserGroupOrg(searchText), {
manual: false,
throttleWait: 500,
refreshDeps: [searchText]
});
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
@@ -97,7 +105,10 @@ function MemberModal({
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
if (searchText) return orgs.filter((item) => item.name.includes(searchText));
if (searchText && searchedData) {
const orgids = searchedData.orgs.map((item) => item._id);
return orgs.filter((org) => orgids.includes(String(org._id)));
}
if (!searchText && filterClass !== 'org') return [];
if (parentPath === '') {
setParentPath(`/${orgs[0].pathId}`);
@@ -110,27 +121,34 @@ function MemberModal({
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
}));
}, [orgs, searchText, filterClass, parentPath]);
}, [searchText, filterClass, parentPath, orgs, searchedData]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const filterMembers = useMemo(() => {
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
if (searchText) {
return searchedData?.members;
}
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
if (currentOrg && filterClass === 'org') {
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
}
return members;
}, [members, searchText, filterClass, currentOrg]);
}, [members, searchedData, searchText, filterClass, currentOrg]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const filterGroups = useMemo(() => {
if (searchText) return groups.filter((item) => item.name.includes(searchText));
if (searchText) {
return searchedData?.groups.map((item) => ({
groupName: item.name,
_id: item.id,
...item
}));
}
if (!searchText && filterClass !== 'group') return [];
return groups;
}, [groups, searchText, filterClass]);
}, [searchText, filterClass, groups, searchedData]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
@@ -146,6 +164,7 @@ function MemberModal({
CollaboratorContext,
(v) => v.onUpdateCollaborators
);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
@@ -210,6 +229,7 @@ function MemberModal({
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
minW="800px"
maxW={'60vw'}
h={'100%'}
maxH={'90vh'}
isCentered
@@ -300,136 +320,122 @@ function MemberModal({
</Box>
)}
{filterClass && (
<ScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{filterOrgs.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
{org.count && (
<>
<Tag size="sm" my="auto">
{org.count}
</Tag>
</>
)}
</HStack>
<PermissionTags permission={collaborator?.permission.value} />
<ScrollData
flexDirection={'column'}
gap={1}
userSelect={'none'}
height={'fit-content'}
>
{filterOrgs?.map((org) => {
const onChange = () => {
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
{org.count && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
setParentPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
<>
<Tag size="sm" my="auto">
{org.count}
</Tag>
</>
)}
</HStack>
);
})}
{filterMembers.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
<PermissionTags permission={collaborator?.permission.value} />
{org.count && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
setParentPath(getOrgChildrenPath(org));
e.stopPropagation();
}}
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
<PermissionTags permission={collaborator?.permission.value} />
</HStack>
);
})}
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isChecked={selectedGroupIdList.includes(group._id)}
pointerEvents="none"
/>
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
<PermissionTags permission={collaborator?.permission.value} />
</HStack>
);
})}
</ScrollData>
)}
)}
</HStack>
);
})}
{filterMembers?.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
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 (
<MemberItemCard
avatar={member.avatar}
key={member.tmbId}
name={member.memberName}
permission={collaborator?.permission.value}
onChange={onChange}
isChecked={selectedMemberIdList.includes(member.tmbId)}
orgs={memberOrgNames}
/>
);
})}
{filterGroups?.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
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={selectedGroupIdList.includes(group._id)}
/>
);
})}
</ScrollData>
</Flex>
</Flex>
@@ -441,29 +447,27 @@ function MemberModal({
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<HStack
justifyContent="space-between"
<MemberItemCard
key={item.id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
>
<MyAvatar src={item.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{item.name}
</Box>
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={item.onDelete}
/>
</HStack>
avatar={item.avatar}
name={item.name ?? ''}
onChange={item.onDelete}
onDelete={item.onDelete}
orgs={(() => {
if (!item.id.startsWith('member-')) return [];
const id = item.id.replace('member-', '');
const memberOrgs = orgs.filter((org) =>
org.members.find((v) => v.tmbId === id)
);
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 memberOrgNames;
})()}
/>
);
})}
</Flex>

View File

@@ -4,34 +4,48 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateNotificationAccount } from '@/web/support/user/api';
import { updateContact, updateNotificationAccount } from '@/web/support/user/api';
import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type FormType = {
account: string;
contact: string;
verifyCode: string;
};
const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const UpdateContactModal = ({
onClose,
mode
}: {
onClose: () => void;
mode: 'contact' | 'notification_account';
}) => {
const { t } = useTranslation();
const { initUserInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const { register, handleSubmit, watch } = useForm<FormType>({
defaultValues: {
account: '',
contact: '',
verifyCode: ''
}
});
const account = watch('account');
const account = watch('contact');
const verifyCode = watch('verifyCode');
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
if (mode === 'contact') {
return updateContact(data);
} else {
return updateNotificationAccount({
account: data.contact,
verifyCode: data.verifyCode
});
}
},
{
onSuccess() {
@@ -62,7 +76,11 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:support.user.info.notification_receiving_hint')}
title={
mode === 'notification_account'
? t('common:support.user.info.notification_receiving_hint')
: t('account_info:contact')
}
>
<ModalBody px={10}>
<Flex flexDirection="column">
@@ -75,7 +93,7 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
{...register('contact', { required: true })}
placeholder={placeholder}
></Input>
</Flex>
@@ -108,4 +126,4 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
);
};
export default UpdateNotificationModal;
export default UpdateContactModal;

View File

@@ -0,0 +1,39 @@
import { Box, Flex, VStack } from '@chakra-ui/react';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Tag from '@fastgpt/web/components/common/Tag';
import React from 'react';
function OrgTags({ orgs, type = 'simple' }: { orgs: string[]; type?: 'simple' | 'tag' }) {
return (
<MyTooltip
label={
<VStack gap="1" alignItems={'start'}>
{orgs.map((org, index) => (
<Box key={index} fontSize="sm" fontWeight={400} color="myGray.500">
{org.slice(1)}
</Box>
))}
</VStack>
}
>
{type === 'simple' ? (
<Box fontSize="sm" fontWeight={400} w="full" color="myGray.500" whiteSpace={'nowrap'}>
{orgs
.map((org) => org.split('/').pop())
.join(', ')
.slice(0, 30)}
{orgs.length > 1 && '...'}
</Box>
) : (
<Flex direction="row" gap="1" p="2" alignItems={'start'} wrap={'wrap'}>
{orgs.map((org, index) => (
<Tag key={index}>{org.split('/').pop()}</Tag>
))}
</Flex>
)}
</MyTooltip>
);
}
export default OrgTags;

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}`
}
]

View File

@@ -49,9 +49,7 @@ const StandDetailModal = dynamic(
);
const ConversionModal = dynamic(() => import('@/pageComponents/account/info/ConversionModal'));
const UpdatePswModal = dynamic(() => import('@/pageComponents/account/info/UpdatePswModal'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);
const UpdateContact = dynamic(() => import('@/components/support/user/inform/UpdateContactModal'));
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const ModelPriceModal = dynamic(() =>
@@ -129,9 +127,9 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
onOpen: onOpenUpdatePsw
} = useDisclosure();
const {
isOpen: isOpenUpdateNotification,
onClose: onCloseUpdateNotification,
onOpen: onOpenUpdateNotification
isOpen: isOpenUpdateContact,
onClose: onCloseUpdateContact,
onOpen: onOpenUpdateContact
} = useDisclosure();
const {
File,
@@ -259,25 +257,14 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:notification_receiving')}:&nbsp;</Box>
<Box
flex={1}
{...(!userInfo?.team.notificationAccount && userInfo?.permission.isOwner
? { color: 'red.600' }
: {})}
>
{userInfo?.team.notificationAccount
? userInfo?.team.notificationAccount
: userInfo?.permission.isOwner
? t('account_info:please_bind_notification_receiving_path')
: t('account_info:reminder_create_bound_notification_account')}
<Box {...labelStyles}>{t('account_info:contact')}:&nbsp;</Box>
<Box flex={1} {...(!userInfo?.contact ? { color: 'red.600' } : {})}>
{userInfo?.contact ? userInfo?.contact : t('account_info:please_bind_contact')}
</Box>
{userInfo?.permission.isOwner && (
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateNotification}>
{t('account_info:change')}
</Button>
)}
<Button size={'sm'} variant={'whitePrimary'} onClick={onOpenUpdateContact}>
{t('account_info:change')}
</Button>
</Flex>
)}
{feConfigs.isPlus && (
@@ -310,7 +297,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
<ConversionModal onClose={onCloseConversionModal} onOpenContact={onOpenContact} />
)}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
{isOpenUpdateContact && <UpdateContact onClose={onCloseUpdateContact} mode="contact" />}
<File
onSelect={(e) =>
onSelectImage(e, {

View File

@@ -104,7 +104,8 @@ const Team = () => {
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
avatar: userInfo.team.avatar,
notificationAccount: userInfo.team.notificationAccount
});
}}
/>

View File

@@ -0,0 +1,29 @@
import { NextAPI } from '@/service/middleware/entry';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { NextApiRequest, NextApiResponse } from 'next';
/*
* 复制 Team 表中的 notificationAccount 到 User 表的 contact 中
*/
async function handler(req: NextApiRequest, _res: NextApiResponse) {
await authCert({ req, authRoot: true });
const users = await MongoUser.find();
const teams = await MongoTeam.find();
for await (const user of users) {
try {
const team = teams.find((team) => String(team.ownerId) === String(user._id));
if (team && !user.contact) {
user.contact = team.notificationAccount;
}
await user.save();
} catch (error) {
console.error(error);
}
}
return { success: true };
}
export default NextAPI(handler);

View File

@@ -7,7 +7,8 @@ import { UserType } from '@fastgpt/global/support/user/type.d';
import type {
FastLoginProps,
OauthLoginProps,
PostLoginProps
PostLoginProps,
SearchResult
} from '@fastgpt/global/support/user/api.d';
import {
AccountRegisterBody,
@@ -70,6 +71,10 @@ export const updatePasswordByOld = ({ oldPsw, newPsw }: { oldPsw: string; newPsw
export const updateNotificationAccount = (data: { account: string; verifyCode: string }) =>
PUT('/proApi/support/user/team/updateNotificationAccount', data);
export const updateContact = (data: { contact: string; verifyCode: string }) => {
return PUT('/proApi/support/user/account/updateContact', data);
};
export const postLogin = ({ password, ...props }: PostLoginProps) =>
POST<ResLogin>('/support/user/account/loginByPassword', {
...props,
@@ -92,3 +97,14 @@ export const getCaptchaPic = (username: string) =>
}>('/proApi/support/user/account/captcha/getImgCaptcha', { username });
export const postSyncMembers = () => POST('/proApi/support/user/team/org/sync');
export const GetSearchUserGroupOrg = (
searchKey: string,
options?: {
members?: boolean;
orgs?: boolean;
groups?: boolean;
}
) => GET<SearchResult>('/proApi/support/user/search', { searchKey, ...options });
export const ExportMembers = () => GET<{ csv: string }>('/proApi/support/user/team/member/export');

View File

@@ -9,6 +9,7 @@ import {
InviteMemberProps,
InviteMemberResponse,
UpdateInviteProps,
UpdateStatusProps,
UpdateTeamProps
} from '@fastgpt/global/support/user/team/controller.d';
import type { TeamTagItemType, TeamTagSchema } from '@fastgpt/global/support/user/team/type';
@@ -31,7 +32,7 @@ export const putSwitchTeam = (teamId: string) =>
PUT<string>(`/proApi/support/user/team/switch`, { teamId });
/* --------------- team member ---------------- */
export const getTeamMembers = (props: PaginationProps) =>
export const getTeamMembers = (props: PaginationProps<{ withLeaved?: boolean }>) =>
GET<PaginationResponse<TeamMemberItemType>>(`/proApi/support/user/team/member/list`, props);
export const postInviteTeamMember = (data: InviteMemberProps) =>
POST<InviteMemberResponse>(`/proApi/support/user/team/member/invite`, data);
@@ -41,6 +42,8 @@ export const delRemoveMember = (tmbId: string) =>
DELETE(`/proApi/support/user/team/member/delete`, { tmbId });
export const updateInviteResult = (data: UpdateInviteProps) =>
PUT('/proApi/support/user/team/member/updateInvite', data);
export const updateStatus = (data: UpdateStatusProps) =>
PUT('/proApi/support/user/team/member/updateStatus', data);
export const delLeaveTeam = () => DELETE('/proApi/support/user/team/member/leave');
/* -------------- team collaborator -------------------- */