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:
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,7 +44,7 @@ const ConfigPerModal = ({
|
||||
<>
|
||||
<MyModal
|
||||
isOpen
|
||||
iconSrc="/imgs/modal/key.svg"
|
||||
iconSrc="keyPrimary"
|
||||
onClose={onClose}
|
||||
title={t('common:permission.Permission config')}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}>);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ const AccountContainer = ({
|
||||
...(userInfo?.team?.permission.hasManagePer
|
||||
? [
|
||||
{
|
||||
icon: 'support/outlink/apikeyLight',
|
||||
icon: 'key',
|
||||
label: t('account:api_key'),
|
||||
value: TabEnum.apikey
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TeamSelector = ({
|
||||
key={'manage'}
|
||||
alignItems={'center'}
|
||||
borderRadius={'md'}
|
||||
cursor={'default'}
|
||||
cursor={'pointer'}
|
||||
gap={3}
|
||||
onClick={() => router.push('/account/team')}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
123
projects/app/src/pages/api/admin/initv4818.ts
Normal file
123
projects/app/src/pages/api/admin/initv4818.ts
Normal 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
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
25
projects/app/src/pages/api/common/system/writefile.ts
Normal file
25
projects/app/src/pages/api/common/system/writefile.ts
Normal 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);
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -135,7 +135,7 @@ const ApiDatasetForm = ({
|
||||
</Flex>
|
||||
<Input
|
||||
bg={'myWhite.600'}
|
||||
placeholder={'Token'}
|
||||
placeholder={'User ID'}
|
||||
maxLength={200}
|
||||
{...register('yuqueServer.userId', { required: true })}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
5
projects/app/src/service/core/ai/apiproxy.d.ts
vendored
Normal file
5
projects/app/src/service/core/ai/apiproxy.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export type CreateModelParams = {
|
||||
name: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
};
|
||||
66
projects/app/src/service/core/ai/apiproxy.ts
Normal file
66
projects/app/src/service/core/ai/apiproxy.ts
Normal 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 = {};
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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:`, {
|
||||
|
||||
@@ -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, '流响应错误');
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) || '';
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,}$/;
|
||||
|
||||
@@ -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`);
|
||||
|
||||
30
projects/app/src/web/support/user/team/org/api.ts
Normal file
30
projects/app/src/web/support/user/team/org/api.ts
Normal 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);
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user