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:
@@ -81,6 +81,9 @@ COPY --from=builder /app/projects/app/package.json ./package.json
|
||||
COPY ./projects/app/data /app/data
|
||||
RUN chown -R nextjs:nodejs /app/data
|
||||
|
||||
# Add tmp directory permission control
|
||||
RUN mkdir -p /tmp && chmod 666 /tmp
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.8.17",
|
||||
"version": "4.8.18",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
10
projects/app/public/imgs/avatar/defaultOrgAvatar.svg
Normal file
10
projects/app/public/imgs/avatar/defaultOrgAvatar.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="36" height="36" fill="url(#paint0_linear_13996_748)"/>
|
||||
<path d="M14.904 10.8976C14.904 10.2323 14.904 9.89961 15.0335 9.64549C15.1474 9.42195 15.3291 9.24021 15.5527 9.12631C15.8068 8.99683 16.1395 8.99683 16.8048 8.99683H19.1952C19.8605 8.99683 20.1932 8.99683 20.4473 9.12631C20.6709 9.24021 20.8526 9.42195 20.9665 9.64549C21.096 9.89961 21.096 10.2323 21.096 10.8976V12.598C21.096 13.2633 21.096 13.596 20.9665 13.8501C20.8526 14.0737 20.6709 14.2554 20.4473 14.3693C20.1932 14.4988 19.8605 14.4988 19.1952 14.4988H18.8506V17.1972H23.215C24.1296 17.1972 24.871 17.9386 24.871 18.8532V21.4949H25.0992C25.7645 21.4949 26.0972 21.4949 26.3513 21.6244C26.5749 21.7383 26.7566 21.92 26.8705 22.1435C27 22.3977 27 22.7303 27 23.3957V25.096C27 25.7614 27 26.0941 26.8705 26.3482C26.7566 26.5717 26.5749 26.7535 26.3513 26.8674C26.0972 26.9968 25.7645 26.9968 25.0992 26.9968H22.7088C22.0435 26.9968 21.7108 26.9968 21.4567 26.8674C21.2331 26.7535 21.0514 26.5717 20.9375 26.3482C20.808 26.0941 20.808 25.7614 20.808 25.096V23.3957C20.808 22.7303 20.808 22.3977 20.9375 22.1435C21.0514 21.92 21.2331 21.7383 21.4567 21.6244C21.7108 21.4949 22.0435 21.4949 22.7088 21.4949H22.999V19.0692H13.001V21.4949H13.2912C13.9565 21.4949 14.2892 21.4949 14.5433 21.6244C14.7669 21.7383 14.9486 21.92 15.0625 22.1435C15.192 22.3977 15.192 22.7303 15.192 23.3957V25.1027C15.192 25.768 15.192 26.1007 15.0625 26.3548C14.9486 26.5783 14.7669 26.7601 14.5433 26.874C14.2892 27.0035 13.9565 27.0035 13.2912 27.0035H10.9008C10.2355 27.0035 9.90279 27.0035 9.64866 26.874C9.42512 26.7601 9.24338 26.5783 9.12948 26.3548C9 26.1007 9 25.768 9 25.1027V23.3957C9 22.7303 9 22.3977 9.12948 22.1435C9.24338 21.92 9.42512 21.7383 9.64866 21.6244C9.90279 21.4949 10.2355 21.4949 10.9008 21.4949H11.129V18.8532C11.129 17.9386 11.8704 17.1972 12.785 17.1972H16.9657V14.4988H16.8048C16.1395 14.4988 15.8068 14.4988 15.5527 14.3693C15.3291 14.2554 15.1474 14.0737 15.0335 13.8501C14.904 13.596 14.904 13.2633 14.904 12.598V10.8976Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_13996_748" x1="36" y1="1.07288e-06" x2="-1.07288e-06" y2="36" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFDCF3"/>
|
||||
<stop offset="1" stop-color="#00C2D8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1700983497588" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6628" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M698.483573 594.905936A287.506808 287.506808 0 1 1 984.611923 306.020671v1.181535a287.309885 287.309885 0 0 1-286.12835 287.70373z" fill="#FFFFFF" p-id="6629"></path><path d="M698.483573 39.387645A267.814561 267.814561 0 1 1 433.229005 308.777585v-1.575379A267.420716 267.420716 0 0 1 698.483573 39.387645m0-39.384494A307.199055 307.199055 0 1 0 1004.30417 308.580663v-1.378457A306.411365 306.411365 0 0 0 698.680495 0.003151z" fill="#007FB7" p-id="6630"></path><path d="M787.689452 236.310116m-78.768988 0a78.768988 78.768988 0 1 0 157.537977 0 78.768988 78.768988 0 1 0-157.537977 0Z" fill="#D1EBF2" p-id="6631"></path><path d="M787.689452 177.233375a59.076741 59.076741 0 1 1-59.076741 59.076741 59.076741 59.076741 0 0 1 59.076741-59.076741m0-39.384495a98.461236 98.461236 0 1 0 98.461236 98.461236 98.461236 98.461236 0 0 0-98.461236-98.461236z" fill="#007FB7" p-id="6632"></path><path d="M39.384062 974.57246v-113.033499l390.300338-392.466484 162.067194 108.701204-116.381181 58.682896v124.455002l-135.876505 5.316906v131.150366l-127.014993 4.923062-65.772106 99.248925L39.384062 974.57246z" fill="#D1EBF2" p-id="6633"></path><path d="M433.229005 494.475475l120.713474 80.935136-75.421306 38.006037-21.661472 10.830736v118.153482l-98.461235 3.741527-38.793727 2.166148v131.347288l-98.461236 3.741527h-19.692247l-11.224581 16.73841L137.845298 979.101677l-78.768988-19.692247v-89.796647l374.152695-374.152695m-5.119985-50.805998L19.691815 853.268218v136.467272L155.56832 1024l67.938253-102.399685 135.876505-5.316907v-131.150365l135.876505-5.316907v-131.347288L630.151476 580.333673l-203.027068-136.664195z" fill="#007FB7" p-id="6634"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -44,6 +44,21 @@ function embedChatbot() {
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'data-bot-src') {
|
||||
const newBotSrc = script.getAttribute('data-bot-src');
|
||||
if (newBotSrc) {
|
||||
iframe.src = newBotSrc;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(script, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-bot-src']
|
||||
});
|
||||
|
||||
let chatBtnDragged = false;
|
||||
let chatBtnDown = false;
|
||||
let chatBtnMouseX;
|
||||
@@ -96,3 +111,4 @@ function embedChatbot() {
|
||||
document.body.appendChild(ChatBtn);
|
||||
}
|
||||
window.addEventListener('load', embedChatbot);
|
||||
|
||||
|
||||
@@ -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