Feat: App folder and permission (#1726)

* app folder

* feat: app foldere

* fix: run app param error

* perf: select app ux

* perf: folder rerender

* fix: ts

* fix: parentId

* fix: permission

* perf: loading ux

* perf: per select ux

* perf: clb context

* perf: query extension tip

* fix: ts

* perf: app detail per

* perf: default per
This commit is contained in:
Archer
2024-06-11 10:16:24 +08:00
committed by GitHub
parent b20d075d35
commit bc6864c3dc
89 changed files with 2495 additions and 695 deletions

View File

@@ -39,6 +39,7 @@ export default function InputGuideBox({
);
},
{
manual: false,
refreshDeps: [text],
throttleWait: 300
}

View File

@@ -155,12 +155,13 @@ ${JSON.stringify(questionGuides)}`;
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto',
color: 'primary.600'
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'14px'} mr={2} />
<Box mr={1}>{tool.toolName}</Box>
<Avatar src={tool.toolAvatar} borderRadius={'md'} w={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />
)}
@@ -219,7 +220,14 @@ ${toolResponse}`}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex alignItems={'center'} px={3} py={'1.5px'} borderRadius="md" bg={chatStatusMap.bg}>
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}

View File

@@ -0,0 +1,116 @@
import React, { useCallback } 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 '@/components/Avatar';
import { useToast } from '@fastgpt/web/hooks/useToast';
export type EditResourceInfoFormType = {
id: string;
name: string;
avatar?: string;
intro?: string;
};
const EditResourceModal = ({
onClose,
onEdit,
title,
...defaultForm
}: EditResourceInfoFormType & {
title: string;
onClose: () => void;
onEdit: (data: EditResourceInfoFormType) => any;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
defaultValues: defaultForm
});
const avatar = watch('avatar');
const { runAsync: onSave, loading } = useRequest2(
(data: EditResourceInfoFormType) => onEdit(data),
{
onSuccess: (res) => {
onClose();
}
}
);
const { File, onOpen: onOpenSelectFile } = 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.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
return (
<MyModal isOpen onClose={onClose} iconSrc={avatar} title={title}>
<ModalBody>
<Box>
<FormLabel mb={1}>{t('core.app.Name and avatar')}</FormLabel>
<HStack spacing={4}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
{...register('name', { required: true })}
bg={'myGray.50'}
autoFocus
maxLength={20}
/>
</HStack>
</Box>
<Box mt={4}>
<FormLabel mb={1}>{t('common.Intro')}</FormLabel>
<Textarea {...register('intro')} bg={'myGray.50'} maxLength={200} />
</Box>
</ModalBody>
<ModalFooter>
<Button isLoading={loading} onClick={handleSubmit(onSave)}>
{t('common.Confirm')}
</Button>
</ModalFooter>
<File onSelect={onSelectFile} />
</MyModal>
);
};
export default EditResourceModal;

View File

