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

@@ -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
};
};