V4.8.18 feature (#3565)

* feat: org CRUD (#3380)

* feat: add org schema

* feat: org manage UI

* feat: OrgInfoModal

* feat: org tree view

* feat: org management

* fix: init root org

* feat: org permission for app

* feat: org support for dataset

* fix: disable org role control

* styles: opt type signatures

* fix: remove unused permission

* feat: delete org collaborator

* perf: Team org ui (#3499)

* perf: org ui

* perf: org ui

* feat: org auth for app & dataset (#3498)

* feat: auth org resource permission

* feat: org auth support for app & dataset

* perf: org permission check (#3500)

* i18n (#3501)

* name

* i18n

* feat: support dataset changeOwner (#3483)

* feat: support dataset changeOwner

* chore: update dataset change owner api

* feat: permission manage UI for org (#3503)

* perf: password check;perf: image upload check;perf: sso login check (#3509)

* perf: password check

* perf: image upload check

* perf: sso login check

* force show update notification modal & fix login page text (#3512)

* fix login page English text

* update notification modal

* perf: notify account (#3515)

* perf(plugin): improve searXNG empty result handling and documentation (#3507)

* perf(plugin): improve searXNG empty result handling and documentation

* 修改了文档和代码部分无搜索的结果的反馈

* refactor: org pathId (#3516)

* optimize payment process (#3517)

* feat: support wecom sso (#3518)

* feat: support wecom sso

* chore: remove unused wecom js-sdk dependency

* fix qrcode script (#3520)

* fix qrcode script

* i18n

* perf: full text collection and search code;perf: rename function (#3519)

* perf: full text collection and search code

* perf: rename function

* perf: notify modal

* remove invalid code

* perf: sso login

* perf: pay process

* 4.8.18 test (#3524)

* perf: remove local token

* perf: index

* perf: file encoding;perf: leave team code;@c121914yu perf: full text search code (#3528)

* perf: text encoding

* perf: leave team code

* perf: full text search code

* fix: http status

* perf: embedding search and vector avatar

* perf: async read file (#3531)

* refactor: team permission  manager (#3535)

* perf: classify org, group and member

* refactor: team per manager

* fix: missing functions

* 4.8.18 test (#3543)

* perf: login check

* doc

* perf: llm model config

* perf: team clb config

* fix: MemberModal UI (#3553)

* fix: adapt MemberModal title and icon

* fix: adapt member modal

* fix: search input placeholder

* fix: add button text

* perf: org permission (#3556)

* docs:用户答疑的官方文档补充 (#3540)

* docs:用户答疑的官方文档补充

* 问题回答的内容修补

* share link random avatar (#3541)

* share link random avatar

* fix

* delete unused code

* share page avatar (#3558)

* feat: init 4818

* share page avatar

* feat: tmp upgrade code (#3559)

* feat: tmp upgrade code

* fulltext search test

* update action

* full text tmp code (#3561)

* full text tmp code

* fix: init

* fix: init

* remove tmp code

* remove tmp code

* 4818-alpha

* 4.8.18 test (#3562)

* full text tmp code

* fix: init

* upgrade code

* account log

* account log

* perf: dockerfile

* upgrade code

* chore: update docs app template submission (#3564)

---------

Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2025-01-11 15:15:38 +08:00
committed by GitHub
parent bb669ca3ff
commit 10d8c56e23
205 changed files with 5305 additions and 2428 deletions

View File

@@ -19,6 +19,9 @@ 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 pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
@@ -48,9 +51,9 @@ export const navbarWidth = '64px';
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { Loading } = useLoading();
const { loading, feConfigs, isNotSufficientModal } = useSystemStore();
const { loading, feConfigs, notSufficientModalType } = useSystemStore();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng();
const isChatPage = useMemo(
@@ -68,6 +71,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
const showUpdateNotification =
isUpdateNotification &&
!userInfo?.team.notificationAccount &&
!!userInfo?.team.permission.isOwner;
useMount(() => {
setUserDefaultLng();
});
@@ -113,8 +121,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{feConfigs?.isPlus && (
<>
{!!userInfo && <UpdateInviteModal />}
{isNotSufficientModal && <NotSufficientModal />}
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
{!!userInfo && <SystemMsgModal />}
{showUpdateNotification && (
<UpdateNotification onClose={() => setIsUpdateNotification(false)} />
)}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)}

View File

@@ -1,17 +1,13 @@
import React, { useCallback } from 'react';
import React from 'react';
import { ModalFooter, ModalBody, Input, Button, Box, Textarea, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal/index';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToast } from '@fastgpt/web/hooks/useToast';
export type EditResourceInfoFormType = {
id: string;
@@ -31,7 +27,6 @@ const EditResourceModal = ({
onEdit: (data: EditResourceInfoFormType) => any;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
defaultValues: defaultForm
});
@@ -46,31 +41,14 @@ const EditResourceModal = ({
}
);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.appAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
return (
<MyModal isOpen onClose={onClose} iconSrc={avatar} title={title}>
@@ -108,7 +86,15 @@ const EditResourceModal = ({
</Button>
</ModalFooter>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};

View File

@@ -7,7 +7,6 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import CollaboratorContextProvider, {
MemberManagerInputPropsType
} from '../../support/permission/MemberManager/context';
@@ -24,7 +23,6 @@ const FolderSlideCard = ({
deleteTip,
onDelete,
defaultPer,
managePer,
isInheritPermission,
resumeInheritPermission,
@@ -39,11 +37,6 @@ const FolderSlideCard = ({
deleteTip: string;
onDelete: () => void;
defaultPer?: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;

View File

@@ -104,14 +104,9 @@ const ChatBox = ({
showVoiceIcon = true,
showEmptyIntro = false,
active = true,
shareId,
outLinkUid,
teamId,
teamToken,
onStartChat
}: Props) => {
const ScrollContainerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
@@ -925,10 +920,6 @@ const ChatBox = ({
isLastChild={index === chatRecords.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
@@ -1004,17 +995,13 @@ const ChatBox = ({
onCloseUserLike,
onMark,
onReadUserDislike,
outLinkUid,
questionGuides,
retryInput,
shareId,
showEmpty,
showMarkIcon,
showVoiceIcon,
statusBoxData,
t,
teamId,
teamToken,
userAvatar,
variableList?.length,
welcomeText

View File

@@ -250,7 +250,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
<MyModal
isOpen={!!apiKey}
w={['400px', '600px']}
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
title={
<Box>
<Box fontWeight={'bold'}>{t('common:support.openapi.New api key')}</Box>
@@ -330,7 +330,7 @@ function EditKeyModal({
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
title={isEdit ? t('publish:edit_api_key') : t('publish:create_api_key')}
>
<ModalBody>

View File

@@ -44,7 +44,7 @@ const ConfigPerModal = ({
<>
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
onClose={onClose}
title={t('common:permission.Permission config')}
>

View File

@@ -1,308 +0,0 @@
import {
Flex,
Box,
ModalBody,
Checkbox,
ModalFooter,
Button,
Grid,
HStack
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { useMemo, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
export type AddModalPropsType = {
onClose: () => void;
mode?: 'member' | 'all';
};
function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
});
}, [members, searchText, userInfo?.team?.tmbId]);
const filterGroups = useMemo(() => {
if (mode !== 'all') return [];
return groups.filter((item) => {
if (permission.isOwner) return true; // owner can see all groups
if (myGroups.find((i) => String(i._id) === String(item._id))) return false;
if (!searchText) return true;
return item.name.includes(searchText);
});
}, [groups, searchText, myGroups, mode, permission]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPerLabelList(selectedPermission).join('、');
}, [getPerLabelList, selectedPermission]);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
permission: selectedPermission
}),
{
successToast: t('common:common.Add Success'),
errorToast: 'Error',
onSuccess() {
onClose();
}
}
);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="modal/AddClb"
title={t('user:team.add_collaborator')}
minW="800px"
h={'100%'}
isCentered
isLoading={loadingMembersAndGroups}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" borderRight="1px solid" borderColor="myGray.200" p="4">
<SearchInput
placeholder={t('user:search_user')}
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{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={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(group._id)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox isChecked={selectedGroupIdList.includes(group._id)} />
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</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={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedMemberIdList.includes(member.tmbId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
</Flex>
</Flex>
<Flex p="4" flexDirection="column">
<Box>
{t('user:has_chosen') + ': '}{' '}
{selectedMemberIdList.length + selectedGroupIdList.length}
</Box>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(groupId)) {
return state.filter((v) => v !== groupId);
}
return [...state, groupId];
});
};
const group = groups.find((v) => String(v._id) === groupId);
return (
<HStack
justifyContent="space-between"
key={groupId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(groupId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<MyAvatar src={group?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{group?.name === DefaultGroupName ? userInfo?.team.teamName : group?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedMemberIdList.map((tmbId) => {
const member = filterMembers.find((v) => v.tmbId === tmbId);
return member ? (
<HStack
justifyContent="space-between"
key={tmbId}
alignItems="center"
py="2"
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.50' }}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius="50%" />
<Box w="full" ml={2}>
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
) : null;
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{t(perLabel as any)}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default AddMemberModal;

View File

@@ -1,19 +1,19 @@
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = {
onClose: () => void;
};
@@ -65,7 +65,7 @@ function ManageModal({ onClose }: ManageModalProps) {
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
@@ -85,14 +85,20 @@ function ManageModal({ onClose }: ManageModalProps) {
onUpdate({
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
orgs: item.orgId ? [item.orgId] : undefined,
permission
});
}}
onDelete={() => {
onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
groupId: item.groupId,
orgId: item.orgId
} as RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>);
}}
/>
)}

View File

@@ -1,13 +1,13 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, type BoxProps, Flex } from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Tag, { type TagProps } from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
@@ -31,12 +31,12 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
{collaboratorList?.map((member) => {
return (
<Tag
key={member.tmbId || member.groupId}
key={member.tmbId || member.groupId || member.orgId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" />
<Avatar src={member.avatar} w="1.25rem" rounded={'50%'} />
<Box fontSize={'sm'} ml={1}>
{member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box>

View File

@@ -0,0 +1,512 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter,
Tag,
Text
} from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useRef, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import {
DEFAULT_ORG_AVATAR,
DEFAULT_TEAM_AVATAR,
DEFAULT_USER_AVATAR
} from '@fastgpt/global/common/system/constants';
import Path from '@/components/common/folder/Path';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
const HoverBoxStyle = {
bgColor: 'myGray.50',
cursor: 'pointer'
};
function MemberModal({
onClose,
addPermissionOnly: addOnly = false
}: {
onClose: () => void;
addPermissionOnly?: boolean;
}) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, loadAndGetOrgs } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return Promise.all([
loadAndGetTeamMembers(true),
loadAndGetGroups(true),
loadAndGetOrgs(true)
]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const [parentPath, setParentPath] = useState('');
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
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 && filterClass !== 'org') return [];
if (parentPath === '') {
setParentPath(`/${orgs[0].pathId}`);
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => ({
...item,
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
}));
}, [orgs, searchText, filterClass, parentPath]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const filterMembers = useMemo(() => {
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
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]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const filterGroups = useMemo(() => {
if (searchText) return groups.filter((item) => item.name.includes(searchText));
if (!searchText && filterClass !== 'group') return [];
return groups;
}, [groups, searchText, filterClass]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
permissionList?.read?.value
);
const perLabel = useMemo(() => {
if (selectedPermission === undefined) return '';
return getPerLabelList(selectedPermission!).join('、');
}, [getPerLabelList, selectedPermission]);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(v) => v.onUpdateCollaborators
);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission!
}),
{
successToast: t('common:common.Add Success'),
onSuccess() {
onClose();
}
}
);
const entryList = useRef([
{ label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' },
{ label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' },
{ label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' }
]);
const selectedList = useMemo(() => {
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
return [
...selectedOrgs.map((item) => ({
id: `org-${item._id}`,
avatar: item.avatar,
name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id))
})),
...selectedGroups.map((item) => ({
id: `group-${item._id}`,
avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id))
})),
...selectedMembers.map((item) => ({
id: `member-${item.tmbId}`,
avatar: item.avatar,
name: item.memberName,
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId))
}))
];
}, [
orgs,
groups,
members,
selectedOrgIdList,
selectedGroupIdList,
selectedMemberIdList,
userInfo?.team.teamName
]);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
minW="800px"
h={'100%'}
maxH={'90vh'}
isCentered
isLoading={loadingMembersAndGroups}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
>
<SearchInput
placeholder={t('user:search_group_org_user')}
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{!searchText && !filterClass && (
<>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
);
})}
</>
)}
{/* Path */}
{!searchText && filterClass && (
<Box mb={1}>
<Path
paths={[
{
parentId: filterClass,
parentName:
filterClass === 'member'
? t('user:team.group.members')
: filterClass === 'org'
? t('user:team.org.org')
: t('user:team.group.group')
},
...paths
]}
onClick={(parentId) => {
if (parentId === '') {
setFilterClass(undefined);
setParentPath('');
} else if (
parentId === 'member' ||
parentId === 'org' ||
parentId === 'group'
) {
setFilterClass(parentId);
setParentPath('');
} else {
setParentPath(parentId);
}
}}
rootName={t('common:common.Team')}
/>
</Box>
)}
<Flex flexDirection={'column'} gap={1} userSelect={'none'}>
{filterMembers.map((member) => {
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
<PermissionTags
permission={addOnly ? undefined : collaborator?.permission.value}
/>
</HStack>
);
})}
{filterOrgs.map((org) => {
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || 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={addOnly ? undefined : collaborator?.permission.value}
/>
{org.count && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
e.stopPropagation();
setParentPath(getOrgChildrenPath(org));
}}
/>
)}
</HStack>
);
})}
{filterGroups.map((group) => {
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || 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={addOnly ? undefined : collaborator?.permission.value}
/>
</HStack>
);
})}
</Flex>
</Flex>
</Flex>
<Flex h={'100%'} p="4" flexDirection="column">
<Box>
{`${t('user:has_chosen')}: `}
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
</Box>
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<HStack
justifyContent="space-between"
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>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
{!addOnly && !!permissionList && (
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{t(perLabel as any)}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
)}
{addOnly && (
<HStack bg={'blue.50'} color={'blue.600'} padding={'6px 12px'} rounded={'5px'}>
<MyIcon name="common/info" w="1rem" h="1rem" />
<Text fontSize="12px">{t('user:permission_add_tip')}</Text>
</HStack>
)}
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default MemberModal;

View File

@@ -49,13 +49,16 @@ function PermissionSelect({
onDelete
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLButtonElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
if (!permissionList) return { singleCheckBoxList: [], multipleCheckBoxList: [] };
const list = Object.entries(permissionList).map(([_, value]) => {
return {
name: value.name,
@@ -77,6 +80,8 @@ function PermissionSelect({
};
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
if (!permissionList) return undefined;
const per = new Permission({ per: value });
if (per.hasManagePer) return permissionList['manage'].value;
@@ -107,7 +112,7 @@ function PermissionSelect({
}
});
return (
return selectedSingleValue !== undefined ? (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
w="fit-content"
@@ -241,7 +246,7 @@ function PermissionSelect({
</MenuList>
</Box>
</Menu>
);
) : null;
}
export default React.memo(PermissionSelect);

View File

@@ -7,12 +7,15 @@ import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next';
export type PermissionTagsProp = {
permission: PermissionValueType;
permission?: PermissionValueType;
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const { t } = useTranslation();
if (permission === undefined) return null;
const perTagList = getPerLabelList(permission);
return (

View File

@@ -1,32 +1,37 @@
import { useDisclosure } from '@chakra-ui/react';
import {
import type {
CollaboratorItemType,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReactNode, useCallback } from 'react';
import type {
PermissionListType,
PermissionValueType
} from '@fastgpt/global/support/permission/type';
import { type ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
import MemberListCard, { type MemberListCardProps } from './MemberListCard';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const MemberModal = dynamic(() => import('./MemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
permissionList?: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
onDelOneCollaborator: (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
@@ -46,19 +51,19 @@ type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
onUpdateCollaborators: () => {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
onDelOneCollaborator: () => {
throw new Error('Function not implemented.');
},
getPerLabelList: function (): string[] {
getPerLabelList: (): string[] => {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
refetchCollaboratorList: (): void => {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
onGetCollaboratorList: (): Promise<CollaboratorItemType[]> => {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false,
@@ -76,19 +81,20 @@ const CollaboratorContextProvider = ({
refreshDeps = [],
isInheritPermission,
hasParent,
mode = 'member'
addPermissionOnly
}: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void;
isInheritPermission?: boolean;
hasParent?: boolean;
addPermissionOnly?: boolean;
}) => {
const onUpdateCollaboratorsThen = async (props: UpdateClbPermissionProps) => {
await onUpdateCollaborators(props);
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string }>
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList();
@@ -116,6 +122,8 @@ const CollaboratorContextProvider = ({
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
if (!permissionList) return [];
const Per = new Permission({ per });
const labels: string[] = [];
@@ -123,7 +131,7 @@ const CollaboratorContextProvider = ({
labels.push(permissionList['manage'].name);
} else if (Per.hasWritePer) {
labels.push(permissionList['write'].name);
} else {
} else if (Per.hasReadPer) {
labels.push(permissionList['read'].name);
}
@@ -198,12 +206,12 @@ const CollaboratorContextProvider = ({
MemberListCard
})}
{isOpenAddMember && (
<AddMemberModal
<MemberModal
onClose={() => {
onCloseAddMember();
refetchResource?.();
}}
mode={mode}
addPermissionOnly={addPermissionOnly}
/>
)}
{isOpenManageModal && (

View File

@@ -38,8 +38,8 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
initUserInfo();
onClose();
},
successToast: t('account_info:bind_notification_success'),
errorToast: t('account_info:bind_notification_error')
successToast: t('common:support.user.info.bind_notification_success'),
errorToast: t('common:support.user.info.bind_notification_error')
}
);
@@ -49,9 +49,9 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
?.map((item) => {
switch (item) {
case 'email':
return t('account_info:email_label');
return t('common:support.user.login.Email');
case 'phone':
return t('account_info:phone_label');
return t('common:support.user.login.Phone number');
}
})
.join('/');
@@ -62,16 +62,16 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('account_info:notification_receiving_hint')}
title={t('common:support.user.info.notification_receiving_hint')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('account_info:bind_notification_hint')}</Box>
<Box fontSize={'sm'}>{t('common:support.user.info.bind_notification_hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('account_info:user_account')}</Box>
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
@@ -80,12 +80,12 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('account_info:verification_code')}</Box>
<Box flex={'0 0 70px'}>{t('common:support.user.info.verification_code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('account_info:code_required')}
placeholder={t('common:support.user.info.code_required')}
></Input>
<SendCodeBox username={account} />
</Flex>
@@ -93,14 +93,14 @@ const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
{t('common:common.Cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('account_info:confirm')}
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -1,35 +1,153 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Button, Flex, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { NotSufficientModalType, useSystemStore } from '@/web/common/system/useSystemStore';
import ExtraPlan from '@/pages/price/components/ExtraPlan';
import StandardPlan from '@/pages/price/components/Standard';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useUserStore } from '@/web/support/user/useUserStore';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useMount } from 'ahooks';
const NotSufficientModal = () => {
const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
const { t } = useTranslation();
const router = useRouter();
const { setIsNotSufficientModal } = useSystemStore();
const { setNotSufficientModalType } = useSystemStore();
const onClose = () => setIsNotSufficientModal(false);
const onClose = () => setNotSufficientModalType(undefined);
const {
isOpen: isRechargeModalOpen,
onOpen: onRechargeModalOpen,
onClose: onRechargeModalClose
} = useDisclosure();
const textMap = {
[TeamErrEnum.aiPointsNotEnough]: t('common:support.wallet.Not sufficient'),
[TeamErrEnum.datasetSizeNotEnough]: t('common:support.wallet.Dataset_not_sufficient'),
[TeamErrEnum.datasetAmountNotEnough]: t('common:support.wallet.Dataset_amount_not_sufficient'),
[TeamErrEnum.teamMemberOverSize]: t('common:support.wallet.Team_member_over_size'),
[TeamErrEnum.appAmountNotEnough]: t('common:support.wallet.App_amount_not_sufficient')
};
return (
<MyModal isOpen iconSrc="common/confirm/deleteTip" title={t('common:common.Warning')}>
<ModalBody>{t('common:support.wallet.Not sufficient')}</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
onClick={() => {
router.push('/account/info');
onClose();
}}
>
{t('common:support.wallet.To read plan')}
</Button>
</ModalFooter>
</MyModal>
<>
<MyModal
isOpen
iconSrc="common/confirm/deleteTip"
title={t('common:common.Warning')}
w={'420px'}
>
<ModalBody>{textMap[type]}</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
onClick={() => {
onRechargeModalOpen();
}}
>
{t('common:support.wallet.To read plan')}
</Button>
</ModalFooter>
</MyModal>
{isRechargeModalOpen && (
<RechargeModal onClose={onRechargeModalClose} onPaySuccess={onClose} />
)}
</>
);
};
export default NotSufficientModal;
const RechargeModal = ({
onClose,
onPaySuccess
}: {
onClose: () => void;
onPaySuccess: () => void;
}) => {
const { t } = useTranslation();
const { teamPlanStatus, initTeamPlanStatus } = useUserStore();
useMount(() => {
initTeamPlanStatus();
});
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
return standardSubLevelMap[teamPlanStatus.standard.currentSubLevel].label;
}, [teamPlanStatus?.standard?.currentSubLevel]);
const [tab, setTab] = useState<'standard' | 'extra'>('standard');
return (
<MyModal
isOpen
iconSrc="common/wallet"
iconColor={'primary.600'}
title={t('common:user.Pay')}
onClose={onClose}
isCentered
minW={'90%'}
maxH={'90%'}
>
<ModalBody px={'52px'}>
<Flex alignItems={'center'} mb={6}>
<FormLabel fontSize={'16px'} fontWeight={'medium'}>
{t('common:support.wallet.subscription.Current plan')}
</FormLabel>
<Box fontSize={'14px'} ml={5} color={'myGray.900'}>
{t(planName as any)}
</Box>
</Flex>
<Flex alignItems={'center'} mb={6}>
<FormLabel fontSize={'16px'} fontWeight={'medium'}>
{t('common:info.resource')}
</FormLabel>
<Flex fontSize={'14px'} ml={5} color={'myGray.900'}>
<Box>{`${t('common:support.user.team.Dataset usage')}:`}</Box>
<Box
ml={2}
>{`${teamPlanStatus?.usedDatasetSize} / ${teamPlanStatus?.datasetMaxSize || t('account_info:unlimited')}`}</Box>
<Box ml={5}>{`${t('common:support.wallet.subscription.AI points usage')}:`}</Box>
<Box
ml={2}
>{`${Math.round(teamPlanStatus?.usedPoints || 0)} / ${teamPlanStatus?.totalPoints || t('account_info:unlimited')}`}</Box>
</Flex>
</Flex>
<FillRowTabs
list={[
{ label: t('common:support.wallet.subscription.Sub plan'), value: 'standard' },
{ label: t('common:support.wallet.subscription.Extra plan'), value: 'extra' }
]}
value={tab}
onChange={(e) => {
setTab(e as 'standard' | 'extra');
}}
/>
<Box
mt={3}
p={8}
bg={'myGray.50'}
border={'1px solid'}
borderColor={'myGray.200'}
rounded={'12px'}
>
{tab === 'standard' ? (
<StandardPlan standardPlan={teamPlanStatus?.standard} onPaySuccess={onPaySuccess} />
) : (
<ExtraPlan onPaySuccess={onPaySuccess} />
)}
</Box>
</ModalBody>
</MyModal>
);
};

View File

@@ -1,5 +1,5 @@
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, ModalBody } from '@chakra-ui/react';
import { checkBalancePayResult } from '@/web/support/wallet/bill/api';
@@ -7,6 +7,8 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { getErrText } from '@fastgpt/global/common/error/utils';
import LightTip from '@fastgpt/web/components/common/LightTip';
import Script from 'next/script';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
export type QRPayProps = {
readPrice: number;
@@ -23,25 +25,25 @@ const QRCodePayModal = ({
billId,
onSuccess
}: QRPayProps & { tip?: string; onSuccess?: () => any }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const dom = useRef<HTMLDivElement>(null);
const drawCode = useCallback(() => {
if (dom.current && window.QRCode && !dom.current.innerHTML) {
new window.QRCode(dom.current, {
text: codeUrl,
width: qrCodeSize,
height: qrCodeSize,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
}, [codeUrl]);
useEffect(() => {
let timer: NodeJS.Timeout;
const drawCode = () => {
if (dom.current && window.QRCode && !dom.current.innerHTML) {
new window.QRCode(dom.current, {
text: codeUrl,
width: qrCodeSize,
height: qrCodeSize,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
};
const check = async () => {
try {
const res = await checkBalancePayResult(billId);
@@ -52,9 +54,6 @@ const QRCodePayModal = ({
title: res,
status: 'success'
});
setTimeout(() => {
router.reload();
}, 1000);
return;
} catch (error) {
toast({
@@ -73,18 +72,26 @@ const QRCodePayModal = ({
check();
return () => clearTimeout(timer);
}, [billId, onSuccess, toast]);
}, [billId, drawCode, onSuccess, toast]);
return (
<MyModal isOpen title={t('common:user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'} pb={10} whiteSpace={'pre-wrap'}>
{tip && <LightTip text={tip} mb={8} textAlign={'left'} />}
<Box ref={dom} id={'payQRCode'} display={'inline-block'} h={`${qrCodeSize}px`}></Box>
<Box mt={5} textAlign={'center'}>
{t('common:pay.wechat', { price: readPrice })}
</Box>
</ModalBody>
</MyModal>
<>
<Script
src={getWebReqUrl('/js/qrcode.min.js')}
strategy="lazyOnload"
onLoad={drawCode}
></Script>
<MyModal isOpen title={t('common:user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'} pb={10} whiteSpace={'pre-wrap'}>
{tip && <LightTip text={tip} mb={8} textAlign={'left'} />}
<Box ref={dom} id={'payQRCode'} display={'inline-block'} h={`${qrCodeSize}px`}></Box>
<Box mt={5} textAlign={'center'}>
{t('common:pay.wechat', { price: readPrice })}
</Box>
</ModalBody>
</MyModal>
</>
);
};

View File

@@ -94,7 +94,7 @@ const AccountContainer = ({
...(userInfo?.team?.permission.hasManagePer
? [
{
icon: 'support/outlink/apikeyLight',
icon: 'key',
label: t('account:api_key'),
value: TabEnum.apikey
}

View File

@@ -75,7 +75,7 @@ const TeamSelector = ({
key={'manage'}
alignItems={'center'}
borderRadius={'md'}
cursor={'default'}
cursor={'pointer'}
gap={3}
onClick={() => router.push('/account/team')}
>

View File

@@ -3,8 +3,10 @@ import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/rea
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updatePasswordByOld } from '@/web/support/user/api';
import { PasswordRule } from '@/web/support/user/login/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
type FormType = {
oldPsw: string;
@@ -14,7 +16,9 @@ type FormType = {
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { register, handleSubmit } = useForm<FormType>({
const { toast } = useToast();
const { register, handleSubmit, getValues } = useForm<FormType>({
defaultValues: {
oldPsw: '',
newPsw: '',
@@ -22,19 +26,25 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
}
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: (data: FormType) => {
if (data.newPsw !== data.confirmPsw) {
return Promise.reject(t('account_info:password_mismatch'));
}
return updatePasswordByOld(data);
},
const { runAsync: onSubmit, loading: isLoading } = useRequest2(updatePasswordByOld, {
onSuccess() {
onClose();
},
successToast: t('account_info:password_update_success'),
errorToast: t('account_info:password_update_error')
});
const onSubmitErr = (err: Record<string, any>) => {
const val = Object.values(err)[0];
if (!val) return;
if (val.message) {
toast({
status: 'warning',
title: val.message,
duration: 3000,
isClosable: true
});
}
};
return (
<MyModal
@@ -45,34 +55,39 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 70px'}>{t('account_info:old_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:old_password') + ':'}
</Box>
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:new_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:new_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('account_info:password_tip')}
{...register('newPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
pattern: {
value: PasswordRule,
message: t('account_info:password_tip')
}
})}
></Input>
</Flex>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 70px'}>{t('account_info:confirm_password') + ':'}</Box>
<Box flex={'0 0 70px'} fontSize={'sm'}>
{t('account_info:confirm_password') + ':'}
</Box>
<Input
flex={1}
type={'password'}
placeholder={t('user:password.confirm')}
{...register('confirmPsw', {
required: true,
maxLength: {
value: 60,
message: t('account_info:password_length_error')
}
validate: (val) => (getValues('newPsw') === val ? true : t('user:password.not_match'))
})}
></Input>
</Flex>
@@ -81,7 +96,7 @@ const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data), onSubmitErr)}>
{t('account_info:confirm')}
</Button>
</ModalFooter>

View File

@@ -9,8 +9,7 @@ import {
Link,
Progress,
Grid,
BoxProps,
FlexProps
BoxProps
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
@@ -20,7 +19,6 @@ import type { UserType } from '@fastgpt/global/support/user/type.d';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
@@ -29,7 +27,6 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { formatStorePrice2Read } from '@fastgpt/global/support/wallet/usage/tools';
import { putUpdateMemberName } from '@/web/support/user/team/api';
import { getDocPath } from '@/web/common/system/doc';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import {
StandardSubLevelEnum,
standardSubLevelMap
@@ -49,7 +46,9 @@ import TeamSelector from '../components/TeamSelector';
const StandDetailModal = dynamic(() => import('./components/standardDetailModal'), { ssr: false });
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const ModelPriceModal = dynamic(() =>
@@ -131,7 +130,11 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
onClose: onCloseUpdateNotification,
onOpen: onOpenUpdateNotification
} = useDisclosure();
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
@@ -151,32 +154,6 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
[reset, t, toast, updateUserInfo]
);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file || !userInfo) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.userAvatar,
file,
maxW: 300,
maxH: 300
});
onclickSave({
...userInfo,
avatar: src
});
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : t('account_info:avatar_selection_exception'),
status: 'warning'
});
}
},
[onclickSave, t, toast, userInfo]
);
const labelStyles: BoxProps = {
flex: '0 0 80px',
fontSize: 'sm',
@@ -329,7 +306,21 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxW: 300,
maxH: 300,
callback: (src) => {
if (!userInfo) return;
onclickSave({
...userInfo,
avatar: src
});
}
})
}
/>
</Box>
);
};

View File

@@ -0,0 +1,84 @@
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Flex, ModalBody } from '@chakra-ui/react';
import { MultipleRowArraySelect } from '@fastgpt/web/components/common/MySelect/MultipleRowSelect';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { ModelProviderList } from '@fastgpt/global/core/ai/provider';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { HUGGING_FACE_ICON } from '@fastgpt/global/common/system/constants';
import { getModelFromList } from '@fastgpt/global/core/ai/model';
const DefaultModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList } =
useSystemStore();
const [value, setValue] = useState<string[]>([]);
const modelList = useMemo(() => {
return [
...llmModelList,
...vectorModelList,
...audioSpeechModelList,
...reRankModelList,
whisperModel
].map((item) => ({
provider: item.provider,
name: item.name,
model: item.model
}));
}, [llmModelList, vectorModelList, whisperModel, audioSpeechModelList, reRankModelList]);
const selectorList = useMemo(() => {
const renderList = ModelProviderList.map<{
label: React.JSX.Element;
value: string;
children: { label: string | React.ReactNode; value: string }[];
}>((provider) => ({
label: (
<Flex alignItems={'center'} py={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={provider?.avatar || HUGGING_FACE_ICON}
fallbackSrc={HUGGING_FACE_ICON}
w={'1rem'}
/>
<Box>{t(provider.name as any)}</Box>
</Flex>
),
value: provider.id,
children: []
}));
for (const item of modelList) {
const modelData = getModelFromList(modelList, item.model);
const provider =
renderList.find((item) => item.value === (modelData?.provider || 'Other')) ??
renderList[renderList.length - 1];
provider.children.push({
label: modelData.name,
value: modelData.model
});
}
return renderList.filter((item) => item.children.length > 0);
}, [modelList, t]);
console.log(selectorList);
return (
<MyModal
isOpen
title={t('account:add_default_model')}
iconSrc="common/model"
iconColor="primary.600"
onClose={onClose}
>
<ModalBody>11</ModalBody>
</MyModal>
);
};
export default DefaultModal;

View File

@@ -1,15 +1,72 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import React from 'react';
import React, { useState } from 'react';
import AccountContainer from '../components/AccountContainer';
import { Box } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import ModelTable from '@/components/core/ai/ModelTable';
import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
const DefaultModal = dynamic(() => import('./components/DefaultModal'), {
ssr: false
});
const ModelProvider = () => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const [tab, setTab] = useState<'model' | 'channel'>('model');
const { isOpen: isOpenDefault, onOpen: onOpenDefault, onClose: onCloseDefault } = useDisclosure();
return (
<AccountContainer>
<Box h={'100%'} py={4} px={6}>
<ModelTable />
</Box>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{/* Header */}
{/* <Flex justifyContent={'space-between'}>
<FillRowTabs<'model' | 'channel'>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:channel'), value: 'channel' }
]}
value={tab}
px={8}
py={1}
onChange={setTab}
/>
{tab === 'model' && (
<MyMenu
trigger="hover"
size="mini"
Button={<Button>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('account:default_model'),
onClick: onOpenDefault
},
{
label: t('account:custom_model')
}
]
}
]}
/>
)}
{tab === 'channel' && <Button>{t('account:create_channel')}</Button>}
</Flex> */}
<Box flex={'1 0 0'}>
{tab === 'model' && <ModelTable />}
{/* {tab === 'channel' && <ChannelTable />} */}
</Box>
</Flex>
{isOpenDefault && <DefaultModal onClose={onCloseDefault} />}
</AccountContainer>
);
};

View File

@@ -1,10 +1,8 @@
import React, { useCallback } from 'react';
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
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';
@@ -12,7 +10,6 @@ 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 { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { DEFAULT_TEAM_AVATAR } from '@fastgpt/global/common/system/constants';
export type EditTeamFormDataType = CreateTeamProps & {
@@ -41,33 +38,15 @@ function EditModal({
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png,.svg',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.teamAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (data: CreateTeamProps) => {
return postCreateTeam(data);
@@ -154,7 +133,15 @@ function EditModal({
</Button>
)}
</ModalFooter>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
}

View File

@@ -6,9 +6,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useTranslation } from 'next-i18next';
import React, { useMemo } from 'react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
@@ -23,7 +21,11 @@ export type GroupFormType = {
function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGroupId?: string }) {
const { refetchGroups, groups, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const { t } = useTranslation();
const { File: AvatarSelect, onOpen: onOpenSelectAvatar } = useSelectFile({
const {
File: AvatarSelect,
onOpen: onOpenSelectAvatar,
onSelectImage
} = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
@@ -41,13 +43,10 @@ function GroupInfoModal({ onClose, editGroupId }: { onClose: () => void; editGro
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.groupAvatar,
file: file[0],
return onSelectImage(file, {
maxW: 300,
maxH: 300
});
return src;
},
{
onSuccess: (src: string) => {

View File

@@ -1,6 +1,8 @@
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
@@ -26,19 +28,35 @@ import MemberTag from '../../../../../components/support/user/team/Info/MemberTa
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useState } from 'react';
import IconButton from '../OrgManage/IconButton';
const ChangeOwnerModal = dynamic(() => import('./GroupTransferOwnerModal'));
const GroupInfoModal = dynamic(() => import('./GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./GroupManageMember'));
function MemberTable({
onEditGroup,
onManageMember
}: {
onEditGroup: (groupId: string) => void;
onManageMember: (groupId: string) => void;
}) {
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const [editGroupId, setEditGroupId] = useState<string>();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
const { ConfirmModal: ConfirmDeleteGroupModal, openConfirm: openDeleteGroupModal } = useConfirm({
type: 'delete',
@@ -72,145 +90,184 @@ function MemberTable({
onOpen: onOpenChangeOwner,
onClose: onCloseChangeOwner
} = useDisclosure();
const onChangeOwner = (groupId: string) => {
setEditGroupId(groupId);
onOpenChangeOwner();
};
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('account_team:group_name')}
</Th>
<Th bg="myGray.100">{t('account_team:owner')}</Th>
<Th bg="myGray.100">{t('account_team:member')}</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{groups?.map((group) => (
<Tr key={group._id} overflow={'unset'}>
<Td>
<HStack>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack>
</Td>
<Td>
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find(
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find(
(i) =>
i.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
}
avatar={group.avatar}
/>
<Box>
({group.name === DefaultGroupName ? members.length : group.members.length})
</Box>
</HStack>
</Td>
<Td>
<MemberTag
name={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.memberName ?? ''
: members.find(
(item) =>
item.tmbId ===
group.members.find((item) => item.role === 'owner')?.tmbId
)?.memberName ?? ''
}
avatar={
group.name === DefaultGroupName
? members.find((item) => item.role === 'owner')?.avatar ?? ''
: members.find(
(i) =>
i.tmbId === group.members.find((item) => item.role === 'owner')?.tmbId
)?.avatar ?? ''
}
/>
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>
<Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
<MyMenu
Button={<MyIcon name={'edit'} cursor={'pointer'} w="1rem" />}
menuList={[
{
children: [
{
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
</Td>
<Td>
{group.name === DefaultGroupName ? (
<AvatarGroup avatars={members.map((v) => v.avatar)} groupId={group._id} />
) : hasGroupManagePer(group) ? (
<MyTooltip label={t('account_team:manage_member')}>
<Box cursor="pointer" onClick={() => onManageMember(group._id)}>
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
</Box>
</MyTooltip>
) : (
<AvatarGroup
avatars={group.members.map(
(v) => members.find((m) => m.tmbId === v.tmbId)?.avatar ?? ''
)}
groupId={group._id}
/>
)}
</Td>
<Td>
{hasGroupManagePer(group) && group.name !== DefaultGroupName && (
<MyMenu
Button={<IconButton name={'more'} />}
menuList={[
{
children: [
{
label: t('account_team:edit_info'),
icon: 'edit',
onClick: () => {
onEditGroup(group._id);
}
},
{
label: t('account_team:manage_member'),
icon: 'support/team/group',
onClick: () => {
onManageMember(group._id);
}
},
...(isGroupOwner(group)
? [
{
label: t('account_team:transfer_ownership'),
icon: 'modal/changePer',
onClick: () => {
onChangeOwner(group._id);
},
type: 'primary' as MenuItemType
},
type: 'primary' as MenuItemType
},
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
openDeleteGroupModal(() => delDeleteGroup(group._id))();
},
type: 'danger' as MenuItemType
}
]
: [])
]
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</MyBox>
<ConfirmDeleteGroupModal />
{isOpenChangeOwner && editGroupId && (
<ChangeOwnerModal groupId={editGroupId} onClose={onCloseChangeOwner} />
)}
</MyBox>
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
</>
);
}

View File

@@ -1,106 +1,224 @@
import Avatar from '@fastgpt/web/components/common/Avatar';
import { Box, HStack, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
Box,
Button,
Flex,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
useDisclosure
} from '@chakra-ui/react';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { delRemoveMember } 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';
import MyIcon from '@fastgpt/web/components/common/Icon';
import dynamic from 'next/dynamic';
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';
function MemberTable() {
const { userInfo } = useUserStore();
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { toast } = useToast();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs, setNotSufficientModalType } = useSystemStore();
const { groups, refetchGroups, myTeams, refetchTeams, members, refetchMembers, onSwitchTeam } =
useContextSelector(TeamContext, (v) => v);
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({
type: 'delete'
});
const { members, groups, refetchMembers, refetchGroups } = useContextSelector(
TeamContext,
(v) => v
const { runAsync: onLeaveTeam } = useRequest2(
async () => {
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam();
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
return (
<MyBox>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</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>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<HStack>
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('account_team:label_sync')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('common:user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
setNotSufficientModalType(TeamErrEnum.teamMemberOverSize);
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
onClick={() => openLeaveConfirm(onLeaveTeam)()}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
</HStack>
</Flex>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
<Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}>
<Table overflow={'unset'}>
<Thead>
<Tr bgColor={'white !important'}>
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</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>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</Box>
<ConfirmLeaveTeamModal />
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
</>
);
}

View File

@@ -0,0 +1,31 @@
import { IconProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
function IconButton({
name,
w = '1rem',
h = '1rem',
...props
}: {
name: IconNameType;
} & IconProps) {
return (
<MyIcon
name={name}
w={w}
h={h}
transition={'background 0.1s'}
cursor={'pointer'}
p="1"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
{...props}
/>
);
}
export default IconButton;

View File

@@ -0,0 +1,162 @@
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { postCreateOrg, putUpdateOrg } from '@/web/support/user/team/org/api';
import { Button, HStack, Input, ModalBody, ModalFooter, Textarea } from '@chakra-ui/react';
import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants';
import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useForm } from 'react-hook-form';
export type OrgFormType = {
_id: string;
avatar: string;
description?: string;
name: string;
path: string;
parentId?: string;
};
export const defaultOrgForm: OrgFormType = {
_id: '',
avatar: '',
description: '',
name: '',
path: ''
};
function OrgInfoModal({
editOrg,
onClose,
onSuccess
}: {
editOrg: OrgFormType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const isEdit = !!editOrg._id;
const { register, handleSubmit, setValue, watch } = useForm<OrgFormType>({
defaultValues: {
name: editOrg.name,
avatar: editOrg.avatar,
description: editOrg.description
}
});
const avatar = watch('avatar');
const { run: onCreate, loading: isLoadingCreate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg.parentId) return;
return postCreateOrg({
name: data.name,
avatar: data.avatar,
parentId: editOrg.parentId,
description: data.description
});
},
{
successToast: t('common:common.Create Success'),
onSuccess: () => {
onClose();
onSuccess();
}
}
);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
async (data: OrgFormType) => {
if (!editOrg._id) return;
return putUpdateOrg({
orgId: editOrg._id,
name: data.name,
avatar: data.avatar,
description: data.description
});
},
{
successToast: t('common:common.Update Success'),
onSuccess: () => {
onClose();
onSuccess();
}
}
);
const {
File: AvatarSelect,
onOpen: onOpenSelectAvatar,
onSelectImage
} = useSelectFile({
fileType: '.jpg, .jpeg, .png',
multiple: false
});
const { loading: uploadingAvatar, run: onSelectAvatar } = useRequest2(
async (file: File[]) => {
return onSelectImage(file, {
maxW: 300,
maxH: 300
});
},
{
onSuccess: (src: string) => {
setValue('avatar', src);
}
}
);
const isLoading = uploadingAvatar || isLoadingUpdate || isLoadingCreate;
return (
<MyModal
isOpen
onClose={onClose}
title={isEdit ? t('account_team:edit_org_info') : t('account_team:create_org')}
iconSrc={'modal/edit'}
>
<ModalBody flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={4}>
<FormLabel w="80px">{t('user:team.avatar_and_name')}</FormLabel>
<HStack>
<Avatar
src={avatar || DEFAULT_ORG_AVATAR}
onClick={onOpenSelectAvatar}
cursor={'pointer'}
borderRadius={'md'}
/>
<Input
bgColor="myGray.50"
{...register('name', { required: true })}
placeholder={t('account_team:org_name')}
/>
</HStack>
<FormLabel w="80px">{t('account_team:org_description')}</FormLabel>
<Textarea
bgColor="myGray.50"
{...register('description')}
placeholder={t('account_team:org_description')}
/>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button
isLoading={isLoading}
onClick={handleSubmit((data) => {
if (isEdit) {
onUpdate(data);
} else {
onCreate(data);
}
})}
>
{isEdit ? t('common:common.Save') : t('common:new_create')}
</Button>
</ModalFooter>
<AvatarSelect onSelect={onSelectAvatar} />
</MyModal>
);
}
export default OrgInfoModal;

View File

@@ -0,0 +1,197 @@
import { putUpdateOrgMembers } from '@/web/support/user/team/org/api';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import type { GroupMemberRole } from '@fastgpt/global/support/permission/memberGroup/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import type React from 'react';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import dynamic from 'next/dynamic';
export type GroupFormType = {
members: {
tmbId: string;
role: `${GroupMemberRole}`;
}[];
};
function CheckboxIcon({
name
}: {
isChecked?: boolean;
isIndeterminate?: boolean;
name: IconNameType;
}) {
return <MyIcon name={name} w="12px" />;
}
function OrgMemberManageModal({
currentOrg,
refetchOrgs,
onClose
}: {
currentOrg: OrgType;
refetchOrgs: () => void;
onClose: () => void;
}) {
const { t } = useTranslation();
const allMembers = useContextSelector(TeamContext, (v) => v.members);
const [selectedMembers, setSelectedMembers] = useState<string[]>(
currentOrg.members.map((item) => item.tmbId)
);
const [searchKey, setSearchKey] = useState('');
const filterMembers = useMemo(() => {
if (!searchKey) return allMembers;
const regx = new RegExp(searchKey, 'i');
return allMembers.filter((member) => regx.test(member.memberName));
}, [searchKey, allMembers]);
const { run: onUpdate, loading: isLoadingUpdate } = useRequest2(
() => {
return putUpdateOrgMembers({
orgId: currentOrg._id,
members: selectedMembers.map((tmbId) => ({
tmbId
}))
});
},
{
successToast: t('common:common.Update Success'),
onSuccess() {
refetchOrgs();
onClose();
}
}
);
const isSelected = (memberId: string) => {
return selectedMembers.find((tmbId) => tmbId === memberId);
};
const handleToggleSelect = (memberId: string) => {
if (isSelected(memberId)) {
setSelectedMembers((state) => state.filter((tmbId) => tmbId !== memberId));
} else {
setSelectedMembers((state) => [...state, memberId]);
}
};
const isLoading = isLoadingUpdate;
return (
<MyModal
onClose={onClose}
isOpen
title={t('user:team.group.manage_member')}
iconSrc={currentOrg?.avatar}
minW="800px"
h={'100%'}
isCentered
>
<ModalBody flex={1}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" p="4">
<SearchInput
placeholder={t('user:search_user')}
fontSize="sm"
bg={'myGray.50'}
onChange={(e) => {
setSearchKey(e.target.value);
}}
/>
<Flex flexDirection="column" mt={3} flexGrow="1" overflow={'auto'} maxH={'400px'}>
{filterMembers.map((member) => {
return (
<HStack
py="2"
px={3}
borderRadius={'md'}
alignItems="center"
key={member.tmbId}
cursor={'pointer'}
_hover={{
bg: 'myGray.50',
...(!isSelected(member.tmbId) ? { svg: { color: 'myGray.50' } } : {})
}}
_notLast={{ mb: 2 }}
onClick={() => handleToggleSelect(member.tmbId)}
>
<Checkbox
isChecked={!!isSelected(member.tmbId)}
icon={<CheckboxIcon name={'common/check'} />}
pointerEvents="none"
/>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
);
})}
</Flex>
</Flex>
<Flex borderLeft="1px" borderColor="myGray.200" flexDirection="column" p="4" h={'100%'}>
<Box mt={2}>{`${t('common:chosen')}:${selectedMembers.length}`}</Box>
<Flex mt={3} flexDirection="column" flexGrow="1" overflow={'auto'} maxH={'400px'}>
{selectedMembers.map((tmbId) => {
const member = allMembers.find((item) => item.tmbId === tmbId)!;
return (
<HStack
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
key={tmbId}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<HStack>
<Avatar src={member?.avatar} w="1.5rem" borderRadius={'md'} />
<Box>{member?.memberName}</Box>
</HStack>
<MyIcon
name={'common/closeLight'}
w={'1rem'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => handleToggleSelect(tmbId)}
/>
</HStack>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button isLoading={isLoading} onClick={onUpdate}>
{t('common:common.Save')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMemberManageModal;

View File

@@ -0,0 +1,74 @@
import { putMoveOrg } from '@/web/support/user/team/org/api';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import OrgTree from './OrgTree';
import dynamic from 'next/dynamic';
import { useUserStore } from '@/web/support/user/useUserStore';
function OrgMoveModal({
movingOrg,
orgs,
onClose,
onSuccess
}: {
movingOrg: OrgType;
orgs: OrgType[];
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [selectedOrg, setSelectedOrg] = useState<OrgType>();
const { userInfo } = useUserStore();
const team = userInfo?.team!;
const { runAsync: onMoveOrg, loading } = useRequest2(putMoveOrg, {
onSuccess: () => {
onClose();
onSuccess();
}
});
const filterMovingOrgs = useMemo(
() => orgs.filter((org) => org._id !== movingOrg._id),
[movingOrg._id, orgs]
);
return (
<MyModal
isOpen
onClose={onClose}
title={t('account_team:move_org')}
iconSrc="common/file/move"
iconColor="primary.600"
>
<ModalBody>
<OrgTree
orgs={filterMovingOrgs}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
</ModalBody>
<ModalFooter>
<Button
isDisabled={!selectedOrg}
isLoading={loading}
onClick={() => {
if (!selectedOrg) return;
return onMoveOrg({
orgId: movingOrg._id,
targetOrgId: selectedOrg._id
});
}}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default OrgMoveModal;

View File

@@ -0,0 +1,103 @@
import { Box, HStack, VStack } from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToggle } from 'ahooks';
import { useMemo } from 'react';
import IconButton from './IconButton';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
function OrgTreeNode({
org,
list,
selectedOrg,
setSelectedOrg,
index = 0
}: {
org: OrgType;
list: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
index?: number;
}) {
const children = useMemo(
() => list.filter((item) => item.path === getOrgChildrenPath(org)),
[org, list]
);
const [isExpanded, toggleIsExpanded] = useToggle(index === 0);
return (
<Box userSelect={'none'}>
<HStack
borderRadius="sm"
_hover={{ bg: 'myGray.100' }}
py={1}
pr={2}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
cursor={'pointer'}
{...(selectedOrg === org
? {
bg: 'primary.50 !important',
onClick: () => setSelectedOrg(undefined)
}
: {
onClick: () => setSelectedOrg(org)
})}
>
{index > 0 && (
<IconButton
name={isExpanded ? 'common/downArrowFill' : 'common/rightArrowFill'}
color={'myGray.500'}
p={0}
w={'1.25rem'}
visibility={children.length > 0 ? 'visible' : 'hidden'}
onClick={(e) => {
e.stopPropagation();
toggleIsExpanded.toggle();
}}
/>
)}
<HStack
flex={'1 0 0'}
onClick={() => setSelectedOrg(org)}
cursor={'pointer'}
borderRadius={'xs'}
>
<Avatar src={org.avatar} w={'1.25rem'} borderRadius={'xs'} />
<Box>{org.name}</Box>
</HStack>
</HStack>
{isExpanded &&
children.length > 0 &&
children.map((child) => (
<Box key={child._id} mt={0.5}>
<OrgTreeNode
org={child}
index={index + 1}
list={list}
selectedOrg={selectedOrg}
setSelectedOrg={setSelectedOrg}
/>
</Box>
))}
</Box>
);
}
function OrgTree({
orgs,
selectedOrg,
setSelectedOrg
}: {
orgs: OrgType[];
selectedOrg?: OrgType;
setSelectedOrg: (org?: OrgType) => void;
}) {
const root = orgs[0];
if (!root) return;
return (
<OrgTreeNode org={root} list={orgs} setSelectedOrg={setSelectedOrg} selectedOrg={selectedOrg} />
);
}
export default OrgTree;

View File

@@ -0,0 +1,354 @@
import { deleteOrg, deleteOrgMember } from '@/web/support/user/team/org/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Box,
Divider,
Flex,
HStack,
Table,
TableContainer,
Tag,
Tbody,
Td,
Th,
Thead,
Tr,
VStack
} from '@chakra-ui/react';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useState } from 'react';
import { useContextSelector } from 'use-context-selector';
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import { TeamContext } from '../context';
import { getOrgList } from '@/web/support/user/team/org/api';
import IconButton from './IconButton';
import { defaultOrgForm, type OrgFormType } from './OrgInfoModal';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
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';
const OrgInfoModal = dynamic(() => import('./OrgInfoModal'));
const OrgMemberManageModal = dynamic(() => import('./OrgMemberManageModal'));
const OrgMoveModal = dynamic(() => import('./OrgMoveModal'));
function ActionButton({
icon,
text,
onClick
}: {
icon: IconNameType;
text: string;
onClick: () => void;
}) {
return (
<HStack
gap={'8px'}
w="100%"
transition={'background 0.1s'}
cursor={'pointer'}
p="4px"
rounded={'sm'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={onClick}
>
<MyIcon name={icon} w="1rem" h="1rem" />
<Box fontSize={'sm'}>{text}</Box>
</HStack>
);
}
function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
const { t } = useTranslation();
const { userInfo, isTeamAdmin } = useUserStore();
const { members } = useContextSelector(TeamContext, (v) => v);
const [parentPath, setParentPath] = useState('');
const {
data: orgs = [],
loading: isLoadingOrgs,
refresh: refetchOrgs
} = useRequest2(getOrgList, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const currentOrgs = useMemo(() => {
if (orgs.length === 0) return [];
// Auto select the first org(root org is team)
if (parentPath === '') {
setParentPath(getOrgChildrenPath(orgs[0]));
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => {
return {
...item,
// Member + org
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
};
});
}, [orgs, parentPath]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [editOrg, setEditOrg] = useState<OrgFormType>();
const [manageMemberOrg, setManageMemberOrg] = useState<OrgType>();
const [movingOrg, setMovingOrg] = useState<OrgType>();
// Delete org
const { ConfirmModal: ConfirmDeleteOrgModal, openConfirm: openDeleteOrgModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_org')
});
const deleteOrgHandler = (orgId: string) => openDeleteOrgModal(() => deleteOrgReq(orgId))();
const { runAsync: deleteOrgReq } = useRequest2(deleteOrg, {
onSuccess: () => {
refetchOrgs();
}
});
// Delete member
const { ConfirmModal: ConfirmDeleteMember, openConfirm: openDeleteMemberModal } = useConfirm({
type: 'delete',
content: t('account_team:confirm_delete_member')
});
const { runAsync: deleteMemberReq } = useRequest2(deleteOrgMember, {
onSuccess: () => {
refetchOrgs();
}
});
return (
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
</Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}>
<Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box>
<Flex w={'100%'} gap={'4'}>
{/* Table */}
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px">
{t('common:Name')}
</Th>
<Th bg="myGray.100" borderRightRadius="6px">
{t('common:common.Action')}
</Th>
</Tr>
</Thead>
<Tbody>
{currentOrgs.map((org) => (
<Tr key={org._id} overflow={'unset'}>
<Td>
<HStack
cursor={'pointer'}
onClick={() => setParentPath(getOrgChildrenPath(org))}
>
<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 && (
<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}>
<Td>
<MemberTag name={memberInfo.memberName} avatar={memberInfo.avatar} />
</Td>
<Td w={'6rem'}>
{isTeamAdmin && (
<MyMenu
trigger={'hover'}
Button={<IconButton name="more" />}
menuList={[
{
children: [
{
icon: 'delete',
label: t('account_team:delete'),
type: 'danger',
onClick: () =>
openDeleteMemberModal(() =>
deleteMemberReq(currentOrg._id, member.tmbId)
)()
}
]
}
]}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
{/* Slider */}
<VStack w={'180px'} alignItems={'start'}>
<HStack gap={'6px'}>
<Avatar src={currentOrg?.avatar} w={'1rem'} h={'1rem'} rounded={'xs'} />
<Box fontWeight={500} color={'myGray.900'}>
{currentOrg?.name}
</Box>
{currentOrg?.path !== '' && (
<IconButton name="edit" onClick={() => setEditOrg(currentOrg)} />
)}
</HStack>
<Box fontSize={'xs'}>{currentOrg?.description || t('common:common.no_intro')}</Box>
<Divider my={'20px'} />
<Box fontWeight={500} fontSize="sm" color="myGray.900">
{t('common:common.Action')}
</Box>
{currentOrg && isTeamAdmin && (
<VStack gap="13px" w="100%">
<ActionButton
icon="common/add2"
text={t('account_team:create_sub_org')}
onClick={() => {
setEditOrg({
...defaultOrgForm,
parentId: currentOrg?._id
});
}}
/>
<ActionButton
icon="common/administrator"
text={t('account_team:manage_member')}
onClick={() => setManageMemberOrg(currentOrg)}
/>
{currentOrg?.path !== '' && (
<>
<ActionButton
icon="common/file/move"
text={t('account_team:move_org')}
onClick={() => setMovingOrg(currentOrg)}
/>
<ActionButton
icon="delete"
text={t('account_team:delete_org')}
onClick={() => deleteOrgHandler(currentOrg._id)}
/>
</>
)}
</VStack>
)}
</VStack>
</Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox>
</>
);
}
export default OrgTable;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import {
Box,
Checkbox,
@@ -9,279 +9,422 @@ import {
Td,
Th,
Thead,
Tr
Text,
Tr,
Flex,
Button
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getTeamClbs, updateMemberPermission } from '@/web/support/user/team/api';
import {
deleteMemberPermission,
getTeamClbs,
updateMemberPermission
} from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamContext } from '../context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MemberTag from '../../../../../components/support/user/team/Info/MemberTag';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import {
TeamManagePermissionVal,
TeamPermissionList,
TeamWritePermissionVal
} from '@fastgpt/global/support/permission/user/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
import { useCreation } from 'ahooks';
import { useToggle } from 'ahooks';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyBox from '@fastgpt/web/components/common/MyBox';
import CollaboratorContextProvider, {
CollaboratorContext
} 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';
function PermissionManage() {
function PermissionManage({
Tabs,
onOpenAddMember
}: {
Tabs: React.ReactNode;
onOpenAddMember: () => void;
}) {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { groups, refetchMembers, refetchGroups, members, searchKey } = useContextSelector(
TeamContext,
(v) => v
const collaboratorList = useContextSelector(
CollaboratorContext,
(state) => state.collaboratorList
);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(state) => state.onUpdateCollaborators
);
const onDelOneCollaborator = useContextSelector(
CollaboratorContext,
(state) => state.onDelOneCollaborator
);
const { runAsync: refetchClbs, data: clbs = [] } = useRequest2(getTeamClbs, {
manual: false,
refreshDeps: [userInfo?.team?.teamId]
});
const [isExpandMember, setExpandMember] = useToggle(true);
const [isExpandGroup, setExpandGroup] = useToggle(true);
const [isExpandOrg, setExpandOrg] = useToggle(true);
const filteredGroups = useCreation(
() => groups?.filter((group) => group.name.toLowerCase().includes(searchKey.toLowerCase())),
[groups, searchKey]
);
const filteredMembers = useCreation(
() =>
members
?.filter((member) => member.memberName.toLowerCase().includes(searchKey.toLowerCase()))
.map((member) => {
const clb = clbs?.find((clb) => String(clb.tmbId) === String(member.tmbId));
const permission =
member.role === 'owner'
? new TeamPermission({ isOwner: true })
: new TeamPermission({ per: clb?.permission });
const { tmbList, groupList, orgList } = useMemo(() => {
const tmbList: CollaboratorItemType[] = [];
const groupList: CollaboratorItemType[] = [];
const orgList: CollaboratorItemType[] = [];
return { ...member, permission };
}),
[clbs, members, searchKey]
);
const { runAsync: onUpdateMemberPermission } = useRequest2(updateMemberPermission, {
onSuccess: () => {
refetchGroups();
refetchMembers();
refetchClbs();
}
});
const { runAsync: onAddPermission, loading: addLoading } = useRequest2(
async ({
groupId,
memberId,
per
}: {
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group._id === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
}
}
collaboratorList.forEach((item) => {
if (item.tmbId) {
tmbList.push(item);
} else if (item.groupId) {
groupList.push(item);
} else if (item.orgId) {
orgList.push(item);
}
if (memberId) {
const member = filteredMembers?.find((member) => String(member.tmbId) === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value });
switch (per) {
case 'write':
permission.addPer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
case 'manage':
permission.addPer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
}
}
});
return {
tmbList,
groupList,
orgList
};
}, [collaboratorList]);
const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2(
async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => {
const clb = collaboratorList.find(
(clb) => clb.tmbId === id || clb.groupId === id || clb.orgId === id
);
if (!clb) return;
const updatePer = per === 'write' ? TeamWritePermissionVal : TeamManagePermissionVal;
const permission = new TeamPermission({ per: clb.permission.value });
if (type === 'add') {
permission.addPer(updatePer);
} else {
permission.removePer(updatePer);
}
return onUpdateCollaborators({
...(clb.tmbId && { members: [clb.tmbId] }),
...(clb.groupId && { groups: [clb.groupId] }),
...(clb.orgId && { orgs: [clb.orgId] }),
permission: permission.value
});
}
);
const { runAsync: onRemovePermission, loading: removeLoading } = useRequest2(
async ({
groupId,
memberId,
per
}: {
groupId?: string;
memberId?: string;
per: 'write' | 'manage';
}) => {
if (groupId) {
const group = groups?.find((group) => group._id === groupId);
if (group) {
const permission = new TeamPermission({ per: group.permission.value });
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
groupId: group._id,
permission: permission.value
});
}
}
}
if (memberId) {
const member = members?.find((member) => String(member.tmbId) === memberId);
if (member) {
const permission = new TeamPermission({ per: member.permission.value }); // Hint: member.permission is read-only
switch (per) {
case 'write':
permission.removePer(TeamWritePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
case 'manage':
permission.removePer(TeamManagePermissionVal);
return onUpdateMemberPermission({
memberId: String(member.tmbId),
permission: permission.value
});
}
}
}
}
);
const { runAsync: onDeleteMemberPermission, loading: deleteLoading } =
useRequest2(onDelOneCollaborator);
const userManage = userInfo?.permission.hasManagePer;
const hasDeletePer = (per: TeamPermission) => {
if (userInfo?.permission.isOwner) return true;
if (userManage && !per.hasManagePer) return true;
return false;
};
return (
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{t('user:team.group.group')} / {t('user:team.group.members')}
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
{filteredGroups?.map((group) => (
<Tr key={group._id} overflow={'unset'} border="none">
<Td border="none">
<MemberTag
name={
group.name === DefaultGroupName ? userInfo?.team.teamName ?? '' : group.name
}
avatar={group.avatar}
/>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group._id, per: 'write' })
: onRemovePermission({ groupId: group._id, per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ groupId: group._id, per: 'manage' })
: onRemovePermission({ groupId: group._id, per: 'manage' })
}
/>
</Box>
</Td>
</Tr>
))}
{filteredGroups?.length > 0 && filteredMembers?.length > 0 && (
<Tr borderBottom={'1px solid'} borderColor={'myGray.300'} />
)}
{filteredMembers?.map((member) => (
<Tr key={member.tmbId} overflow={'unset'} border="none">
<Td border="none">
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.memberName}</Box>
</HStack>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'write' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'write' })
}
/>
</Box>
</Td>
<Td border="none">
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onAddPermission({ memberId: String(member.tmbId), per: 'manage' })
: onRemovePermission({ memberId: String(member.tmbId), per: 'manage' })
}
/>
</Box>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs}
<Box ml="auto">
{/* <SearchInput
placeholder={t('user:search_group_org_user')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/> */}
</Box>
{userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/add2" w={'14px'} />}
onClick={onOpenAddMember}
>
{t('user:permission.Add')}
</Button>
)}
</Flex>
<MyBox isLoading={addLoading || deleteLoading}>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="md" maxW={'150px'}>
{`${t('user:team.group.members')} / ${t('user:team.org.org')} / ${t('user:team.group.group')}`}
<QuestionTip ml="1" label={t('user:team.group.permission_tip')} />
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.write')}
</Box>
</Th>
<Th bg="myGray.100">
<Box mx="auto" w="fit-content">
{t('user:team.group.permission.manage')}
<QuestionTip ml="1" label={t('user:team.group.manage_tip')} />
</Box>
</Th>
<Th bg="myGray.100" borderRightRadius="md">
<Box mx="auto" w="fit-content">
{t('common:common.Action')}
</Box>
</Th>
</Tr>
</Thead>
<Tbody>
<>
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandMember && !!tmbList.length ? 0 : 3}>
<MyIconButton
icon={isExpandMember ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandMember.toggle}
/>
<Box color={'myGray.900'}>{t('user:team.group.members')}</Box>
</HStack>
</Tr>
{isExpandMember &&
tmbList.map((member) => (
<Tr key={member.tmbId}>
<Td pl={10}>
<HStack>
<Avatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box>{member.name}</Box>
</HStack>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userManage}
isChecked={member.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={member.permission.isOwner || !userInfo?.permission.isOwner}
isChecked={member.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: member.tmbId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: member.tmbId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(member.permission) &&
userInfo?.team.tmbId !== member.tmbId && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() =>
onDeleteMemberPermission({ tmbId: String(member.tmbId) })
}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandOrg && !!orgList.length ? 0 : 3}>
<MyIconButton
icon={isExpandOrg ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandOrg.toggle}
/>
<Text>{t('user:team.org.org')}</Text>
</HStack>
</Tr>
{isExpandOrg &&
orgList.map((org) => (
<Tr key={org.orgId}>
<Td pl={10}>
<MemberTag name={org.name} avatar={org.avatar} />
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={org.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'write' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={org.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'manage' })
: onUpdatePermission({
id: org.orgId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(org.permission) && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ orgId: org.orgId! })}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
<>
<Tr borderBottom={'1px solid'} borderColor={'myGray.200'} />
<Tr userSelect={'none'}>
<HStack pl={3} pt={3} pb={isExpandGroup && !!groupList.length ? 0 : 3}>
<MyIconButton
icon={isExpandGroup ? 'common/downArrowFill' : 'common/rightArrowFill'}
onClick={setExpandGroup.toggle}
/>
<Text>{t('user:team.group.group')}</Text>
</HStack>
</Tr>
{isExpandGroup &&
groupList.map((group) => (
<Tr key={group.groupId}>
<Td pl={10}>
<MemberTag
name={
group.name === DefaultGroupName
? userInfo?.team.teamName ?? ''
: group.name
}
avatar={group.avatar}
/>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userManage}
isChecked={group.permission.hasWritePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'write'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'write'
})
}
/>
</Box>
</Td>
<Td>
<Box mx="auto" w="fit-content">
<Checkbox
isDisabled={!userInfo?.permission.isOwner}
isChecked={group.permission.hasManagePer}
onChange={(e) =>
e.target.checked
? onUpdatePermission({
id: group.groupId!,
type: 'add',
per: 'manage'
})
: onUpdatePermission({
id: group.groupId!,
type: 'remove',
per: 'manage'
})
}
/>
</Box>
</Td>
<Td>
{hasDeletePer(group.permission) && (
<Box mx="auto" w="fit-content">
<MyIconButton
icon="common/trash"
onClick={() => onDeleteMemberPermission({ groupId: group.groupId! })}
/>
</Box>
)}
</Td>
</Tr>
))}
</>
</Tbody>
</Table>
</TableContainer>
</MyBox>
</>
);
}
export default PermissionManage;
export const Render = ({ Tabs }: { Tabs: React.ReactNode }) => {
const { userInfo } = useUserStore();
return userInfo?.team ? (
<CollaboratorContextProvider
permission={userInfo?.team.permission}
permissionList={TeamPermissionList}
onGetCollaboratorList={getTeamClbs}
onUpdateCollaborators={updateMemberPermission}
onDelOneCollaborator={deleteMemberPermission}
refreshDeps={[userInfo?.team.teamId]}
addPermissionOnly={true}
>
{({ onOpenAddMember }) => <PermissionManage Tabs={Tabs} onOpenAddMember={onOpenAddMember} />}
</CollaboratorContextProvider>
) : null;
};
export default Render;

View File

@@ -10,6 +10,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { getGroupList } from '@/web/support/user/team/group/api';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
const EditInfoModal = dynamic(() => import('./EditInfoModal'));
@@ -24,8 +25,6 @@ type TeamModalContextType = {
refetchMembers: () => void;
refetchTeams: () => void;
refetchGroups: () => void;
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
teamSize: number;
};
@@ -50,10 +49,6 @@ export const TeamContext = createContext<TeamModalContextType>({
throw new Error('Function not implemented.');
},
searchKey: '',
setSearchKey: function (_value: React.SetStateAction<string>): void {
throw new Error('Function not implemented.');
},
teamSize: 0
});
@@ -61,7 +56,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo, loadAndGetTeamMembers } = useUserStore();
const [searchKey, setSearchKey] = useState('');
const {
data: myTeams = [],
@@ -114,8 +108,6 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
refetchTeams,
isLoading,
onSwitchTeam,
searchKey,
setSearchKey,
// create | update team
setEditTeamData,

View File

@@ -1,110 +1,67 @@
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import AccountContainer from '../components/AccountContainer';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
import Icon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import TeamSelector from '../components/TeamSelector';
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { useRouter } from 'next/router';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { delLeaveTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamContext, TeamModalContextProvider } from './components/context';
import dynamic from 'next/dynamic';
import TeamTagModal from '@/components/support/user/team/TeamTagModal';
import MemberTable from './components/MemberTable';
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
const GroupManage = dynamic(() => import('./components/GroupManage/index'));
const GroupInfoModal = dynamic(() => import('./components/GroupManage/GroupInfoModal'));
const ManageGroupMemberModal = dynamic(() => import('./components/GroupManage/GroupManageMember'));
const OrgManage = dynamic(() => import('./components/OrgManage/index'));
export enum TeamTabEnum {
member = 'member',
org = 'org',
group = 'group',
permission = 'permission'
}
const Team = () => {
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const {
myTeams,
refetchTeams,
members,
refetchMembers,
setEditTeamData,
onSwitchTeam,
searchKey,
setSearchKey,
teamSize,
isLoading
} = useContextSelector(TeamContext, (v) => v);
const { teamTab = TeamTabEnum.member } = router.query as { teamTab: `${TeamTabEnum}` };
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenGroupInfo,
onOpen: onOpenGroupInfo,
onClose: onCloseGroupInfo
} = useDisclosure();
const {
isOpen: isOpenManageGroupMember,
onOpen: onOpenManageGroupMember,
onClose: onCloseManageGroupMember
} = useDisclosure();
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { runAsync: onLeaveTeam, loading: isLoadingLeaveTeam } = useRequest2(
async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
onSuccess() {
refetchTeams();
},
errorToast: t('account_team:user_team_leave_team_failed')
}
const { setEditTeamData, teamSize, isLoading } = useContextSelector(TeamContext, (v) => v);
const Tabs = useMemo(
() => (
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:org'), value: TeamTabEnum.org },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
),
[router, t, teamTab]
);
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('account_team:confirm_leave_team')
});
const [editGroupId, setEditGroupId] = useState<string>();
const onEditGroup = (groupId: string) => {
setEditGroupId(groupId);
onOpenGroupInfo();
};
const onManageMember = (groupId: string) => {
setEditGroupId(groupId);
onOpenManageGroupMember();
};
return (
<AccountContainer isLoading={isLoading}>
{/* header */}
@@ -168,142 +125,11 @@ const Team = () => {
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
<Flex justify={'space-between'} align={'center'} pb={'1rem'}>
<FillRowTabs
list={[
{ label: t('account_team:member'), value: TeamTabEnum.member },
{ label: t('account_team:group'), value: TeamTabEnum.group },
{ label: t('account_team:permission'), value: TeamTabEnum.permission }
]}
px={'1rem'}
value={teamTab}
onChange={(e) => {
router.replace({
query: {
...router.query,
teamTab: e
}
});
}}
/>
<Flex alignItems={'center'}>
{teamTab === TeamTabEnum.member &&
userInfo?.team.permission.hasManagePer &&
feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="core/dataset/tag" w={'16px'} />}
onClick={() => {
onOpenTeamTagsAsync();
}}
>
{t('account_team:label_sync')}
</Button>
)}
{teamTab === TeamTabEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'16px'} color={'white'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('account_team:user_team_invite_member')}
</Button>
)}
{teamTab === TeamTabEnum.member && !userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
>
{t('account_team:user_team_leave_team')}
</Button>
)}
{teamTab === TeamTabEnum.group && userInfo?.team.permission.hasManagePer && (
<Button
variant={'primary'}
size="md"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="support/permission/collaborator" w={'14px'} />}
onClick={onOpenGroupInfo}
>
{t('user:team.group.create')}
</Button>
)}
{teamTab === TeamTabEnum.permission && (
<Box ml="auto">
<SearchInput
placeholder={t('user:team.group.search_placeholder')}
w="200px"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
</Box>
)}
</Flex>
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{teamTab === TeamTabEnum.member && <MemberTable />}
{teamTab === TeamTabEnum.group && (
<GroupManage onEditGroup={onEditGroup} onManageMember={onManageMember} />
)}
{teamTab === TeamTabEnum.permission && <PermissionManage />}
</Box>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
{isOpenGroupInfo && (
<GroupInfoModal
onClose={() => {
onCloseGroupInfo();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
{isOpenManageGroupMember && (
<ManageGroupMemberModal
onClose={() => {
onCloseManageGroupMember();
setEditGroupId(undefined);
}}
editGroupId={editGroupId}
/>
)}
<ConfirmLeaveTeamModal />
</AccountContainer>
);
};

View File

@@ -0,0 +1,123 @@
import { NextAPI } from '@/service/middleware/entry';
import { delay } from '@fastgpt/global/common/system/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { jiebaSplit } from '@fastgpt/service/common/string/jieba';
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextApiRequest, NextApiResponse } from 'next';
/*
简单版迁移:直接升级到最新镜像,会去除 MongoDatasetData 里的索引。直接执行这个脚本。
无缝迁移:
1. 先用 4.8.18-tmp 版本,会同时有 MongoDatasetData 和 MongoDatasetDataText 两个表和索引,依然是 MongoDatasetData 生效。会同步更新两张表数据。
2. 执行升级脚本,不要删除 MongoDatasetData 里的数据。
3. 切换正式版镜像,让 MongoDatasetDataText 生效。
4. 删除 MongoDatasetData 里的索引和多余字段。4819 再删
*/
let success = 0;
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
const batchSize = req.body.batchSize || 500;
success = 0;
const start = Date.now();
await initData(batchSize);
// await restore();
console.log('Init data time:', Date.now() - start);
success = 0;
// batchUpdateFields();
return { success: true };
}
export default NextAPI(handler);
const restore = async () => {
try {
const data = await MongoDatasetData.findOne({ fullTextToken: { $exists: false } });
if (!data) return;
data.fullTextToken = jiebaSplit({ text: `${data.q}\n${data.a}`.trim() });
await data.save();
success++;
console.log('Success:', success);
await restore();
} catch (error) {
console.log(error);
await delay(500);
await restore();
}
};
const initData = async (batchSize: number) => {
while (true) {
try {
// 找到没有初始化的数据
const dataList = await MongoDatasetData.find(
{
initFullText: { $exists: false }
},
'_id teamId datasetId collectionId fullTextToken'
)
.limit(batchSize)
.lean();
if (dataList.length === 0) break;
try {
await MongoDatasetDataText.insertMany(
dataList.map((item) => ({
teamId: item.teamId,
datasetId: item.datasetId,
collectionId: item.collectionId,
dataId: item._id,
fullTextToken: item.fullTextToken
})),
{ ordered: false, lean: true }
);
} catch (error: any) {
if (error.code === 11000) {
console.log('Duplicate key error');
} else {
throw error;
}
}
// 把成功插入的新数据的 dataId 更新为已初始化
await MongoDatasetData.updateMany(
{ _id: { $in: dataList.map((item) => item._id) } },
// FullText tmp
// { $set: { initFullText: true } }
{ $set: { initFullText: true }, $unset: { fullTextToken: 1 } }
);
success += dataList.length;
console.log('Success:', success);
// await initData(batchSize);
} catch (error: any) {
console.log(error, '===');
await delay(500);
// await initData(batchSize);
}
}
};
const batchUpdateFields = async (batchSize = 2000) => {
// Update in batches
await MongoDatasetData.updateMany(
{ initFullText: { $exists: true } },
{
$unset: {
initFullText: 1,
fullTextToken: 1
}
}
);
};

View File

@@ -89,7 +89,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
path: file.path,
filename: file.originalname,
contentType: file.mimetype,
encoding: file.encoding,
metadata: metadata
});

View File

@@ -1,38 +1,30 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller';
import { UploadImgProps } from '@fastgpt/global/common/file/api';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextAPI } from '@/service/middleware/entry';
/*
Upload avatar image
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const body = req.body as UploadImgProps;
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<string> {
await connectToDatabase();
const body = req.body as UploadImgProps;
const { teamId } = await authCert({ req, authToken: true });
const { teamId } = await authCert({ req, authToken: true });
const imgId = await uploadMongoImg({
teamId,
...body
});
jsonRes(res, { data: imgId });
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
return uploadMongoImg({
teamId,
...body
});
}
export default NextAPI(handler);
export const config = {
api: {
bodyParser: {
sizeLimit: '16mb'
sizeLimit: '12mb'
}
}
};

View File

@@ -0,0 +1,25 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import * as fs from 'fs';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
export type writefileQuery = {};
export type writefileBody = {
name: string;
content: string;
};
export type writefileResponse = {};
async function handler(
req: ApiRequestProps<writefileBody, writefileQuery>,
res: ApiResponseType<any>
): Promise<writefileResponse> {
await authCert({ req, authRoot: true });
const { name, content } = req.body;
await fs.promises.writeFile(`public/${name}`, content);
return {};
}
export default NextAPI(handler);

View File

@@ -15,8 +15,8 @@ import { ClientSession } from '@fastgpt/service/common/mongo';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type CreateAppBody = {
parentId?: ParentIdType;
@@ -148,6 +148,8 @@ export const onCreateApp = async ({
);
}
await refreshSourceAvatar(avatar, undefined, session);
return appId;
};

View File

@@ -18,6 +18,7 @@ import { ClientSession } from '@fastgpt/service/common/mongo';
import { deleteChatFiles } from '@fastgpt/service/core/chat/controller';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema';
import { removeImageByPath } from '@fastgpt/service/common/file/image/controller';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@@ -57,7 +58,7 @@ export const onDelOneApp = async ({
const apps = await findAppAndAllChildren({
teamId,
appId,
fields: '_id'
fields: '_id avatar'
});
const del = async (session: ClientSession) => {
@@ -109,6 +110,8 @@ export const onDelOneApp = async ({
},
{ session }
);
await removeImageByPath(app.avatar, session);
}
};

View File

@@ -11,6 +11,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema';
import { isEqual } from 'lodash';
import { onCreateApp } from '../create';
import { onDelOneApp } from '../del';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type UpdateHttpPluginBody = {
appId: string;
@@ -49,13 +50,15 @@ async function handler(req: ApiRequestProps<UpdateHttpPluginBody>, res: NextApiR
await MongoApp.findByIdAndUpdate(
appId,
{
name,
avatar,
intro,
...(name && { name }),
...(avatar && { avatar }),
...(intro !== undefined && { intro }),
pluginData
},
{ session }
);
await refreshSourceAvatar(avatar, app.avatar, session);
});
}

View File

@@ -15,8 +15,9 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
export type ListAppBody = {
parentId?: ParentIdType;
@@ -25,7 +26,7 @@ export type ListAppBody = {
searchKey?: string;
};
/*
/*
获取 APP 列表权限
1. 校验 folder 权限和获取 team 权限owner 单独处理)
2. 获取 team 下所有 app 权限。获取我的所有组。并计算出我所有的app权限。
@@ -60,7 +61,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
]);
// Get team all app permissions
const [perList, myGroupMap] = await Promise.all([
const [perList, myGroupMap, myOrgSet] = await Promise.all([
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
@@ -77,11 +78,18 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
map.set(String(item._id), 1);
});
return map;
}),
getOrgIdSetWithParentByTmbId({
teamId,
tmbId
})
]);
// Get my permissions
const myPerList = perList.filter(
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
(item) =>
String(item.tmbId) === String(tmbId) ||
myGroupMap.has(String(item.groupId)) ||
myOrgSet.has(String(item.orgId))
);
const findAppsQuery = (() => {
@@ -151,9 +159,11 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
const tmbPer = myPerList.find(
(item) => String(item.resourceId) === appId && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
const groupPer = concatPer(
myPerList
.filter((item) => String(item.resourceId) === appId && !!item.groupId)
.filter(
(item) => String(item.resourceId) === appId && (!!item.groupId || !!item.orgId)
)
.map((item) => item.permission)
);

View File

@@ -22,6 +22,7 @@ import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/co
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type AppUpdateQuery = {
appId: string;
@@ -95,6 +96,8 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
isPlugin: app.type === AppTypeEnum.plugin
});
await refreshSourceAvatar(avatar, app.avatar, session);
return MongoApp.findByIdAndUpdate(
appId,
{

View File

@@ -12,18 +12,16 @@ import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controlle
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NextAPI } from '@/service/middleware/entry';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
import { getRandomUserAvatar } from '@fastgpt/global/support/user/utils';
async function handler(req: NextApiRequest, res: NextApiResponse) {
let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps;
// auth link permission
const { outLinkConfig, uid, appId } = await authOutLink({ shareId, outLinkUid });
const { uid, appId } = await authOutLink({ shareId, outLinkUid });
// auth app permission
const [tmb, chat, app] = await Promise.all([
MongoTeamMember.findById(outLinkConfig.tmbId, '_id userId')
.populate<{ user: UserModelSchema }>('user', 'avatar')
.lean(),
const [chat, app] = await Promise.all([
MongoChat.findOne({ appId, chatId, shareId }).lean(),
MongoApp.findById(appId).lean()
]);
@@ -48,7 +46,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId,
appId: app._id,
title: chat?.title,
userAvatar: tmb?.user?.avatar,
userAvatar: getRandomUserAvatar(),
variables: chat?.variables,
app: {
chatConfig: getAppChatConfig({

View File

@@ -64,7 +64,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
path: file.path,
filename: file.originalname,
contentType: file.mimetype,
encoding: file.encoding,
metadata: fileMetadata
});

View File

@@ -11,6 +11,8 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type DatasetCreateQuery = {};
export type DatasetCreateBody = CreateDatasetParams;
@@ -63,19 +65,29 @@ async function handler(
// check limit
await checkTeamDatasetLimit(teamId);
const { _id } = await MongoDataset.create({
...parseParentIdInMongo(parentId),
name,
intro,
teamId,
tmbId,
vectorModel,
agentModel,
avatar,
type,
apiServer,
feishuServer,
yuqueServer
const datasetId = await mongoSessionRun(async (session) => {
const [{ _id }] = await MongoDataset.create(
[
{
...parseParentIdInMongo(parentId),
name,
intro,
teamId,
tmbId,
vectorModel,
agentModel,
avatar,
type,
apiServer,
feishuServer,
yuqueServer
}
],
{ session }
);
await refreshSourceAvatar(avatar, undefined, session);
return _id;
});
pushTrack.createDataset({
@@ -85,6 +97,6 @@ async function handler(
uid: userId
});
return _id;
return datasetId;
}
export default NextAPI(handler);

View File

@@ -8,6 +8,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoDatasetCollectionTags } from '@fastgpt/service/core/dataset/tag/schema';
import { removeImageByPath } from '@fastgpt/service/common/file/image/controller';
async function handler(req: NextApiRequest) {
const { id: datasetId } = req.query as {
@@ -51,6 +52,10 @@ async function handler(req: NextApiRequest) {
},
{ session }
);
for await (const dataset of datasets) {
await removeImageByPath(dataset.avatar, session);
}
});
}

View File

@@ -17,7 +17,8 @@ import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers';
import { getGroupPer } from '@fastgpt/service/support/permission/controller';
import { concatPer } from '@fastgpt/service/support/permission/controller';
import { getOrgIdSetWithParentByTmbId } from '@fastgpt/service/support/permission/org/controllers';
export type GetDatasetListBody = {
parentId: ParentIdType;
@@ -50,7 +51,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
]);
// Get team all app permissions
const [perList, myGroupMap] = await Promise.all([
const [perList, myGroupMap, myOrgSet] = await Promise.all([
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.dataset,
teamId,
@@ -67,10 +68,17 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
map.set(String(item._id), 1);
});
return map;
}),
getOrgIdSetWithParentByTmbId({
teamId,
tmbId
})
]);
const myPerList = perList.filter(
(item) => String(item.tmbId) === String(tmbId) || myGroupMap.has(String(item.groupId))
(item) =>
String(item.tmbId) === String(tmbId) ||
myGroupMap.has(String(item.groupId)) ||
myOrgSet.has(String(item.orgId))
);
const findDatasetQuery = (() => {
@@ -122,9 +130,11 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
const tmbPer = myPerList.find(
(item) => String(item.resourceId) === datasetId && !!item.tmbId
)?.permission;
const groupPer = getGroupPer(
const groupPer = concatPer(
myPerList
.filter((item) => String(item.resourceId) === datasetId && !!item.groupId)
.filter(
(item) => String(item.resourceId) === datasetId && (!!item.groupId || !!item.orgId)
)
.map((item) => item.permission)
);
return new DatasetPermission({

View File

@@ -28,6 +28,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema';
import { addDays } from 'date-fns';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type DatasetUpdateQuery = {};
export type DatasetUpdateResponse = any;
@@ -144,6 +145,8 @@ async function handler(
autoSync,
session
});
await refreshSourceAvatar(avatar, dataset.avatar, session);
};
await mongoSessionRun(async (session) => {

View File

@@ -7,12 +7,14 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import { NextAPI } from '@/service/middleware/entry';
import { useReqFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { username, password } = req.body as PostLoginProps;
if (!username || !password) {
throw new Error('缺少参数');
return Promise.reject(CommonErrEnum.invalidParams);
}
// 检测用户是否存在
@@ -23,11 +25,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
'status'
);
if (!authCert) {
throw new Error('用户未注册');
return Promise.reject(UserErrEnum.account_psw_error);
}
if (authCert.status === UserStatusEnum.forbidden) {
throw new Error('账号已停用,无法登录');
return Promise.reject('Invalid account!');
}
const user = await MongoUser.findOne({
@@ -36,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
});
if (!user) {
throw new Error('密码错误');
return Promise.reject(UserErrEnum.account_psw_error);
}
const userDetail = await getUserDetail({
@@ -68,4 +70,4 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
};
}
export default NextAPI(useReqFrequencyLimit(120, 10), handler);
export default NextAPI(useReqFrequencyLimit(120, 10, true), handler);

View File

@@ -6,6 +6,9 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc
/* update user info */
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller';
export type UserAccountUpdateQuery = {};
export type UserAccountUpdateBody = UserUpdateParams;
export type UserAccountUpdateResponse = {};
@@ -16,22 +19,22 @@ async function handler(
const { avatar, timezone } = req.body;
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
throw new Error('can not find it');
}
const userId = tmb.userId;
const user = await getUserDetail({ tmbId });
// 更新对应的记录
await MongoUser.updateOne(
{
_id: userId
},
{
...(avatar && { avatar }),
...(timezone && { timezone })
}
);
await mongoSessionRun(async (session) => {
await MongoUser.updateOne(
{
_id: user._id
},
{
...(avatar && { avatar }),
...(timezone && { timezone })
}
).session(session);
await refreshSourceAvatar(avatar, user.avatar, session);
});
return {};
}

View File

@@ -1,40 +1,37 @@
import React, { useCallback } from 'react';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { AppContext } from '@/pages/app/detail/components/context';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useI18n } from '@/web/context/I18n';
import { resumeInheritPer } from '@/web/core/app/api';
import {
deleteAppCollaborators,
getCollaboratorList,
postUpdateAppCollaborators
} from '@/web/core/app/api/collaborator';
import {
Box,
Flex,
Button,
Flex,
FormControl,
Input,
Textarea,
ModalBody,
ModalFooter,
ModalBody
Textarea
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import CollaboratorContextProvider from '@/components/support/permission/MemberManager/context';
import {
postUpdateAppCollaborators,
deleteAppCollaborators,
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import type { AppSchema } from '@fastgpt/global/core/app/type.d';
import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { resumeInheritPer } from '@/web/core/app/api';
import { useI18n } from '@/web/context/I18n';
import ResumeInherit from '@/components/support/permission/ResumeInheritText';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useTranslation } from 'next-i18next';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useContextSelector } from 'use-context-selector';
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
@@ -42,7 +39,11 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { toast } = useToast();
const { updateAppDetail, appDetail, reloadApp } = useContextSelector(AppContext, (v) => v);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
@@ -101,45 +102,28 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
[handleSubmit, onClose, saveSubmitError, saveSubmitSuccess]
);
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.appAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const onUpdateCollaborators = ({
members,
groups,
orgs,
permission
}: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: PermissionValueType;
}) =>
postUpdateAppCollaborators({
members,
groups,
permission,
orgs,
appId: appDetail._id
});
const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) =>
const onDelCollaborator = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) =>
deleteAppCollaborators({
appId: appDetail._id,
...props
@@ -203,7 +187,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
)}
<Box mt={6}>
<CollaboratorContextProvider
mode="all"
permission={appDetail.permission}
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
@@ -211,7 +194,8 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
onUpdateCollaborators({
permission: props.permission,
members: props.members,
groups: props.groups
groups: props.groups,
orgs: props.orgs
})
}
onDelOneCollaborator={onDelCollaborator}
@@ -267,7 +251,15 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => {
</Button>
</ModalFooter>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};

View File

@@ -66,7 +66,7 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
cursor={'pointer'}
onClick={onOpenInfoEdit}
>
<MyIcon name={'support/team/key'} w={'16px'} mr={2} />
<MyIcon name={'key'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:Role_setting')}</Box>
</MyBox>
<Box w={'full'} h={'1px'} bg={'myGray.200'} my={1} />

View File

@@ -57,7 +57,7 @@ const NodeCard = (props: Props) => {
name = t('common:core.module.template.UnKnow Module'),
intro,
minW = '300px',
maxW = '600px',
maxW = '666px',
minH = 0,
w = 'full',
h = 'full',

View File

@@ -1,10 +1,7 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useRef } from 'react';
import { Box, Flex, Button, ModalBody, Input, Grid, Card } from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { postCreateApp } from '@/web/core/app/api';
import { useRouter } from 'next/router';
import { emptyTemplates } from '@/web/core/app/templates';
@@ -13,7 +10,6 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
@@ -25,7 +21,6 @@ import {
getTemplateMarketItemList
} from '@/web/core/app/api/template';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppTemplateSchemaType } from '@fastgpt/global/core/app/type';
type FormType = {
avatar: string;
@@ -44,7 +39,6 @@ const CreateModal = ({
onOpenTemplateModal: (type: AppTypeEnum) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const router = useRouter();
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
const { isPc } = useSystem();
@@ -86,33 +80,15 @@ const CreateModal = ({
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.appAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { runAsync: onclickCreate, loading: isCreating } = useRequest2(
async (data: FormType, templateId?: string) => {
if (!templateId) {
@@ -290,7 +266,15 @@ const CreateModal = ({
))}
</Grid>
</ModalBody>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};

View File

@@ -17,14 +17,12 @@ import {
} from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import { HttpPluginImgUrl, MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { HttpPluginImgUrl } from '@fastgpt/global/common/file/image/constants';
import {
postCreateHttpPlugin,
putUpdateHttpPlugin,
@@ -124,33 +122,15 @@ const HttpPluginEditModal = ({
errorToast: t('common:common.Update Failed')
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: 'image/*',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.pluginAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
/* load api from url */
const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({
mutationFn: async () => {
@@ -473,7 +453,15 @@ const HttpPluginEditModal = ({
)}
</ModalFooter>
</MyModal>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</>
);
};

View File

@@ -344,7 +344,7 @@ const ListItem = () => {
...(app.permission.hasManagePer
? [
{
icon: 'support/team/key',
icon: 'key',
type: 'grayBg' as MenuItemType,
label: t('common:permission.Permission'),
onClick: () => setEditPerAppIndex(index)
@@ -431,13 +431,13 @@ const ListItem = () => {
avatar={editPerApp.avatar}
name={editPerApp.name}
managePer={{
mode: 'all',
permission: editPerApp.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerApp._id),
permissionList: AppPermissionList,
onUpdateCollaborators: (props: {
members?: string[];
groups?: string[];
orgs?: string[];
permission: number;
}) =>
postUpdateAppCollaborators({
@@ -448,6 +448,7 @@ const ListItem = () => {
props: RequireOnlyOne<{
tmbId?: string;
groupId?: string;
orgId?: string;
}>
) =>
deleteAppCollaborators({

View File

@@ -301,7 +301,6 @@ const MyApps = () => {
deleteTip={t('app:confirm_delete_folder_tip')}
onDelete={() => onDeleFolder(folderDetail._id)}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: AppPermissionList,
@@ -324,10 +323,12 @@ const MyApps = () => {
refreshDeps: [folderDetail._id, folderDetail.inheritPermission],
onDelOneCollaborator: async ({
tmbId,
groupId
groupId,
orgId
}: {
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
if (tmbId) {
return deleteAppCollaborators({
@@ -339,6 +340,11 @@ const MyApps = () => {
appId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteAppCollaborators({
appId: folderDetail._id,
orgId
});
}
}
}}

View File

@@ -135,7 +135,7 @@ const ApiDatasetForm = ({
</Flex>
<Input
bg={'myWhite.600'}
placeholder={'Token'}
placeholder={'User ID'}
maxLength={200}
{...register('yuqueServer.userId', { required: true })}
/>

View File

@@ -1,4 +1,4 @@
import { Box, Button, Flex, FormLabel } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import CollaboratorContextProvider, {
MemberManagerInputPropsType

View File

@@ -1,15 +1,12 @@
import React, { useEffect, useState } from 'react';
import { Box, Flex, Switch, Input } from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { postRebuildEmbedding } from '@/web/core/dataset/api';
import type { VectorModelItemType } from '@fastgpt/global/core/ai/model.d';
@@ -68,11 +65,6 @@ const Info = ({ datasetId }: { datasetId: string }) => {
title: t('common:common.confirm.Common Tip')
});
const { File } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const { runAsync: onSave } = useRequest2(
(data: DatasetItemType) => {
return updateDataset({
@@ -87,27 +79,6 @@ const Info = ({ datasetId }: { datasetId: string }) => {
}
);
const { runAsync: onSelectFile } = useRequest2(
(e: File[]) => {
const file = e[0];
if (!file) return Promise.resolve(null);
return compressImgFileAndUpload({
type: MongoImageTypeEnum.datasetAvatar,
file,
maxW: 300,
maxH: 300
});
},
{
onSuccess(src: string | null) {
if (src) {
setValue('avatar', src);
}
},
errorToast: t('common:common.avatar.Select Failed')
}
);
const { runAsync: onRebuilding } = useRequest2(
(vectorModel: VectorModelItemType) => {
return postRebuildEmbedding({
@@ -383,7 +354,6 @@ const Info = ({ datasetId }: { datasetId: string }) => {
<Box>
<MemberManager
managePer={{
mode: 'all',
permission: datasetDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(datasetId),
permissionList: DatasetPermissionList,
@@ -392,7 +362,7 @@ const Info = ({ datasetId }: { datasetId: string }) => {
...body,
datasetId
}),
onDelOneCollaborator: async ({ groupId, tmbId }) => {
onDelOneCollaborator: async ({ groupId, tmbId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId,
@@ -403,6 +373,11 @@ const Info = ({ datasetId }: { datasetId: string }) => {
datasetId,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId,
orgId
});
}
}
}}
@@ -411,7 +386,6 @@ const Info = ({ datasetId }: { datasetId: string }) => {
</>
)}
<File onSelect={onSelectFile} />
<ConfirmDelModal />
<ConfirmRebuildModal countDown={10} />
<ConfirmSyncScheduleModal />

View File

@@ -355,6 +355,9 @@ const TestHistories = React.memo(function TestHistories({
boxShadow: '1',
'& .delete': {
display: 'block'
},
'& .time': {
display: 'none'
}
}}
cursor={'pointer'}
@@ -381,16 +384,14 @@ const TestHistories = React.memo(function TestHistories({
<Box flex={1} mr={2} wordBreak={'break-all'} fontWeight={'400'}>
{item.text}
</Box>
<Box flex={'0 0 70px'}>
<Box className="time" flex={'0 0 auto'} fontSize={'xs'} color={'myGray.500'}>
{t(formatTimeToChatTime(item.time) as any).replace('#', ':')}
</Box>
<MyTooltip label={t('common:core.dataset.test.delete test history')}>
<Box w={'14px'} h={'14px'}>
<Box className="delete" display={'none'} w={'0.8rem'} h={'0.8rem'} ml={1}>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
display={'none'}
w={'0.8rem'}
_hover={{ color: 'red.600' }}
onClick={(e) => {
e.stopPropagation();

View File

@@ -1,9 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Box, Flex, Button, ModalFooter, ModalBody, Input, HStack } from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -15,7 +13,6 @@ import { postCreateDataset } from '@/web/core/dataset/api';
import type { CreateDatasetParams } from '@/global/core/dataset/api.d';
import { useTranslation } from 'next-i18next';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
@@ -90,33 +87,15 @@ const CreateModal = ({
const vectorModel = watch('vectorModel');
const agentModel = watch('agentModel');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: 'image/*',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.datasetAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar' as const, src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.avatar.Select Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
/* create a new kb and router to it */
const { run: onclickCreate, loading: creating } = useRequest2(
async (data: CreateDatasetParams) => await postCreateDataset(data),
@@ -275,7 +254,15 @@ const CreateModal = ({
<ComplianceTip pb={6} pt={0} px={9} type={'dataset'} />
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};

View File

@@ -1,5 +1,5 @@
import React, { useMemo, useRef, useState } from 'react';
import { resumeInheritPer } from '@/web/core/dataset/api';
import { postChangeOwner, resumeInheritPer } from '@/web/core/dataset/api';
import { Box, Flex, Grid, HStack } from '@chakra-ui/react';
import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
@@ -31,6 +31,7 @@ import { useTranslation } from 'next-i18next';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import SideTag from './SideTag';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal'));
@@ -156,6 +157,8 @@ function List() {
>
{formatDatasets.map((dataset, index) => {
const owner = members.find((v) => v.tmbId === dataset.tmbId);
const vectorModelAvatar = getModelProvider(dataset.vectorModel.provider)?.avatar;
return (
<MyTooltip
key={dataset._id}
@@ -281,7 +284,7 @@ function List() {
<HStack>
{isPc && dataset.type !== DatasetTypeEnum.folder && (
<HStack spacing={1} className="time">
<Avatar src={dataset.vectorModel.avatar} w={'0.85rem'} />
<Avatar src={vectorModelAvatar} w={'0.85rem'} />
<Box color={'myGray.500'} fontSize={'mini'}>
{dataset.vectorModel.name}
</Box>
@@ -347,7 +350,7 @@ function List() {
...(dataset.permission.hasManagePer
? [
{
icon: 'support/team/key',
icon: 'key',
label: t('common:permission.Permission'),
onClick: () => setEditPerDatasetIndex(index)
}
@@ -422,6 +425,12 @@ function List() {
{!!editPerDataset && (
<ConfigPerModal
onChangeOwner={(tmbId: string) =>
postChangeOwner({
datasetId: editPerDataset._id,
ownerId: tmbId
}).then(() => loadMyDatasets())
}
hasParent={!!parentId}
refetchResource={loadMyDatasets}
isInheritPermission={editPerDataset.inheritPermission}
@@ -431,7 +440,6 @@ function List() {
avatar={editPerDataset.avatar}
name={editPerDataset.name}
managePer={{
mode: 'all',
permission: editPerDataset.permission,
onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id),
permissionList: DatasetPermissionList,

View File

@@ -238,7 +238,6 @@ const Dataset = () => {
})
}
managePer={{
mode: 'all',
permission: folderDetail.permission,
onGetCollaboratorList: () => getCollaboratorList(folderDetail._id),
permissionList: DatasetPermissionList,
@@ -257,7 +256,7 @@ const Dataset = () => {
permission,
datasetId: folderDetail._id
}),
onDelOneCollaborator: async ({ tmbId, groupId }) => {
onDelOneCollaborator: async ({ tmbId, groupId, orgId }) => {
if (tmbId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
@@ -268,6 +267,11 @@ const Dataset = () => {
datasetId: folderDetail._id,
groupId
});
} else if (orgId) {
return deleteDatasetCollaborators({
datasetId: folderDetail._id,
orgId
});
}
},
refreshDeps: [folderDetail._id, folderDetail.inheritPermission]

View File

@@ -1,7 +1,7 @@
import React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { LoginPageTypeEnum, PasswordRule } from '@/web/support/user/login/constants';
import { postFindPassword } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { ResLogin } from '@/global/support/api/userRes.d';
@@ -70,6 +70,18 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
refreshDeps: [loginSuccess, t, toast]
}
);
const onSubmitErr = (err: Record<string, any>) => {
const val = Object.values(err)[0];
if (!val) return;
if (val.message) {
toast({
status: 'warning',
title: val.message,
duration: 3000,
isClosable: true
});
}
};
return (
<>
@@ -79,8 +91,8 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Box
mt={9}
onKeyDown={(e) => {
if (e.keyCode === 13 && !e.shiftKey && !requesting) {
handleSubmit(onclickFindPassword)();
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickFindPassword, onSubmitErr)();
}
}}
>
@@ -123,16 +135,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
bg={'myGray.50'}
type={'password'}
size={'lg'}
placeholder={t('user:password.new_password')}
placeholder={t('login:password_tip')}
{...register('password', {
required: t('user:password.password_required'),
minLength: {
value: 4,
message: t('user:password.password_condition')
},
maxLength: {
value: 20,
message: t('user:password.password_condition')
required: true,
pattern: {
value: PasswordRule,
message: t('login:password_tip')
}
})}
></Input>
@@ -160,7 +168,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
fontWeight={['medium', 'medium']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickFindPassword)}
onClick={handleSubmit(onclickFindPassword, onSubmitErr)}
>
{t('user:password.retrieve')}
</Button>

View File

@@ -1,4 +1,4 @@
import React, { useState, Dispatch, useCallback } from 'react';
import React, { Dispatch } from 'react';
import { FormControl, Flex, Input, Button, Box, Link } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
@@ -9,6 +9,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getDocPath } from '@/web/common/system/doc';
import { useTranslation } from 'next-i18next';
import FormLayout from './components/FormLayout';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
interface Props {
setPageType: Dispatch<`${LoginPageTypeEnum}`>;
@@ -30,31 +31,22 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
formState: { errors }
} = useForm<LoginFormType>();
const [requesting, setRequesting] = useState(false);
const onclickLogin = useCallback(
const { runAsync: onclickLogin, loading: requesting } = useRequest2(
async ({ username, password }: LoginFormType) => {
setRequesting(true);
try {
loginSuccess(
await postLogin({
username,
password
})
);
toast({
title: t('login:login_success'),
status: 'success'
});
} catch (error: any) {
toast({
title: error.message || t('login:login_failed'),
status: 'error'
});
}
setRequesting(false);
loginSuccess(
await postLogin({
username,
password
})
);
toast({
title: t('login:login_success'),
status: 'success'
});
},
[loginSuccess, t, toast]
{
refreshDeps: [loginSuccess]
}
);
const isCommunityVersion = !!(feConfigs?.register_method && !feConfigs?.isPlus);

View File

@@ -3,16 +3,16 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AbsoluteCenter, Box, Button, Flex } from '@chakra-ui/react';
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import { OAuthEnum } from '@fastgpt/global/support/user/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { customAlphabet } from 'nanoid';
import { useRouter } from 'next/router';
import { Dispatch, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import I18nLngSelector from '@/components/Select/I18nLngSelector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { useToast } from '@fastgpt/web/hooks/useToast';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
import { checkIsWecomTerminal } from '@fastgpt/global/support/user/login/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import Avatar from '@fastgpt/web/components/common/Avatar';
import dynamic from 'next/dynamic';
interface Props {
children: React.ReactNode;
@@ -20,17 +20,38 @@ interface Props {
pageType: `${LoginPageTypeEnum}`;
}
type OAuthItem = {
label: string;
provider: OAuthEnum | LoginPageTypeEnum;
icon: any;
pageType?: LoginPageTypeEnum;
redirectUrl?: string;
};
const FormLayout = ({ children, setPageType, pageType }: Props) => {
const { t } = useTranslation();
const router = useRouter();
const { setLoginStore, feConfigs } = useSystemStore();
const { lastRoute = '/app/list' } = router.query as { lastRoute: string };
const state = useRef(nanoid());
const redirectUri = `${location.origin}/login/provider`;
const { isPc } = useSystem();
const oAuthList = [
const { lastRoute = '/app/list' } = router.query as { lastRoute: string };
const state = useRef(getNanoid(8));
const redirectUri = `${location.origin}/login/provider`;
const isWecomWorkTerminal = checkIsWecomTerminal();
const oAuthList: OAuthItem[] = [
...(feConfigs?.sso?.url
? [
{
label: feConfigs.sso.title || 'Unknown',
provider: OAuthEnum.sso,
icon: feConfigs.sso.icon,
redirectUrl: `${feConfigs.sso.url}/login/oauth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state.current}`
}
]
: []),
...(feConfigs?.oauth?.wechat && pageType !== LoginPageTypeEnum.wechat
? [
{
@@ -82,6 +103,18 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
}
]
: []),
...(feConfigs?.oauth?.wecom
? [
{
label: t('login:wecom'),
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://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}`
}
]
: []),
...(pageType !== LoginPageTypeEnum.passwordLogin
? [
{
@@ -99,23 +132,32 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
[feConfigs?.sso?.url, oAuthList.length]
);
const onClickSso = useCallback(() => {
if (!feConfigs?.sso?.url) return;
setLoginStore({
provider: OAuthEnum.sso,
lastRoute,
state: state.current
});
const url = `${feConfigs.sso.url}/login/oauth/authorize?redirect_uri=${encodeURIComponent(redirectUri)}&state=${state.current}`;
window.open(url, '_self');
}, [feConfigs?.sso?.url, lastRoute, redirectUri, setLoginStore]);
const onClickOauth = useCallback(
async (item: OAuthItem) => {
if (item.redirectUrl) {
setLoginStore({
provider: item.provider as OAuthEnum,
lastRoute,
state: state.current
});
router.replace(item.redirectUrl, '_self');
}
item.pageType && setPageType(item.pageType);
},
[lastRoute, router, setLoginStore, setPageType]
);
useEffect(() => {
if (feConfigs?.sso?.autoLogin) {
onClickSso();
const sso = oAuthList.find((item) => item.provider === OAuthEnum.sso);
const wecom = oAuthList.find((item) => item.provider === OAuthEnum.wecom);
if (feConfigs?.sso?.autoLogin && sso) {
// sso auto
onClickOauth(sso);
} else if (isWecomWorkTerminal && wecom) {
// Auto wecom login
onClickOauth(wecom);
}
}, [feConfigs?.sso?.autoLogin]);
}, [feConfigs?.sso?.autoLogin, isWecomWorkTerminal, onClickOauth]);
return (
<Flex flexDirection={'column'} h={'100%'}>
@@ -158,37 +200,13 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
h={'40px'}
borderRadius={'sm'}
fontWeight={'medium'}
leftIcon={<MyIcon name={item.icon as any} w={'20px'} />}
onClick={() => {
item.redirectUrl &&
setLoginStore({
provider: item.provider,
lastRoute,
state: state.current
});
item.redirectUrl && router.replace(item.redirectUrl, '_self');
item.pageType && setPageType(item.pageType);
}}
leftIcon={<Avatar src={item.icon as any} w={'20px'} />}
onClick={() => onClickOauth(item)}
>
{item.label}
</Button>
</Box>
))}
{feConfigs?.sso?.url && (
<Box mt={4} color={'primary.700'} cursor={'pointer'} textAlign={'center'}>
<Button
variant={'whitePrimary'}
w={'100%'}
h={'40px'}
borderRadius={'sm'}
leftIcon={<MyImage alt="" src={feConfigs.sso.icon as any} w="20px" />}
onClick={onClickSso}
>
{feConfigs.sso.title}
</Button>
</Box>
)}
</Box>
</>
)}
@@ -196,4 +214,6 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
);
};
export default FormLayout;
export default dynamic(() => Promise.resolve(FormLayout), {
ssr: false
});

View File

@@ -1,7 +1,7 @@
import React, { Dispatch } from 'react';
import { FormControl, Box, Input, Button } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { LoginPageTypeEnum } from '@/web/support/user/login/constants';
import { LoginPageTypeEnum, PasswordRule } from '@/web/support/user/login/constants';
import { postRegister } from '@/web/support/user/api';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import type { ResLogin } from '@/global/support/api/userRes';
@@ -87,6 +87,18 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
refreshDeps: [loginSuccess, t, toast]
}
);
const onSubmitErr = (err: Record<string, any>) => {
const val = Object.values(err)[0];
if (!val) return;
if (val.message) {
toast({
status: 'warning',
title: val.message,
duration: 3000,
isClosable: true
});
}
};
const placeholder = feConfigs?.register_method
?.map((item) => {
@@ -108,7 +120,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
mt={9}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickRegister)();
handleSubmit(onclickRegister, onSubmitErr)();
}
}}
>
@@ -151,16 +163,12 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
bg={'myGray.50'}
size={'lg'}
type={'password'}
placeholder={t('user:password.new_password')}
placeholder={t('login:password_tip')}
{...register('password', {
required: t('user:password.password_required'),
minLength: {
value: 4,
message: t('user:password.password_condition')
},
maxLength: {
value: 20,
message: t('user:password.password_condition')
required: true,
pattern: {
value: PasswordRule,
message: t('login:password_tip')
}
})}
></Input>
@@ -175,7 +183,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
validate: (val) =>
getValues('password') === val ? true : t('user:password.not_match')
})}
></Input>
/>
</FormControl>
<Button
type="submit"
@@ -187,7 +195,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
fontWeight={['medium', 'medium']}
colorScheme="blue"
isLoading={requesting}
onClick={handleSubmit(onclickRegister)}
onClick={handleSubmit(onclickRegister, onSubmitErr)}
>
{t('user:register.confirm')}
</Button>

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { useRouter } from 'next/router';
import type { ResLogin } from '@/global/support/api/userRes.d';
import { useUserStore } from '@/web/support/user/useUserStore';
import { clearToken, setToken } from '@/web/support/user/auth';
import { clearToken } from '@/web/support/user/auth';
import { postFastLogin } from '@/web/support/user/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Loading from '@fastgpt/web/components/common/MyLoading';
@@ -24,7 +24,6 @@ const FastLogin = ({
const { t } = useTranslation();
const loginSuccess = useCallback(
(res: ResLogin) => {
setToken(res.token);
setUserInfo(res.user);
setTimeout(() => {

View File

@@ -16,10 +16,9 @@ import type { ResLogin } from '@/global/support/api/userRes.d';
import { useRouter } from 'next/router';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import LoginForm from './components/LoginForm/LoginForm';
import dynamic from 'next/dynamic';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { clearToken, setToken } from '@/web/support/user/auth';
import { clearToken } from '@/web/support/user/auth';
import Script from 'next/script';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useLocalStorageState, useMount } from 'ahooks';
@@ -29,6 +28,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { GET } from '@/web/common/api/request';
import { getDocPath } from '@/web/common/system/doc';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import LoginForm from './components/LoginForm/LoginForm';
const RegisterForm = dynamic(() => import('./components/RegisterForm'));
const ForgetPasswordForm = dynamic(() => import('./components/ForgetPasswordForm'));
@@ -42,7 +42,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
const { t } = useTranslation();
const { lastRoute = '' } = router.query as { lastRoute: string };
const { feConfigs } = useSystemStore();
const [pageType, setPageType] = useState<`${LoginPageTypeEnum}`>();
const [pageType, setPageType] = useState<`${LoginPageTypeEnum}`>(LoginPageTypeEnum.passwordLogin);
const { setUserInfo } = useUserStore();
const { setLastChatAppId } = useChatStore();
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -60,7 +60,6 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user);
setToken(res.token);
const decodeLastRoute = decodeURIComponent(lastRoute);
// 检查是否是当前的 route

View File

@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { ResLogin } from '@/global/support/api/userRes.d';
import { useUserStore } from '@/web/support/user/useUserStore';
import { clearToken, setToken } from '@/web/support/user/auth';
import { clearToken } from '@/web/support/user/auth';
import { oauthLogin } from '@/web/support/user/api';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Loading from '@fastgpt/web/components/common/MyLoading';
@@ -16,7 +16,7 @@ let isOauthLogging = false;
const provider = () => {
const { t } = useTranslation();
const { loginStore } = useSystemStore();
const { initd, loginStore, setLoginStore } = useSystemStore();
const { setUserInfo } = useUserStore();
const router = useRouter();
const { code, state, error } = router.query as { code: string; state: string; error?: string };
@@ -24,7 +24,6 @@ const provider = () => {
const loginSuccess = useCallback(
(res: ResLogin) => {
setToken(res.token);
setUserInfo(res.user);
router.push(loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/app/list');
@@ -34,13 +33,9 @@ const provider = () => {
const authCode = useCallback(
async (code: string) => {
if (!loginStore) {
router.replace('/login');
return;
}
try {
const res = await oauthLogin({
type: loginStore?.provider as `${OAuthEnum}`,
type: loginStore?.provider || OAuthEnum.sso,
code,
callbackUrl: `${location.origin}/login/provider`,
inviterId: localStorage.getItem('inviterId') || undefined,
@@ -76,8 +71,9 @@ const provider = () => {
router.replace('/login');
}, 1000);
}
setLoginStore(undefined);
},
[loginStore, loginSuccess, router, t, toast]
[loginStore?.provider, loginSuccess, router, setLoginStore, t, toast]
);
useEffect(() => {
@@ -90,8 +86,8 @@ const provider = () => {
return;
}
console.log('SSO', { loginStore, code, state });
if (!code || !loginStore) return;
console.log('SSO', { initd, loginStore, code, state });
if (!code || !initd) return;
if (isOauthLogging) return;
@@ -101,7 +97,7 @@ const provider = () => {
await clearToken();
router.prefetch('/app/list');
if (loginStore.provider !== OAuthEnum.sso && state !== loginStore?.state) {
if (loginStore && state !== loginStore.state) {
toast({
status: 'warning',
title: t('common:support.user.login.security_failed')
@@ -114,7 +110,7 @@ const provider = () => {
authCode(code);
}
})();
}, [authCode, code, error, loginStore, loginStore?.state, router, state, t, toast]);
}, [initd, authCode, code, error, loginStore, loginStore?.state, router, state, t, toast]);
return <Loading />;
};
@@ -123,6 +119,8 @@ export default provider;
export async function getServerSideProps(context: any) {
return {
props: { ...(await serviceSideProps(context)) }
props: {
...(await serviceSideProps(context))
}
};
}

View File

@@ -1,4 +1,4 @@
import { Box, Flex, Grid, Button } from '@chakra-ui/react';
import { Box, Flex, Grid, Button, VStack } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useState } from 'react';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -11,7 +11,7 @@ import { BillTypeEnum } from '@fastgpt/global/support/wallet/bill/constants';
import QRCodePayModal, { type QRPayProps } from '@/components/support/wallet/QRCodePayModal';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
const ExtraPlan = () => {
const ExtraPlan = ({ onPaySuccess }: { onPaySuccess?: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { subPlans } = useSystemStore();
@@ -108,19 +108,8 @@ const ExtraPlan = () => {
);
return (
<Flex
mt={['40px', '100px']}
flexDirection={'column'}
alignItems={'center'}
position={'relative'}
>
<Box id={'extra-plan'} fontWeight={'bold'} fontSize={['24px', '36px']} color={'myGray.900'}>
{t('common:support.wallet.subscription.Extra plan')}
</Box>
<Box mt={2} mb={8} color={'myGray.600'} fontSize={'md'}>
{t('common:support.wallet.subscription.Extra plan tip')}
</Box>
<Grid mt={8} gridTemplateColumns={['1fr', '1fr 1fr']} gap={5} w={['100%', 'auto']}>
<VStack>
<Grid gridTemplateColumns={['1fr', '1fr 1fr']} gap={5} w={['100%', 'auto']}>
<Box
bg={'rgba(255, 255, 255, 0.90)'}
px={'32px'}
@@ -284,8 +273,8 @@ const ExtraPlan = () => {
</Box>
</Grid>
{!!qrPayData && <QRCodePayModal {...qrPayData} />}
</Flex>
{!!qrPayData && <QRCodePayModal onSuccess={onPaySuccess} {...qrPayData} />}
</VStack>
);
};

View File

@@ -20,10 +20,10 @@ export enum PackageChangeStatusEnum {
const Standard = ({
standardPlan: myStandardPlan,
refetchTeamSubPlan
onPaySuccess
}: {
standardPlan?: TeamSubSchema;
refetchTeamSubPlan: () => void;
onPaySuccess?: () => void;
}) => {
const { t } = useTranslation();
@@ -78,14 +78,6 @@ const Standard = ({
return (
<>
<Flex flexDirection={'column'} alignItems={'center'} position={'relative'}>
<Box fontWeight={'600'} color={'myGray.900'} fontSize={['24px', '36px']}>
{t('common:support.wallet.subscription.Sub plan')}
</Box>
<Box mt={8} mb={10} fontWeight={'500'} color={'myGray.600'} fontSize={'md'}>
{t('common:support.wallet.subscription.Sub plan tip', {
title: feConfigs?.systemTitle
})}
</Box>
<Box>
<RowTabs
list={[
@@ -96,8 +88,14 @@ const Standard = ({
{
label: (
<Flex>
{t('common:support.wallet.subscription.mode.Year')}
<Box ml={1} color={selectSubMode === SubModeEnum.month ? 'red.600' : 'auto'}>
<Box whiteSpace={'nowrap'}>
{t('common:support.wallet.subscription.mode.Year')}
</Box>
<Box
whiteSpace={'nowrap'}
ml={1}
color={selectSubMode === SubModeEnum.month ? 'red.600' : 'auto'}
>
({t('common:support.wallet.subscription.mode.Year sale')})
</Box>
</Flex>
@@ -271,15 +269,13 @@ const Standard = ({
</Grid>
{!!qrPayData && packageChange && (
<QRCodePayModal tip={packagePayTextMap[packageChange]} {...qrPayData} />
<QRCodePayModal
tip={packagePayTextMap[packageChange]}
onSuccess={onPaySuccess}
{...qrPayData}
/>
)}
</Flex>
<HStack mt={8} color={'blue.700'} ml={8}>
<MyIcon name={'infoRounded'} w={'1rem'} />
<Box fontSize={'sm'} fontWeight={'500'}>
{t('user:bill.standard_valid_tip')}
</Box>
</HStack>
</>
);
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, HStack, VStack } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamPlanStatus } from '@/web/support/user/team/api';
import { useQuery } from '@tanstack/react-query';
@@ -9,50 +9,75 @@ import StandardPlan from './components/Standard';
import ExtraPlan from './components/ExtraPlan';
import PointsCard from './components/Points';
import FAQ from './components/FAQ';
import { getToken } from '@/web/support/user/auth';
import Script from 'next/script';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRouter } from 'next/router';
const PriceBox = () => {
const { userInfo } = useUserStore();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const router = useRouter();
const { data: teamSubPlan, refetch: refetchTeamSubPlan } = useQuery(
['getTeamPlanStatus'],
getTeamPlanStatus,
{
enabled: !!getToken() || !!userInfo
}
);
const { data: teamSubPlan } = useQuery(['getTeamPlanStatus'], getTeamPlanStatus, {
enabled: !!userInfo
});
const onPaySuccess = () => {
setTimeout(() => {
router.reload();
}, 1000);
};
return (
<>
<Script src={getWebReqUrl('/js/qrcode.min.js')} strategy="lazyOnload"></Script>
<Flex
h={'100%'}
flexDir={'column'}
overflow={'overlay'}
w={'100%'}
px={['20px', '5vw']}
py={['30px', '80px']}
bg={`linear-gradient(to right, #F8F8FD00, #F7F7FF),url(/imgs/priceBg.svg)`}
backgroundSize={'cover'}
backgroundRepeat={'no-repeat'}
>
{/* standard sub */}
<StandardPlan
standardPlan={teamSubPlan?.standard}
refetchTeamSubPlan={refetchTeamSubPlan}
/>
<Flex
h={'100%'}
flexDir={'column'}
overflow={'overlay'}
w={'100%'}
px={['20px', '5vw']}
py={['30px', '80px']}
bg={`linear-gradient(to right, #F8F8FD00, #F7F7FF),url(/imgs/priceBg.svg)`}
backgroundSize={'cover'}
backgroundRepeat={'no-repeat'}
>
{/* standard sub */}
<VStack>
<Box fontWeight={'600'} color={'myGray.900'} fontSize={['24px', '36px']}>
{t('common:support.wallet.subscription.Sub plan')}
</Box>
<Box mt={8} mb={10} fontWeight={'500'} color={'myGray.600'} fontSize={'md'}>
{t('common:support.wallet.subscription.Sub plan tip', {
title: feConfigs?.systemTitle
})}
</Box>
<StandardPlan standardPlan={teamSubPlan?.standard} onPaySuccess={onPaySuccess} />
<HStack mt={8} color={'blue.700'} ml={8}>
<MyIcon name={'infoRounded'} w={'1rem'} />
<Box fontSize={'sm'} fontWeight={'500'}>
{t('user:bill.standard_valid_tip')}
</Box>
</HStack>
</VStack>
<ExtraPlan />
{/* extra plan */}
<VStack mt={['40px', '100px']} mb={8}>
<Box id={'extra-plan'} fontWeight={'bold'} fontSize={['24px', '36px']} color={'myGray.900'}>
{t('common:support.wallet.subscription.Extra plan')}
</Box>
<Box mt={2} mb={8} color={'myGray.600'} fontSize={'md'}>
{t('common:support.wallet.subscription.Extra plan tip')}
</Box>
<ExtraPlan onPaySuccess={onPaySuccess} />
</VStack>
{/* points */}
<PointsCard />
{/* points */}
<PointsCard />
{/* question */}
<FAQ />
</Flex>
</>
{/* question */}
<FAQ />
</Flex>
);
};

View File

@@ -1,5 +1,5 @@
import { initHttpAgent } from '@fastgpt/service/common/middle/httpAgent';
import { existsSync, readdirSync, readFileSync } from 'fs';
import fs, { existsSync, readdirSync } from 'fs';
import type { FastGPTFeConfigsType } from '@fastgpt/global/common/system/types/index.d';
import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/index.d';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
@@ -13,7 +13,7 @@ import { defaultGroup, defaultTemplateTypes } from '@fastgpt/web/core/workflow/c
import { MongoPluginGroups } from '@fastgpt/service/core/app/plugin/pluginGroupSchema';
import { MongoTemplateTypes } from '@fastgpt/service/core/app/templates/templateTypeSchema';
export const readConfigData = (name: string) => {
export const readConfigData = async (name: string) => {
const splitName = name.split('.');
const devName = `${splitName[0]}.local.${splitName[1]}`;
@@ -30,7 +30,7 @@ export const readConfigData = (name: string) => {
return `/app/data/${name}`;
})();
const content = readFileSync(filename, 'utf-8');
const content = await fs.promises.readFile(filename, 'utf-8');
return content;
};
@@ -120,13 +120,13 @@ export async function initSystemConfig() {
});
}
function getSystemVersion() {
async function getSystemVersion() {
if (global.systemVersion) return;
try {
if (process.env.NODE_ENV === 'development') {
global.systemVersion = process.env.npm_package_version || '0.0.0';
} else {
const packageJson = json5.parse(readFileSync('/app/package.json', 'utf-8'));
const packageJson = json5.parse(await fs.promises.readFile('/app/package.json', 'utf-8'));
global.systemVersion = packageJson?.version;
}
@@ -138,7 +138,7 @@ function getSystemVersion() {
}
}
function getSystemPlugin() {
async function getSystemPlugin() {
if (global.communityPlugins && global.communityPlugins.length > 0) return;
const basePath =
@@ -149,15 +149,17 @@ function getSystemPlugin() {
const filterFiles = files.filter((item) => item.endsWith('.json'));
// read json file
const fileTemplates = filterFiles.map<SystemPluginTemplateItemType>((filename) => {
const content = readFileSync(`${basePath}/${filename}`, 'utf-8');
return {
...json5.parse(content),
originCost: 0,
currentCost: 0,
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`
};
});
const fileTemplates = await Promise.all(
filterFiles.map<Promise<SystemPluginTemplateItemType>>(async (filename) => {
const content = await fs.promises.readFile(`${basePath}/${filename}`, 'utf-8');
return {
...json5.parse(content),
originCost: 0,
currentCost: 0,
id: `${PluginSourceEnum.community}-${filename.replace('.json', '')}`
};
})
);
fileTemplates.sort((a, b) => (b.weight || 0) - (a.weight || 0));

View File

@@ -0,0 +1,5 @@
export type CreateModelParams = {
name: string;
description: string;
prompt: string;
};

View File

@@ -0,0 +1,66 @@
import { addLog } from '@fastgpt/service/common/system/log';
import axios, { Method } from 'axios';
const url = process.env.API_PROXY_URL;
const token = process.env.API_PROXY_TOKEN;
const instance = axios.create({
baseURL: url,
timeout: 60000, // 超时时间
headers: {
Authorization: `Bearer ${token}`
}
});
/**
* 响应数据检查
*/
const checkRes = (data: any) => {
if (data === undefined) {
addLog.info('api proxy data is empty');
return Promise.reject('服务器异常');
}
return data.data;
};
const responseError = (err: any) => {
console.log('error->', '请求错误', err);
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof err.message === 'string') {
return Promise.reject({ message: err.message });
}
if (typeof err.data === 'string') {
return Promise.reject({ message: err.data });
}
if (err?.response?.data) {
return Promise.reject(err?.response?.data);
}
return Promise.reject(err);
};
const request = <T>(url: string, data: any, method: Method): Promise<T> => {
/* 去空 */
for (const key in data) {
if (data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
url,
method,
data: ['POST', 'PUT'].includes(method) ? data : undefined,
params: !['POST', 'PUT'].includes(method) ? data : undefined
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
};
// TODO: channel crud
export const ApiProxy = {};

View File

@@ -21,7 +21,9 @@ export const getSystemPlugins = async (refresh = false) => {
global.systemPlugins = [];
}
global.systemPlugins = FastGPTProUrl ? await getCommercialPlugins() : getCommunityPlugins();
global.systemPlugins = FastGPTProUrl
? await getCommercialPlugins()
: await getCommunityPlugins();
addLog.info(`Load system plugin successfully: ${global.systemPlugins.length}`);

View File

@@ -12,6 +12,7 @@ import { DatasetDataItemType } from '@fastgpt/global/core/dataset/type';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ClientSession } from '@fastgpt/service/common/mongo';
import { MongoDatasetDataText } from '@fastgpt/service/core/dataset/data/dataTextSchema';
/* insert data.
* 1. create data id
@@ -42,7 +43,8 @@ export async function insertData2Dataset({
const qaStr = getDefaultIndex({ q, a }).text;
// empty indexes check, if empty, create default index
// 1. Get vector indexes and insert
// Empty indexes check, if empty, create default index
indexes =
Array.isArray(indexes) && indexes.length > 0
? indexes.map((index) => ({
@@ -77,7 +79,7 @@ export async function insertData2Dataset({
)
);
// create mongo data
// 2. Create mongo data
const [{ _id }] = await MongoDatasetData.create(
[
{
@@ -87,7 +89,8 @@ export async function insertData2Dataset({
collectionId,
q,
a,
fullTextToken: jiebaSplit({ text: qaStr }),
// FullText tmp
// fullTextToken: jiebaSplit({ text: qaStr }),
chunkIndex,
indexes: indexes?.map((item, i) => ({
...item,
@@ -98,6 +101,20 @@ export async function insertData2Dataset({
{ session }
);
// 3. Create mongo data text
await MongoDatasetDataText.create(
[
{
teamId,
datasetId,
collectionId,
dataId: _id,
fullTextToken: jiebaSplit({ text: qaStr })
}
],
{ session }
);
return {
insertId: _id,
tokens: result.reduce((acc, cur) => acc + cur.tokens, 0)
@@ -225,11 +242,19 @@ export async function updateData2Dataset({
// update mongo other data
mongoData.q = q || mongoData.q;
mongoData.a = a ?? mongoData.a;
mongoData.fullTextToken = jiebaSplit({ text: mongoData.q + mongoData.a });
// FullText tmp
// mongoData.fullTextToken = jiebaSplit({ text: `${mongoData.q}\n${mongoData.a}`.trim() });
// @ts-ignore
mongoData.indexes = newIndexes;
await mongoData.save({ session });
// update mongo data text
await MongoDatasetDataText.updateOne(
{ dataId: mongoData._id },
{ fullTextToken: jiebaSplit({ text: `${mongoData.q}\n${mongoData.a}`.trim() }) },
{ session }
);
// delete vector
const deleteIdList = patchResult
.filter((item) => item.type === 'delete' || item.type === 'update')

View File

@@ -166,9 +166,9 @@ const rebuildData = async ({
// get new mongoData insert to training
const newRebuildingData = await MongoDatasetData.findOneAndUpdate(
{
rebuilding: true,
teamId: mongoData.teamId,
datasetId: mongoData.datasetId,
rebuilding: true
datasetId: mongoData.datasetId
},
{
$unset: {

View File

@@ -42,7 +42,7 @@ export async function initRootUser(retry = 3): Promise<any> {
rootId = _id;
}
// init root team
await createDefaultTeam({ userId: rootId, balance: 9999 * PRICE_SCALE, session });
await createDefaultTeam({ userId: rootId, session });
});
console.log(`root user init:`, {

View File

@@ -220,7 +220,7 @@ export const streamFetch = ({
});
} else if (event === SseResponseEventEnum.error) {
if (parseJson.statusText === TeamErrEnum.aiPointsNotEnough) {
useSystemStore.getState().setIsNotSufficientModal(true);
useSystemStore.getState().setNotSufficientModalType(TeamErrEnum.aiPointsNotEnough);
}
errMsg = getErrText(parseJson, '流响应错误');
}

View File

@@ -9,6 +9,7 @@ import { TOKEN_ERROR_CODE } from '@fastgpt/global/common/error/errorCode';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useSystemStore } from '../system/useSystemStore';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { i18nT } from '@fastgpt/web/i18n/utils';
interface ConfigType {
headers?: { [key: string]: string };
@@ -108,20 +109,23 @@ function responseError(err: any) {
return Promise.reject({ message: err });
}
// 有报错响应
if (err?.code in TOKEN_ERROR_CODE) {
if (
!(window.location.pathname === '/chat/share' || window.location.pathname === '/chat/team')
) {
if (err?.code in TOKEN_ERROR_CODE || err?.response?.data?.code in TOKEN_ERROR_CODE) {
if (!['/chat/share', '/chat/team', '/login'].includes(window.location.pathname)) {
clearToken();
window.location.replace(
getWebReqUrl(`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`)
);
}
return Promise.reject({ message: '无权操作' });
return Promise.reject({ message: i18nT('common:unauth_token') });
}
if (err?.statusText === TeamErrEnum.aiPointsNotEnough) {
useSystemStore.getState().setIsNotSufficientModal(true);
if (
err?.statusText === TeamErrEnum.aiPointsNotEnough ||
err?.statusText === TeamErrEnum.datasetSizeNotEnough ||
err?.statusText === TeamErrEnum.datasetAmountNotEnough ||
err?.statusText === TeamErrEnum.appAmountNotEnough
) {
useSystemStore.getState().setNotSufficientModalType(err.statusText);
return Promise.reject(err);
}
if (err?.response?.data) {

View File

@@ -35,30 +35,11 @@ export const uploadFile2DB = ({
});
};
export const getUploadBase64ImgController = (
props: CompressImgProps & UploadImgProps,
retry = 3
): Promise<string> => {
try {
return compressBase64ImgAndUpload({
maxW: 4000,
maxH: 4000,
maxSize: 1024 * 1024 * 5,
...props
});
} catch (error) {
if (retry > 0) {
return getUploadBase64ImgController(props, retry - 1);
}
return Promise.reject(error);
}
};
/**
* compress image. response base64
* @param maxSize The max size of the compressed image
*/
export const compressBase64ImgAndUpload = async ({
const compressBase64ImgAndUpload = async ({
base64Img,
maxW,
maxH,
@@ -89,7 +70,7 @@ export const compressImgFileAndUpload = async ({
reader.readAsDataURL(file);
const base64Img = await new Promise<string>((resolve, reject) => {
reader.onload = async () => {
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = (err) => {

View File

@@ -3,12 +3,17 @@ import { Box } from '@chakra-ui/react';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useI18n } from '@/web/context/I18n';
import { useMemoizedFn } from 'ahooks';
import { compressImgFileAndUpload } from '../controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
export const useSelectFile = (props?: {
fileType?: string;
multiple?: boolean;
maxCount?: number;
}) => {
const { t } = useTranslation();
const { fileT } = useI18n();
const { fileType = '*', multiple = false, maxCount = 10 } = props || {};
const { toast } = useToast();
@@ -48,8 +53,44 @@ export const useSelectFile = (props?: {
SelectFileDom.current && SelectFileDom.current.click();
}, []);
const { runAsync: onSelectImage, loading } = useRequest2(
async (
e: File[],
{
maxW,
maxH,
callback
}: {
maxW?: number;
maxH?: number;
callback?: (e: string) => any;
}
) => {
const file = e[0];
if (!file) return Promise.resolve('Can not found image');
try {
const src = await compressImgFileAndUpload({
file,
maxW,
maxH
});
console.log(src, '--');
callback?.(src);
return src;
} catch (err: any) {
toast({
title: getErrText(err, t('common:error.upload_image_error')),
status: 'warning'
});
return Promise.reject(getErrText(err, t('common:error.upload_image_error')));
}
}
);
return {
File,
onOpen
onOpen,
onSelectImage,
loading
};
};

View File

@@ -14,9 +14,17 @@ import { InitDateResponse } from '@/global/common/api/systemRes';
import { FastGPTFeConfigsType } from '@fastgpt/global/common/system/types';
import { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
import { defaultWhisperModel } from '@fastgpt/global/core/ai/model';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
type LoginStoreType = { provider: `${OAuthEnum}`; lastRoute: string; state: string };
export type NotSufficientModalType =
| TeamErrEnum.datasetSizeNotEnough
| TeamErrEnum.aiPointsNotEnough
| TeamErrEnum.datasetAmountNotEnough
| TeamErrEnum.teamMemberOverSize
| TeamErrEnum.appAmountNotEnough;
type State = {
initd: boolean;
setInitd: () => void;
@@ -27,14 +35,15 @@ type State = {
setLastAppListRouteType: (e?: string) => void;
loginStore?: LoginStoreType;
setLoginStore: (e: LoginStoreType) => void;
setLoginStore: (e?: LoginStoreType) => void;
loading: boolean;
setLoading: (val: boolean) => null;
gitStar: number;
loadGitStar: () => Promise<void>;
isNotSufficientModal: boolean;
setIsNotSufficientModal: (val: boolean) => void;
notSufficientModalType?: NotSufficientModalType;
setNotSufficientModalType: (val?: NotSufficientModalType) => void;
initDataBufferId?: string;
feConfigs: FastGPTFeConfigsType;
@@ -105,10 +114,10 @@ export const useSystemStore = create<State>()(
} catch (error) {}
},
isNotSufficientModal: false,
setIsNotSufficientModal(val: boolean) {
notSufficientModalType: undefined,
setNotSufficientModalType(type) {
set((state) => {
state.isNotSufficientModal = val;
state.notSufficientModalType = type;
});
},

View File

@@ -67,6 +67,7 @@ import type {
listExistIdResponse
} from '@/pages/api/core/dataset/apiDataset/listExistId';
import { FeishuServer, YuqueServer } from '@fastgpt/global/core/dataset/apiDataset';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
/* ======================== dataset ======================= */
export const getDatasets = (data: GetDatasetListBody) =>
@@ -100,6 +101,9 @@ export const postCreateDatasetFolder = (data: DatasetFolderCreateBody) =>
export const resumeInheritPer = (datasetId: string) =>
GET(`/core/dataset/resumeInheritPermission`, { datasetId });
export const postChangeOwner = (data: { ownerId: string; datasetId: string }) =>
POST(`/proApi/core/dataset/changeOwner`, data);
/* =========== search test ============ */
export const postSearchText = (data: SearchTestProps) =>
POST<SearchTestResponse>(`/core/dataset/searchTest`, data);

View File

@@ -1,20 +1,9 @@
import { loginOut } from '@/web/support/user/api';
const tokenKey = 'token';
export const clearToken = () => {
try {
localStorage.removeItem(tokenKey);
return loginOut();
} catch (error) {
error;
}
};
export const setToken = (token: string) => {
if (typeof window === 'undefined') return '';
localStorage.setItem(tokenKey, token);
};
export const getToken = () => {
if (typeof window === 'undefined') return '';
return localStorage.getItem(tokenKey) || '';
};

View File

@@ -43,12 +43,12 @@ export const useSendCode = ({ type }: { type: `${UserAuthTypeEnum}` }) => {
const sendCodeText = useMemo(() => {
if (codeSending) return t('common:support.user.auth.Sending Code');
if (codeCountDown >= 10) {
return `${codeCountDown}${t('user:password.get_code_again')}`;
return `${codeCountDown}${t('common:support.user.auth.get_code_again')}`;
}
if (codeCountDown > 0) {
return `0${codeCountDown}${t('user:password.get_code_again')}`;
return `0${codeCountDown}${t('common:support.user.auth.get_code_again')}`;
}
return t('user:password.get_code');
return t('common:support.user.auth.get_code');
}, [codeCountDown, codeSending, t]);
const {

View File

@@ -4,3 +4,6 @@ export enum LoginPageTypeEnum {
forgetPassword = 'forgetPassword',
wechat = 'wechat'
}
export const PasswordRule =
/^(?:(?=.*\d)(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])|(?=.*\d)(?=.*[!@#$%^&*_])|(?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[!@#$%^&*_])|(?=.*[A-Z])(?=.*[!@#$%^&*_]))[\dA-Za-z!@#$%^&*_]{6,}$/;

View File

@@ -1,5 +1,9 @@
import { GET, POST, PUT, DELETE } from '@/web/common/api/request';
import { UpdatePermissionBody } from '@fastgpt/global/support/permission/collaborator';
import {
CollaboratorItemType,
DeletePermissionQuery,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import {
CreateTeamProps,
InviteMemberProps,
@@ -15,7 +19,6 @@ import {
} from '@fastgpt/global/support/user/team/type.d';
import { FeTeamPlanStatusType, TeamSubSchema } from '@fastgpt/global/support/wallet/sub/type';
import { TeamInvoiceHeaderType } from '@fastgpt/global/support/user/team/type';
import { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
/* --------------- team ---------------- */
export const getTeamList = (status: `${TeamMemberSchema['status']}`) =>
@@ -37,15 +40,15 @@ 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 delLeaveTeam = (teamId: string) =>
DELETE('/proApi/support/user/team/member/leave', { teamId });
export const getTeamClbs = () =>
GET<ResourcePermissionType[]>(`/proApi/support/user/team/collaborator/list`);
export const delLeaveTeam = () => DELETE('/proApi/support/user/team/member/leave');
/* -------------- team collaborator -------------------- */
export const updateMemberPermission = (data: UpdatePermissionBody) =>
PUT('/proApi/support/user/team/collaborator/updatePermission', data);
export const getTeamClbs = () =>
GET<CollaboratorItemType[]>(`/proApi/support/user/team/collaborator/list`);
export const updateMemberPermission = (data: UpdateClbPermissionProps) =>
PUT('/proApi/support/user/team/collaborator/update', data);
export const deleteMemberPermission = (id: DeletePermissionQuery) =>
DELETE('/proApi/support/user/team/collaborator/delete', id);
/* --------------- team tags ---------------- */
export const getTeamsTags = () => GET<TeamTagSchema[]>(`/proApi/support/user/team/tag/list`);

View File

@@ -0,0 +1,30 @@
import { DELETE, GET, POST, PUT } from '@/web/common/api/request';
import type {
postCreateOrgData,
putUpdateOrgData,
putUpdateOrgMembersData
} from '@fastgpt/global/support/user/team/org/api';
import type { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { putMoveOrgType } from '@fastgpt/global/support/user/team/org/api';
export const getOrgList = () => GET<OrgType[]>('/proApi/support/user/team/org/list');
export const postCreateOrg = (data: postCreateOrgData) =>
POST('/proApi/support/user/team/org/create', data);
export const deleteOrg = (orgId: string) =>
DELETE('/proApi/support/user/team/org/delete', { orgId });
export const deleteOrgMember = (orgId: string, tmbId: string) =>
DELETE('/proApi/support/user/team/org/deleteMember', { orgId, tmbId });
export const putMoveOrg = (data: putMoveOrgType) => PUT('/proApi/support/user/team/org/move', data);
export const putUpdateOrg = (data: putUpdateOrgData) =>
PUT('/proApi/support/user/team/org/update', data);
export const putUpdateOrgMembers = (data: putUpdateOrgMembersData) =>
PUT('/proApi/support/user/team/org/updateMembers', data);
// export const putChnageOrgOwner = (data: putChnageOrgOwnerData) =>
// PUT('/proApi/support/user/team/org/changeOwner', data);

View File

@@ -1,22 +1,28 @@
import type { UserUpdateParams } from '@/types/user';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import type { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import type { OrgMemberSchemaType, OrgType } from '@fastgpt/global/support/user/team/org/type';
import type { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import type { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserUpdateParams } from '@/types/user';
import type { UserType } from '@fastgpt/global/support/user/type.d';
import { getTokenLogin, putUserInfo } from '@/web/support/user/api';
import { FeTeamPlanStatusType } from '@fastgpt/global/support/wallet/sub/type';
import { getTeamPlanStatus } from './team/api';
import { getTeamMembers } from '@/web/support/user/team/api';
import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { getGroupList } from './team/group/api';
import { getOrgList } from './team/org/api';
type State = {
systemMsgReadId: string;
setSysMsgReadId: (id: string) => void;
isUpdateNotification: boolean;
setIsUpdateNotification: (val: boolean) => void;
userInfo: UserType | null;
isTeamAdmin: boolean;
initUserInfo: () => Promise<UserType>;
setUserInfo: (user: UserType | null) => void;
updateUserInfo: (user: UserUpdateParams) => Promise<void>;
@@ -30,6 +36,10 @@ type State = {
teamMemberGroups: MemberGroupListType;
myGroups: MemberGroupListType;
loadAndGetGroups: (init?: boolean) => Promise<MemberGroupListType>;
teamOrgs: OrgType[];
myOrgs: OrgType[];
loadAndGetOrgs: (init?: boolean) => Promise<OrgType[]>;
};
export const useUserStore = create<State>()(
@@ -43,7 +53,15 @@ export const useUserStore = create<State>()(
});
},
isUpdateNotification: true,
setIsUpdateNotification(val: boolean) {
set((state) => {
state.isUpdateNotification = val;
});
},
userInfo: null,
isTeamAdmin: false,
async initUserInfo() {
get().initTeamPlanStatus();
@@ -61,6 +79,7 @@ export const useUserStore = create<State>()(
setUserInfo(user: UserType | null) {
set((state) => {
state.userInfo = user ? user : null;
state.isTeamAdmin = !!user?.team?.permission?.hasManagePer;
});
},
async updateUserInfo(user: UserUpdateParams) {
@@ -107,6 +126,7 @@ export const useUserStore = create<State>()(
return res;
},
teamMemberGroups: [],
teamOrgs: [],
myGroups: [],
loadAndGetGroups: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
@@ -123,13 +143,31 @@ export const useUserStore = create<State>()(
);
});
return res;
},
myOrgs: [],
loadAndGetOrgs: async (init = false) => {
if (!useSystemStore.getState()?.feConfigs?.isPlus) return [];
const randomRefresh = Math.random() > 0.7;
if (!randomRefresh && !init && get().myOrgs.length) return Promise.resolve(get().myOrgs);
const res = await getOrgList();
set((state) => {
state.teamOrgs = res;
state.myOrgs = res.filter((item) =>
item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId))
);
});
return res;
}
})),
{
name: 'userStore',
partialize: (state) => ({
systemMsgReadId: state.systemMsgReadId
systemMsgReadId: state.systemMsgReadId,
isUpdateNotification: state.isUpdateNotification
})
}
)