@@ -1,4 +1,3 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
@@ -37,16 +36,19 @@ const ParentPaths = (props: {
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
fontSize={['sm', fontSize || 'md']}
py={1}
px={[1, 2]}
fontSize={['sm', fontSize || 'sm']}
py={0.5}
px={1.5}
borderRadius={'md'}
{...(i === concatPaths.length - 1
? {
cursor: 'default'
cursor: 'default',
color: 'myGray.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
color: 'myGray.600',
_hover: {
bg: 'myGray.100'
},
@@ -58,7 +60,9 @@ const ParentPaths = (props: {
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<MyIcon name={'common/rightArrowLight'} color={'myGray.500'} w={'14px'} />
<Box mx={1.5} color={'myGray.500'}>
/
</Box>
)}
</Flex>
))}

View File

@@ -0,0 +1,184 @@
import React, { useCallback, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react';
import {
GetResourceFolderListProps,
GetResourceFolderListItemResponse,
ParentIdType
} from '@fastgpt/global/common/parentFolder/type';
import { useMemoizedFn, useMount } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
type FolderItemType = {
id: string;
name: string;
open: boolean;
children?: FolderItemType[];
};
const rootId = 'root';
type Props = {
moveResourceId: string;
title: string;
server: (e: GetResourceFolderListProps) => Promise<GetResourceFolderListItemResponse[]>;
onConfirm: (id: ParentIdType) => Promise<any>;
onClose: () => void;
};
const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => {
const { t } = useTranslation();
const [selectedId, setSelectedId] = React.useState<string>();
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
const [folderList, setFolderList] = useState<FolderItemType[]>([]);
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
setRequestingIdList((state) => [...state, e.parentId]);
return server(e).finally(() =>
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
);
}, {});
useMount(async () => {
const data = await requestServer({ parentId: null });
setFolderList([
{
id: rootId,
name: t('common.folder.Root Path'),
open: true,
children: data.map((item) => ({
id: item.id,
name: item.name,
open: false
}))
}
]);
});
const RenderList = useMemoizedFn(
({ list, index = 0 }: { list: FolderItemType[]; index?: number }) => {
return (
<>
{list
// can not move to itself
.filter((item) => moveResourceId !== item.id)
.map((item) => (
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={1}
pl={index === 0 ? '0.5rem' : `${1.75 * (index - 1) + 0.5}rem`}
pr={2}
borderRadius={'md'}
_hover={{
bg: 'myGray.100'
}}
{...(item.id === selectedId
? {
bg: 'primary.50 !important',
onClick: () => setSelectedId(undefined)
}
: {
onClick: () => setSelectedId(item.id)
})}
>
{index !== 0 && (
<Flex
alignItems={'center'}
justifyContent={'center'}
visibility={!item.children || item.children.length > 0 ? 'visible' : 'hidden'}
w={'1.25rem'}
h={'1.25rem'}
cursor={'pointer'}
borderRadius={'xs'}
_hover={{
bg: 'rgba(31, 35, 41, 0.08)'
}}
onClick={async (e) => {
e.stopPropagation();
if (requestingIdList.includes(item.id)) return;
if (!item.children) {
const data = await requestServer({ parentId: item.id });
item.children = data.map((item) => ({
id: item.id,
name: item.name,
open: false
}));
}
item.open = !item.open;
setFolderList([...folderList]);
}}
>
<MyIcon
name={
requestingIdList.includes(item.id)
? 'common/loading'
: 'common/rightArrowFill'
}
w={'1.25rem'}
color={'myGray.500'}
transform={item.open ? 'rotate(90deg)' : 'none'}
/>
</Flex>
)}
<MyIcon ml={index !== 0 ? '0.5rem' : 0} name={FolderIcon} w={'1.25rem'} />
<Box fontSize={'sm'} ml={2}>
{item.name}
</Box>
</Flex>
{item.children && item.open && (
<Box mt={0.5}>
<RenderList list={item.children} index={index + 1} />
</Box>
)}
</Box>
))}
</>
);
}
);
const { runAsync: onConfirmSelect, loading: confirming } = useRequest2(
() => {
if (selectedId) {
return onConfirm(selectedId === rootId ? null : selectedId);
}
return Promise.reject('');
},
{
onSuccess: () => {
onClose();
},
successToast: t('common.folder.Move Success')
}
);
return (
<MyModal
isLoading={folderList.length === 0}
iconSrc="/imgs/modal/move.svg"
isOpen
w={'30rem'}
title={title}
onClose={onClose}
>
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'}>
<RenderList list={folderList} />
</ModalBody>
<ModalFooter>
<Button isLoading={confirming} isDisabled={!selectedId} onClick={onConfirmSelect}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default MoveModal;

View File

@@ -0,0 +1,68 @@
import { Box, Flex } from '@chakra-ui/react';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import React, { useMemo } from 'react';
import { useTranslation } from 'next-i18next';
const FolderPath = (props: {
paths: ParentTreePathItemType[];
rootName?: string;
FirstPathDom?: React.ReactNode;
onClick: (parentId: string) => void;
fontSize?: string;
}) => {
const { t } = useTranslation();
const { paths, rootName = t('common.folder.Root Path'), FirstPathDom, onClick, fontSize } = props;
const concatPaths = useMemo(
() => [
{
parentId: '',
parentName: rootName
},
...paths
],
[rootName, paths]
);
return paths.length === 0 && !!FirstPathDom ? (
<>{FirstPathDom}</>
) : (
<Flex flex={1} ml={-1.5}>
{concatPaths.map((item, i) => (
<Flex key={item.parentId || i} alignItems={'center'}>
<Box
fontSize={['sm', fontSize || 'sm']}
py={0.5}
px={1.5}
borderRadius={'md'}
{...(i === concatPaths.length - 1
? {
cursor: 'default',
color: 'myGray.700',
fontWeight: 'bold'
}
: {
cursor: 'pointer',
color: 'myGray.600',
_hover: {
bg: 'myGray.100'
},
onClick: () => {
onClick(item.parentId);
}
})}
>
{item.parentName}
</Box>
{i !== concatPaths.length - 1 && (
<Box mx={1.5} color={'myGray.500'}>
/
</Box>
)}
</Flex>
))}
</Flex>
);
};
export default React.memo(FolderPath);

View File

@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { Box, Flex } from '@chakra-ui/react';
import {
GetResourceFolderListProps,
GetResourceListItemResponse,
ParentIdType
} from '@fastgpt/global/common/parentFolder/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import Avatar from '@/components/Avatar';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useMemoizedFn } from 'ahooks';
type ResourceItemType = GetResourceListItemResponse & {
open: boolean;
children?: ResourceItemType[];
};
const SelectOneResource = ({
server,
value,
onSelect
}: {
server: (e: GetResourceFolderListProps) => Promise<GetResourceListItemResponse[]>;
value?: ParentIdType;
onSelect: (e?: string) => any;
}) => {
const [dataList, setDataList] = useState<ResourceItemType[]>([]);
const [requestingIdList, setRequestingIdList] = useState<ParentIdType[]>([]);
const { runAsync: requestServer } = useRequest2((e: GetResourceFolderListProps) => {
if (requestingIdList.includes(e.parentId)) return Promise.reject(null);
setRequestingIdList((state) => [...state, e.parentId]);
return server(e).finally(() =>
setRequestingIdList((state) => state.filter((id) => id !== e.parentId))
);
}, {});
const { loading } = useRequest2(() => requestServer({ parentId: null }), {
manual: false,
onSuccess: (data) => {
setDataList(
data.map((item) => ({
...item,
open: false
}))
);
}
});
const Render = useMemoizedFn(
({ list, index = 0 }: { list: ResourceItemType[]; index?: number }) => {
return (
<>
{list.map((item) => (
<Box key={item.id} _notLast={{ mb: 0.5 }} userSelect={'none'}>
<Flex
alignItems={'center'}
cursor={'pointer'}
py={1}
pl={`${1.25 * index + 0.5}rem`}
pr={2}
borderRadius={'md'}
_hover={{
bg: 'myGray.100'
}}
{...(item.id === value
? {
bg: 'primary.50 !important',
onClick: () => onSelect(undefined)
}
: {
onClick: async () => {
// folder => open(request children) or close
if (item.isFolder) {
if (!item.children) {
const data = await requestServer({ parentId: item.id });
item.children = data.map((item) => ({
...item,
open: false
}));
}
item.open = !item.open;
setDataList([...dataList]);
} else {
onSelect(item.id);
}
}
})}
>
<Flex
alignItems={'center'}
justifyContent={'center'}
visibility={
item.isFolder && (!item.children || item.children.length > 0)
? 'visible'
: 'hidden'
}
w={'1.25rem'}
h={'1.25rem'}
cursor={'pointer'}
borderRadius={'xs'}
_hover={{
bg: 'rgba(31, 35, 41, 0.08)'
}}
>
<MyIcon
name={
requestingIdList.includes(item.id)
? 'common/loading'
: 'common/rightArrowFill'
}
w={'14px'}
color={'myGray.500'}
transform={item.open ? 'rotate(90deg)' : 'none'}
/>
</Flex>
<Avatar ml={index !== 0 ? '0.5rem' : 0} src={item.avatar} w={'1.25rem'} />
<Box fontSize={'sm'} ml={2}>
{item.name}
</Box>
</Flex>
{item.children && item.open && (
<Box mt={0.5}>
<Render list={item.children} index={index + 1} />
</Box>
)}
</Box>
))}
</>
);
}
);
return loading ? <Loading fixed={false} /> : <Render list={dataList} />;
};
export default SelectOneResource;

View File

@@ -0,0 +1,178 @@
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FolderIcon } from '@fastgpt/global/common/file/image/constants';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
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';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const FolderSlideCard = ({
name,
intro,
onEdit,
onMove,
deleteTip,
onDelete,
defaultPer,
managePer
}: {
name: string;
intro?: string;
onEdit: () => void;
onMove: () => void;
deleteTip: string;
onDelete: () => void;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
}) => {
const { t } = useTranslation();
const { ConfirmModal, openConfirm } = useConfirm({
type: 'delete',
content: deleteTip
});
return (
<Box w={'13rem'}>
<Box>
<HStack>
<MyIcon name={FolderIcon} w={'1.5rem'} />
<Box color={'myGray.900'}>{name}</Box>
<MyIcon
name={'edit'}
_hover={{ color: 'primary.600' }}
w={'0.875rem'}
cursor={'pointer'}
onClick={onEdit}
/>
</HStack>
<Box mt={3} fontSize={'sm'} color={'myGray.500'} cursor={'pointer'} onClick={onEdit}>
{intro || '暂无介绍'}
</Box>
</Box>
{managePer.permission.hasManagePer && (
<>
<MyDivider my={6} />
<Box>
<FormLabel>{t('common.Operation')}</FormLabel>
<Button
variant={'transparentBase'}
pl={1}
leftIcon={<MyIcon name={'common/file/move'} w={'1rem'} />}
transform={'none !important'}
w={'100%'}
justifyContent={'flex-start'}
size={'sm'}
fontSize={'mini'}
mt={4}
onClick={onMove}
>
{t('common.Move')}
</Button>
<Button
variant={'transparentDanger'}
pl={1}
leftIcon={<MyIcon name={'delete'} w={'1rem'} />}
transform={'none !important'}
w={'100%'}
justifyContent={'flex-start'}
size={'sm'}
fontSize={'mini'}
mt={3}
onClick={() => {
openConfirm(onDelete)();
}}
>
{t('common.Delete folder')}
</Button>
</Box>
</>
)}
<MyDivider my={6} />
<Box>
<FormLabel>{t('support.permission.Permission')}</FormLabel>
{managePer.permission.hasManagePer && (
<Box mt={5}>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('permission.Default permission')}
</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
/>
</Box>
)}
<Box mt={6}>
<CollaboratorContextProvider {...managePer}>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex alignItems="center" justifyContent="space-between">
<Box fontSize={'sm'} color={'myGray.500'}>
{t('permission.Collaborator')}
</Box>
{managePer.permission.hasManagePer && (
<HStack spacing={3}>
<MyTooltip label={t('permission.Manage')}>
<MyIcon
w="1rem"
name="common/settingLight"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenManageModal}
/>
</MyTooltip>
<MyTooltip label={t('common.Add')}>
<MyIcon
w="1rem"
name="support/permission/collaborator"
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={onOpenAddMember}
/>
</MyTooltip>
</HStack>
)}
</Flex>
<MemberListCard
mt={2}
tagStyle={{
type: 'borderSolid',
colorSchema: 'gray'
}}
/>
</>
);
}}
</CollaboratorContextProvider>
</Box>
</Box>
<ConfirmModal />
</Box>
);
};
export default FolderSlideCard;

View File

@@ -0,0 +1,54 @@
import React, { useState, DragEvent, useCallback } from 'react';
import type { BoxProps } from '@chakra-ui/react';
export const useFolderDrag = ({
onDrop,
activeStyles
}: {
onDrop: (dragId: string, targetId: string) => any;
activeStyles: BoxProps;
}) => {
const [dragId, setDragId] = useState<string>();
const [targetId, setTargetId] = useState<string>();
const getBoxProps = useCallback(
({ dataId, isFolder }: { dataId: string; isFolder: boolean }) => {
return {
draggable: true,
'data-drag-id': isFolder ? dataId : undefined,
onDragStart: (e: DragEvent<HTMLDivElement>) => {
setDragId(dataId);
},
onDragOver: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
const targetId = e.currentTarget.getAttribute('data-drag-id');
if (!targetId) return;
setTargetId(targetId);
},
onDragLeave: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setTargetId(undefined);
},
onDrop: (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
if (targetId && dragId && targetId !== dragId) {
onDrop(dragId, targetId);
}
setTargetId(undefined);
setDragId(undefined);
},
...(activeStyles &&
targetId === dataId && {
...activeStyles
})
};
},
[activeStyles, dragId, onDrop, targetId]
);
return {
getBoxProps
};
};

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Button,
@@ -68,6 +68,14 @@ const DatasetParamsModal = ({
const [refresh, setRefresh] = useState(false);
const [currentTabType, setCurrentTabType] = useState(SearchSettingTabEnum.searchMode);
const chatModelSelectList = (() =>
llmModelList
.filter((model) => model.usedInQueryExtension)
.map((item) => ({
value: item.model,
label: item.name
})))();
const { register, setValue, getValues, handleSubmit, watch } = useForm<DatasetParamsProps>({
defaultValues: {
limit,
@@ -75,7 +83,7 @@ const DatasetParamsModal = ({
searchMode,
usingReRank: !!usingReRank && teamPlanStatus?.standardConstants?.permissionReRank !== false,
datasetSearchUsingExtensionQuery,
datasetSearchExtensionModel: datasetSearchExtensionModel ?? llmModelList[0]?.model,
datasetSearchExtensionModel: datasetSearchExtensionModel || chatModelSelectList[0]?.value,
datasetSearchExtensionBg
}
});
@@ -85,14 +93,6 @@ const DatasetParamsModal = ({
const usingReRankWatch = watch('usingReRank');
const searchModeWatch = watch('searchMode');
const chatModelSelectList = (() =>
llmModelList
.filter((model) => model.usedInQueryExtension)
.map((item) => ({
value: item.model,
label: item.name
})))();
const searchModeList = useMemo(() => {
const list = Object.values(DatasetSearchModeMap);
return list;
@@ -109,6 +109,15 @@ const DatasetParamsModal = ({
return usingReRank !== undefined && reRankModelList.length > 0;
}, [reRankModelList.length, usingReRank]);
useEffect(() => {
if (datasetSearchUsingCfrForm) {
!queryExtensionModel &&
setValue('datasetSearchExtensionModel', chatModelSelectList[0]?.value);
} else {
setValue('datasetSearchExtensionModel', '');
}
}, [chatModelSelectList, datasetSearchUsingCfrForm, queryExtensionModel, setValue]);
return (
<MyModal
isOpen={true}
@@ -270,7 +279,7 @@ const DatasetParamsModal = ({
{t('core.dataset.Query extension intro')}
</Box>
<Flex mt={3} alignItems={'center'}>
<Box flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</Box>
<FormLabel flex={'1 0 0'}>{t('core.dataset.search.Using query extension')}</FormLabel>
<Switch {...register('datasetSearchUsingExtensionQuery')} />
</Flex>
{datasetSearchUsingCfrForm === true && (

View File

@@ -237,7 +237,6 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
});
},
{
manual: true,
onSuccess() {
setNewData(undefined);
},

View File

@@ -14,22 +14,31 @@ const SearchParamsTip = ({
limit = 1500,
responseEmptyText,
usingReRank = false,
usingQueryExtension = false
queryExtensionModel
}: {
searchMode: `${DatasetSearchModeEnum}`;
similarity?: number;
limit?: number;
responseEmptyText?: string;
usingReRank?: boolean;
usingQueryExtension?: boolean;
queryExtensionModel?: string;
}) => {
const { t } = useTranslation();
const { reRankModelList } = useSystemStore();
const { reRankModelList, llmModelList } = useSystemStore();
const hasReRankModel = reRankModelList.length > 0;
const hasEmptyResponseMode = responseEmptyText !== undefined;
const hasSimilarityMode = usingReRank || searchMode === DatasetSearchModeEnum.embedding;
const extensionModelName = useMemo(
() =>
queryExtensionModel
? llmModelList.find((item) => item.model === queryExtensionModel)?.name ??
llmModelList[0]?.name
: undefined,
[llmModelList, queryExtensionModel]
);
return (
<TableContainer
bg={'primary.50'}
@@ -73,8 +82,8 @@ const SearchParamsTip = ({
{usingReRank ? '✅' : '❌'}
</Td>
)}
<Td pt={0} pb={2}>
{usingQueryExtension ? '✅' : '❌'}
<Td pt={0} pb={2} fontSize={'mini'}>
{extensionModelName ? extensionModelName : '❌'}
</Td>
{hasEmptyResponseMode && <Th>{responseEmptyText !== '' ? '✅' : '❌'}</Th>}
</Tr>

View File

@@ -1,108 +1,78 @@
import React, { useMemo } from 'react';
import { ModalBody, Flex, Box, useTheme, ModalFooter, Button } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useQuery } from '@tanstack/react-query';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useAppStore } from '@/web/core/app/store/useAppStore';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import {
GetResourceFolderListProps,
GetResourceListItemResponse
} from '@fastgpt/global/common/parentFolder/type';
import { getMyApps } from '@/web/core/app/api';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
const SelectAppModal = ({
defaultApps = [],
value,
filterAppIds = [],
max = 1,
onClose,
onSuccess
}: {
defaultApps: string[];
value?: SelectAppItemType;
filterAppIds?: string[];
max?: number;
onClose: () => void;
onSuccess: (e: SelectAppItemType[]) => void;
onSuccess: (e: SelectAppItemType) => void;
}) => {
const { t } = useTranslation();
const { Loading } = useLoading();
const theme = useTheme();
const [selectedApps, setSelectedApps] = React.useState<string[]>(defaultApps);
/* 加载模型 */
const { myApps, loadMyApps } = useAppStore();
const { isLoading } = useQuery(['loadMyApos'], () => loadMyApps());
const [selectedApp, setSelectedApp] = useState<SelectAppItemType | undefined>(value);
const apps = useMemo(
() => myApps.filter((app) => !filterAppIds.includes(app._id)),
[myApps, filterAppIds]
const getAppList = useCallback(
async ({ parentId }: GetResourceFolderListProps) => {
return getMyApps({ parentId }).then((res) =>
res
.filter((item) => !filterAppIds.includes(item._id))
.map<GetResourceListItemResponse>((item) => ({
id: item._id,
name: item.name,
avatar: item.avatar,
isFolder: item.type === AppTypeEnum.folder
}))
);
},
[filterAppIds]
);
return (
<MyModal
isOpen
title={`选择应用${max > 1 ? `(${selectedApps.length}/${max})` : ''}`}
title={`选择应用`}
iconSrc="/imgs/workflow/ai.svg"
onClose={onClose}
position={'relative'}
w={'600px'}
>
<ModalBody
display={'grid'}
gridTemplateColumns={['1fr', 'repeat(3, minmax(0, 1fr))']}
gridGap={4}
>
{apps.map((app) => (
<Flex
key={app._id}
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
p={2}
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'primary.100',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}
}
: {
onClick: () => {
if (max === 1) {
setSelectedApps([app._id]);
} else if (selectedApps.length < max) {
setSelectedApps([...selectedApps, app._id]);
}
}
})}
>
<Avatar src={app.avatar} w={['16px', '22px']} />
<Box fontSize={'sm'} color={'myGray.900'} ml={1}>
{app.name}
</Box>
</Flex>
))}
<ModalBody flex={'1 0 0'} overflow={'auto'} minH={'400px'} position={'relative'}>
<SelectOneResource
value={selectedApp?.id}
onSelect={(id) => setSelectedApp(id ? { id } : undefined)}
server={getAppList}
/>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common.Close')}
{t('common.Cancel')}
</Button>
<Button
ml={2}
isDisabled={!selectedApp}
onClick={() => {
onSuccess(
apps
.filter((app) => selectedApps.includes(app._id))
.map((app) => ({
id: app._id,
name: app.name,
logo: app.avatar
}))
);
if (!selectedApp) return;
onSuccess(selectedApp);
onClose();
}}
>
{t('common.Confirm')}
</Button>
</ModalFooter>
<Loading loading={isLoading} fixed={false} />
</MyModal>
);
};

View File

@@ -1,16 +1,17 @@
import React, { useMemo } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, Flex, useDisclosure, useTheme } from '@chakra-ui/react';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { SelectAppItemType } from '@fastgpt/global/core/workflow/type/index.d';
import Avatar from '@/components/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/components/core/workflow/context';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById } from '@/web/core/app/api';
const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const theme = useTheme();
const filterAppIds = useContextSelector(WorkflowContext, (ctx) => ctx.filterAppIds);
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -21,8 +22,28 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
} = useDisclosure();
const value = item.value as SelectAppItemType | undefined;
const filterAppString = useMemo(() => filterAppIds?.join(',') || '', [filterAppIds]);
const { data: appDetail, loading } = useRequest2(
() => {
if (value?.id) return getAppDetailById(value.id);
return Promise.resolve(null);
},
{
manual: false,
refreshDeps: [value?.id],
errorToast: 'Error',
onError() {
onChangeNode({
nodeId,
type: 'updateInput',
key: 'app',
value: {
...item,
value: undefined
}
});
}
}
);
const Render = useMemo(() => {
return (
@@ -33,26 +54,22 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
{t('core.module.Select app')}
</Button>
) : (
<Flex
alignItems={'center'}
border={theme.borders.base}
borderRadius={'md'}
bg={'white'}
px={3}
py={2}
<Button
isLoading={loading}
w={'100%'}
justifyContent={loading ? 'center' : 'flex-start'}
variant={'whiteFlow'}
leftIcon={<Avatar src={appDetail?.avatar} w={6} />}
>
<Avatar src={value?.logo} w={6} />
<Box fontWeight={'medium'} ml={2}>
{value?.name}
</Box>
</Flex>
{appDetail?.name}
</Button>
)}
</Box>
{isOpenSelectApp && (
<SelectAppModal
defaultApps={item.value?.id ? [item.value.id] : []}
filterAppIds={filterAppString.split(',')}
value={item.value}
filterAppIds={filterAppIds}
onClose={onCloseSelectApp}
onSuccess={(e) => {
onChangeNode({
@@ -61,7 +78,7 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
key: 'app',
value: {
...item,
value: e[0]
value: e
}
});
}}
@@ -70,15 +87,17 @@ const SelectAppRender = ({ item, nodeId }: RenderInputProps) => {
</>
);
}, [
filterAppString,
appDetail?.avatar,
appDetail?.name,
filterAppIds,
isOpenSelectApp,
item,
loading,
nodeId,
onChangeNode,
onCloseSelectApp,
onOpenSelectApp,
t,
theme.borders.base,
value
]);

View File

@@ -82,7 +82,7 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
similarity={data.similarity}
limit={data.limit}
usingReRank={data.usingReRank}
usingQueryExtension={data.datasetSearchUsingExtensionQuery}
queryExtensionModel={data.datasetSearchExtensionModel}
/>
</>
);

View File

@@ -0,0 +1,96 @@
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { MemberManagerInputPropsType, CollaboratorContextProvider } from '../MemberManager/context';
import { Box, Button, Flex, HStack, ModalBody } from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import DefaultPermissionList from '../DefaultPerList';
import MyIcon from '@fastgpt/web/components/common/Icon';
export type ConfigPerModalProps = {
avatar?: string;
name: string;
defaultPer: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
};
const ConfigPerModal = ({
avatar,
name,
defaultPer,
managePer,
onClose
}: ConfigPerModalProps & {
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
onClose={onClose}
title={t('permission.Permission config')}
>
<ModalBody>
<HStack>
<Avatar src={avatar} w={'1.75rem'} />
<Box fontSize={'lg'}>{name}</Box>
</HStack>
<Box mt={6}>
<Box fontSize={'sm'}>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="1"
per={defaultPer.value}
defaultPer={defaultPer.defaultValue}
onChange={defaultPer.onChange}
/>
</Box>
<Box mt={4}>
<CollaboratorContextProvider {...managePer}>
{({ MemberListCard, onOpenManageModal, onOpenAddMember }) => {
return (
<>
<Flex
alignItems="center"
flexDirection="row"
justifyContent="space-between"
w="full"
>
<Box fontSize={'sm'}>{t('permission.Collaborator')}</Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
</Flex>
</Flex>
<MemberListCard mt={2} p={1.5} bg="myGray.100" borderRadius="md" />
</>
);
}}
</CollaboratorContextProvider>
</Box>
</ModalBody>
</MyModal>
);
};
export default ConfigPerModal;

View File

@@ -3,6 +3,8 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
export enum defaultPermissionEnum {
private = 'private',
@@ -13,16 +15,16 @@ export enum defaultPermissionEnum {
type Props = Omit<BoxProps, 'onChange'> & {
per: PermissionValueType;
defaultPer: PermissionValueType;
readPer: PermissionValueType;
writePer: PermissionValueType;
onChange: (v: PermissionValueType) => void;
readPer?: PermissionValueType;
writePer?: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any> | any;
};
const DefaultPermissionList = ({
per,
defaultPer,
readPer,
writePer,
readPer = ReadPermissionVal,
writePer = WritePermissionVal,
onChange,
...styles
}: Props) => {
@@ -33,14 +35,17 @@ const DefaultPermissionList = ({
{ label: '团队可编辑', value: writePer }
];
const { runAsync: onRequestChange, loading } = useRequest2(async (v: PermissionValueType) =>
onChange(v)
);
return (
<Box {...styles}>
<MySelect
isLoading={loading}
list={defaultPermissionSelectList}
value={per}
onchange={(v) => {
onChange(v);
}}
onchange={onRequestChange}
/>
</Box>
);

View File

@@ -23,7 +23,6 @@ import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { ChevronDownIcon } from '@chakra-ui/icons';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
@@ -32,11 +31,10 @@ export type AddModalPropsType = {
onClose: () => void;
};
export function AddMemberModal({ onClose }: AddModalPropsType) {
const toast = useToast();
function AddMemberModal({ onClose }: AddModalPropsType) {
const { userInfo } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPreLabelList } =
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const {
@@ -50,7 +48,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
});
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.permission.isOwner) return false;
// if (item.permission.isOwner) return false;
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
@@ -60,8 +58,8 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPreLabelList(selectedPermission).join('、');
}, [getPreLabelList, selectedPermission]);
return getPerLabelList(selectedPermission).join('、');
}, [getPerLabelList, selectedPermission]);
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
mutationFn: () => {
@@ -85,6 +83,7 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="55% 45%"
fontSize={'sm'}
>
<Flex
flexDirection="column"
@@ -141,7 +140,9 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
<MyAvatar src={member.avatar} w="32px" />
<Box ml="2">{member.memberName}</Box>
</Flex>
{!!collaborator && <PermissionTags permission={collaborator.permission} />}
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</Flex>
</Flex>
);
@@ -210,3 +211,5 @@ export function AddMemberModal({ onClose }: AddModalPropsType) {
</MyModal>
);
}
export default AddMemberModal;

View File

@@ -18,10 +18,11 @@ import PermissionTags from './PermissionTags';
import Avatar from '@/components/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
export type ManageModalProps = {
onClose: () => void;
@@ -29,14 +30,12 @@ export type ManageModalProps = {
function ManageModal({ onClose }: ManageModalProps) {
const { userInfo } = useUserStore();
const { collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } =
useContextSelector(CollaboratorContext, (v) => v);
const { mutate: onDelete, isLoading: isDeleting } = useRequest({
mutationFn: (tmbId: string) => onDelOneCollaborator(tmbId)
});
const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) =>
onDelOneCollaborator(tmbId)
);
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
@@ -49,14 +48,7 @@ function ManageModal({ onClose }: ManageModalProps) {
const loading = isDeleting || isUpdating;
return (
<MyModal
isLoading={loading}
isOpen
onClose={onClose}
minW="600px"
title="管理协作者"
iconSrc="common/settingLight"
>
<MyModal isOpen onClose={onClose} minW="600px" title="管理协作者" iconSrc="common/settingLight">
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
@@ -86,26 +78,28 @@ function ManageModal({ onClose }: ManageModalProps) {
</Flex>
</Td>
<Td border="none">
<PermissionTags permission={item.permission} />
<PermissionTags permission={item.permission.value} />
</Td>
<Td border="none">
{item.tmbId !== userInfo?.team?.tmbId && (
<PermissionSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission}
onChange={(per) => {
onUpdate({
tmbId: item.tmbId,
per
});
}}
onDelete={() => {
onDelete(item.tmbId);
}}
/>
)}
{/* Not self; Not owner and other manager */}
{item.tmbId !== userInfo?.team?.tmbId &&
(permission.isOwner || !item.permission.hasManagePer) && (
<PermissionSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission.value}
onChange={(per) => {
onUpdate({
tmbId: item.tmbId,
per
});
}}
onDelete={() => {
onDelete(item.tmbId);
}}
/>
)}
</Td>
</Tr>
);
@@ -114,6 +108,7 @@ function ManageModal({ onClose }: ManageModalProps) {
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={'暂无协作者'} />}
</TableContainer>
{loading && <Loading fixed={false} />}
</ModalBody>
</MyModal>
);

View File

@@ -0,0 +1,42 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import MyBox from '@fastgpt/web/components/common/MyBox';
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 '@/components/Avatar';
import { useTranslation } from 'next-i18next';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
const { t } = useTranslation();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
return (
<MyBox isLoading={isFetchingCollaborator} userSelect={'none'} {...props}>
{collaboratorList?.length === 0 ? (
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
{t('permission.Not collaborator')}
</Box>
) : (
<Flex gap="2" flexWrap={'wrap'}>
{collaboratorList?.map((member) => {
return (
<Tag key={member.tmbId} type={'fill'} colorSchema="white" {...tagStyle}>
<Avatar src={member.avatar} w="1.25rem" />
<Box fontSize={'sm'}>{member.name}</Box>
</Tag>
);
})}
</Flex>
)}
</MyBox>
);
};
export default MemberListCard;

View File

@@ -49,7 +49,7 @@ function PermissionSelect({
...props
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
@@ -66,10 +66,16 @@ function PermissionSelect({
});
return {
singleCheckBoxList: list.filter((item) => item.checkBoxType === 'single'),
singleCheckBoxList: list
.filter((item) => item.checkBoxType === 'single')
.filter((item) => {
if (permission.isOwner) return true;
if (item.value === permissionList['manage'].value) return false;
return true;
}),
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
};
}, [permissionList]);
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
const per = new Permission({ per: value });
@@ -88,6 +94,12 @@ function PermissionSelect({
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
const onSelectPer = (per: PermissionValueType) => {
if (per === value) return;
onChange(per);
setIsOpen(false);
};
useOutsideClick({
ref: ref,
handler: () => {
@@ -151,8 +163,7 @@ function PermissionSelect({
const per = new Permission({ per: value });
per.removePer(selectedSingleValue);
per.addPer(item.value);
onChange(per.value);
setIsOpen(false);
onSelectPer(per.value);
};
return (

View File

@@ -10,9 +10,9 @@ export type PermissionTagsProp = {
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPreLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const perTagList = getPreLabelList(permission);
const perTagList = getPerLabelList(permission);
return (
<Flex gap="2" alignItems="center">

View File

@@ -1,3 +1,4 @@
import { BoxProps, useDisclosure } from '@chakra-ui/react';
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
@@ -5,8 +6,14 @@ import { PermissionListType, PermissionValueType } from '@fastgpt/global/support
import { useQuery } from '@tanstack/react-query';
import { ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (tmbIds: string[], permission: PermissionValueType) => any;
@@ -16,7 +23,12 @@ export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void;
isFetchingCollaborator: boolean;
getPreLabelList: (per: PermissionValueType) => string[];
getPerLabelList: (per: PermissionValueType) => string[];
};
export type ChildrenProps = {
onOpenAddMember: () => void;
onOpenManageModal: () => void;
MemberListCard: (props: MemberListCardProps) => JSX.Element;
};
type CollaboratorContextType = MemberManagerPropsType & {};
@@ -30,7 +42,7 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
onDelOneCollaborator: function () {
throw new Error('Function not implemented.');
},
getPreLabelList: function (): string[] {
getPerLabelList: function (): string[] {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
@@ -39,33 +51,36 @@ export const CollaboratorContext = createContext<CollaboratorContextType>({
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false
isFetchingCollaborator: false,
permission: new Permission()
});
export const CollaboratorContextProvider = ({
permission,
onGetCollaboratorList,
permissionList,
onUpdateCollaborators,
onDelOneCollaborator,
children
}: MemberManagerInputPropsType & {
children: ReactNode;
children: (props: ChildrenProps) => ReactNode;
}) => {
const {
data: collaboratorList = [],
refetch: refetchCollaboratorList,
isLoading: isFetchingCollaborator
} = useQuery(['collaboratorList'], onGetCollaboratorList);
const onUpdateCollaboratorsThen = async (tmbIds: string[], permission: PermissionValueType) => {
await onUpdateCollaborators(tmbIds, permission);
refetchCollaboratorList();
};
const onDelOneCollaboratorThem = async (tmbId: string) => {
const onDelOneCollaboratorThen = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const getPreLabelList = useCallback(
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
const Per = new Permission({ per });
const labels: string[] = [];
@@ -91,17 +106,33 @@ export const CollaboratorContextProvider = ({
[permissionList]
);
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
onClose: onCloseAddMember
} = useDisclosure();
const {
isOpen: isOpenManageModal,
onOpen: onOpenManageModal,
onClose: onCloseManageModal
} = useDisclosure();
const contextValue = {
permission,
onGetCollaboratorList,
collaboratorList,
refetchCollaboratorList,
isFetchingCollaborator,
permissionList,
onUpdateCollaborators: onUpdateCollaboratorsThen,
onDelOneCollaborator: onDelOneCollaboratorThem,
getPreLabelList
onDelOneCollaborator: onDelOneCollaboratorThen,
getPerLabelList
};
return (
<CollaboratorContext.Provider value={contextValue}>{children}</CollaboratorContext.Provider>
<CollaboratorContext.Provider value={contextValue}>
{children({ onOpenAddMember, onOpenManageModal, MemberListCard })}
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
</CollaboratorContext.Provider>
);
};

View File

@@ -1,99 +0,0 @@
import React, { useState } from 'react';
import { Flex, Box, Button, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { AddMemberModal } from './AddMemberModal';
import { useContextSelector } from 'use-context-selector';
import ManageModal from './ManageModal';
import {
CollaboratorContext,
CollaboratorContextProvider,
MemberManagerInputPropsType
} from './context';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
function MemberManger() {
const { t } = useTranslation();
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
onClose: onCloseAddMember
} = useDisclosure();
const {
isOpen: isOpenManageModal,
onOpen: onOpenManageModal,
onClose: onCloseManageModal
} = useDisclosure();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
return (
<>
<Flex alignItems="center" flexDirection="row" justifyContent="space-between" w="full">
<Box fontSize={'sm'}></Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
</Flex>
</Flex>
{/* member list */}
<MyBox
isLoading={isFetchingCollaborator}
mt={2}
bg="myGray.100"
borderRadius="md"
size={'md'}
>
{collaboratorList?.length === 0 ? (
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
</Box>
) : (
<Flex gap="2" p={1.5}>
{collaboratorList?.map((member) => {
return (
<Tag px="4" py="1.5" bgColor="white" key={member.tmbId} width="fit-content">
<Flex alignItems="center">
<Avatar src={member.avatar} w="24px" />
<TagLabel mx="2">{member.name}</TagLabel>
</Flex>
</Tag>
);
})}
</Flex>
)}
</MyBox>
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
</>
);
}
function Render(props: MemberManagerInputPropsType) {
return (
<CollaboratorContextProvider {...props}>
<MemberManger />
</CollaboratorContextProvider>
);
}
export default React.memo(Render);

View File

@@ -6,6 +6,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Avatar from '@/components/Avatar';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
@@ -46,7 +47,7 @@ const TeamMenu = () => {
<Flex w={'100%'} alignItems={'center'}>
{userInfo?.team ? (
<>
<Image src={userInfo.team.avatar} alt={''} w={'16px'} />
<Avatar src={userInfo.team.avatar} w={'1rem'} />
<Box ml={2}>{userInfo.team.teamName}</Box>
</>
) : (