### What problem does this PR solve? feat: Move files in file manager #1826 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| @@ -0,0 +1,6 @@ | |||
| <svg t="1722928702193" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6094" | |||
| width="200" height="200"> | |||
| <path | |||
| d="M572.330667 597.333333H298.666667v-85.333333h273.664l-77.994667-77.994667L554.666667 373.632 735.701333 554.666667l-60.373333 60.330666L554.666667 735.701333l-60.330667-60.373333L572.330667 597.333333zM533.333333 263.509333H853.333333a85.333333 85.333333 0 0 1 85.333334 85.333334V810.666667a85.333333 85.333333 0 0 1-85.333334 85.333333H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333333V213.333333a85.333333 85.333333 0 0 1 85.333334-85.333333h241.493333a85.333333 85.333333 0 0 1 76.117333 46.72L533.333333 263.509333z m0 85.333334a85.333333 85.333333 0 0 1-76.117333-46.72L412.202667 213.333333H170.666667v597.333334h682.666666V348.842667h-320z" | |||
| fill="#666666" p-id="6095"></path> | |||
| </svg> | |||
| @@ -28,6 +28,23 @@ export interface IListResult { | |||
| loading: boolean; | |||
| } | |||
| export const useFetchPureFileList = () => { | |||
| const { mutateAsync, isPending: loading } = useMutation({ | |||
| mutationKey: ['fetchPureFileList'], | |||
| gcTime: 0, | |||
| mutationFn: async (parentId: string) => { | |||
| const { data } = await fileManagerService.listFile({ | |||
| parent_id: parentId, | |||
| }); | |||
| return data; | |||
| }, | |||
| }); | |||
| return { loading, fetchList: mutateAsync }; | |||
| }; | |||
| export const useFetchFileList = (): ResponseType<any> & IListResult => { | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| @@ -225,3 +242,31 @@ export const useConnectToKnowledge = () => { | |||
| return { data, loading, connectFileToKnowledge: mutateAsync }; | |||
| }; | |||
| export interface IMoveFileBody { | |||
| src_file_ids: string[]; | |||
| dest_file_id: string; // target folder id | |||
| } | |||
| export const useMoveFile = () => { | |||
| const queryClient = useQueryClient(); | |||
| const { t } = useTranslation(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: ['moveFile'], | |||
| mutationFn: async (params: IMoveFileBody) => { | |||
| const { data } = await fileManagerService.moveFile(params); | |||
| if (data.retcode === 0) { | |||
| message.success(t('message.operated')); | |||
| queryClient.invalidateQueries({ queryKey: ['fetchFileList'] }); | |||
| } | |||
| return data.retcode; | |||
| }, | |||
| }); | |||
| return { data, loading, moveFile: mutateAsync }; | |||
| }; | |||
| @@ -26,6 +26,7 @@ export default { | |||
| download: 'Download', | |||
| close: 'Close', | |||
| preview: 'Preview', | |||
| move: 'Move', | |||
| }, | |||
| login: { | |||
| login: 'Sign in', | |||
| @@ -564,6 +565,7 @@ The above is the content you need to summarize.`, | |||
| fileError: 'File error', | |||
| uploadLimit: | |||
| 'The file size cannot exceed 10M, and the total number of files cannot exceed 128', | |||
| destinationFolder: 'Destination folder', | |||
| }, | |||
| flow: { | |||
| cite: 'Cite', | |||
| @@ -26,6 +26,7 @@ export default { | |||
| download: '下載', | |||
| close: '關閉', | |||
| preview: '預覽', | |||
| move: '移動', | |||
| }, | |||
| login: { | |||
| login: '登入', | |||
| @@ -524,6 +525,7 @@ export default { | |||
| preview: '預覽', | |||
| fileError: '文件錯誤', | |||
| uploadLimit: '文件大小不能超過10M,文件總數不超過128個', | |||
| destinationFolder: '目標資料夾', | |||
| }, | |||
| flow: { | |||
| cite: '引用', | |||
| @@ -26,6 +26,7 @@ export default { | |||
| download: '下载', | |||
| close: '关闭', | |||
| preview: '预览', | |||
| move: '移动', | |||
| }, | |||
| login: { | |||
| login: '登录', | |||
| @@ -542,6 +543,7 @@ export default { | |||
| preview: '预览', | |||
| fileError: '文件错误', | |||
| uploadLimit: '文件大小不能超过10M,文件总数不超过128个', | |||
| destinationFolder: '目标文件夹', | |||
| }, | |||
| flow: { | |||
| flow: '工作流', | |||
| @@ -1,6 +1,12 @@ | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { | |||
| getExtension, | |||
| isSupportedPreviewDocumentType, | |||
| } from '@/utils/document-util'; | |||
| import { downloadFile } from '@/utils/file-util'; | |||
| import { | |||
| DeleteOutlined, | |||
| @@ -11,18 +17,13 @@ import { | |||
| } from '@ant-design/icons'; | |||
| import { Button, Space, Tooltip } from 'antd'; | |||
| import { useHandleDeleteFile } from '../hooks'; | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import { | |||
| getExtension, | |||
| isSupportedPreviewDocumentType, | |||
| } from '@/utils/document-util'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| record: IFile; | |||
| setCurrentRecord: (record: any) => void; | |||
| showRenameModal: (record: IFile) => void; | |||
| showMoveFileModal: (ids: string[]) => void; | |||
| showConnectToKnowledgeModal: (record: IFile) => void; | |||
| setSelectedRowKeys(keys: string[]): void; | |||
| } | |||
| @@ -33,6 +34,7 @@ const ActionCell = ({ | |||
| showRenameModal, | |||
| showConnectToKnowledgeModal, | |||
| setSelectedRowKeys, | |||
| showMoveFileModal, | |||
| }: IProps) => { | |||
| const documentId = record.id; | |||
| const beingUsed = false; | |||
| @@ -64,6 +66,10 @@ const ActionCell = ({ | |||
| showConnectToKnowledgeModal(record); | |||
| }; | |||
| const onShowMoveFileModal = () => { | |||
| showMoveFileModal([documentId]); | |||
| }; | |||
| return ( | |||
| <Space size={0}> | |||
| {isKnowledgeBase || ( | |||
| @@ -90,6 +96,18 @@ const ActionCell = ({ | |||
| </Button> | |||
| </Tooltip> | |||
| )} | |||
| {isKnowledgeBase || ( | |||
| <Tooltip title={t('move', { keyPrefix: 'common' })}> | |||
| <Button | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={onShowMoveFileModal} | |||
| className={styles.iconButton} | |||
| > | |||
| <SvgIcon name={`move`} width={16}></SvgIcon> | |||
| </Button> | |||
| </Tooltip> | |||
| )} | |||
| {isKnowledgeBase || ( | |||
| <Tooltip title={t('delete', { keyPrefix: 'common' })}> | |||
| <Button | |||
| @@ -17,13 +17,14 @@ import { | |||
| MenuProps, | |||
| Space, | |||
| } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { | |||
| useHandleBreadcrumbClick, | |||
| useHandleDeleteFile, | |||
| useSelectBreadcrumbItems, | |||
| } from './hooks'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { | |||
| IListResult, | |||
| useFetchParentFolderList, | |||
| @@ -36,6 +37,7 @@ interface IProps | |||
| showFolderCreateModal: () => void; | |||
| showFileUploadModal: () => void; | |||
| setSelectedRowKeys: (keys: string[]) => void; | |||
| showMoveFileModal: (ids: string[]) => void; | |||
| } | |||
| const FileToolbar = ({ | |||
| @@ -45,6 +47,7 @@ const FileToolbar = ({ | |||
| setSelectedRowKeys, | |||
| searchString, | |||
| handleInputChange, | |||
| showMoveFileModal, | |||
| }: IProps) => { | |||
| const { t } = useTranslate('knowledgeDetails'); | |||
| const breadcrumbItems = useSelectBreadcrumbItems(); | |||
| @@ -111,6 +114,10 @@ const FileToolbar = ({ | |||
| setSelectedRowKeys, | |||
| ); | |||
| const handleShowMoveFileModal = useCallback(() => { | |||
| showMoveFileModal(selectedRowKeys); | |||
| }, [selectedRowKeys, showMoveFileModal]); | |||
| const disabled = selectedRowKeys.length === 0; | |||
| const items: MenuProps['items'] = useMemo(() => { | |||
| @@ -127,8 +134,20 @@ const FileToolbar = ({ | |||
| </Flex> | |||
| ), | |||
| }, | |||
| { | |||
| key: '5', | |||
| onClick: handleShowMoveFileModal, | |||
| label: ( | |||
| <Flex gap={10}> | |||
| <span className={styles.deleteIconWrapper}> | |||
| <SvgIcon name={`move`} width={18}></SvgIcon> | |||
| </span> | |||
| <b>{t('move', { keyPrefix: 'common' })}</b> | |||
| </Flex> | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [handleRemoveFile, t]); | |||
| }, [handleShowMoveFileModal, t, handleRemoveFile]); | |||
| return ( | |||
| <div className={styles.filter}> | |||
| @@ -21,24 +21,12 @@ const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => { | |||
| return onOk(ret.name); | |||
| }; | |||
| const handleCancel = () => { | |||
| hideModal(); | |||
| }; | |||
| const onFinish = (values: any) => { | |||
| console.log('Success:', values); | |||
| }; | |||
| const onFinishFailed = (errorInfo: any) => { | |||
| console.log('Failed:', errorInfo); | |||
| }; | |||
| return ( | |||
| <Modal | |||
| title={t('newFolder', { keyPrefix: 'fileManager' })} | |||
| open={visible} | |||
| onOk={handleOk} | |||
| onCancel={handleCancel} | |||
| onCancel={hideModal} | |||
| okButtonProps={{ loading }} | |||
| confirmLoading={loading} | |||
| > | |||
| @@ -47,8 +35,6 @@ const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => { | |||
| labelCol={{ span: 4 }} | |||
| wrapperCol={{ span: 20 }} | |||
| style={{ maxWidth: 600 }} | |||
| onFinish={onFinish} | |||
| onFinishFailed={onFinishFailed} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| @@ -4,6 +4,7 @@ import { | |||
| useCreateFolder, | |||
| useDeleteFile, | |||
| useFetchParentFolderList, | |||
| useMoveFile, | |||
| useRenameFile, | |||
| useUploadFile, | |||
| } from '@/hooks/file-manager-hooks'; | |||
| @@ -246,3 +247,48 @@ export const useHandleBreadcrumbClick = () => { | |||
| return { handleBreadcrumbClick }; | |||
| }; | |||
| export const useHandleMoveFile = ( | |||
| setSelectedRowKeys: (keys: string[]) => void, | |||
| ) => { | |||
| const { | |||
| visible: moveFileVisible, | |||
| hideModal: hideMoveFileModal, | |||
| showModal: showMoveFileModal, | |||
| } = useSetModalState(); | |||
| const { moveFile, loading } = useMoveFile(); | |||
| const [sourceFileIds, setSourceFileIds] = useState<string[]>([]); | |||
| const onMoveFileOk = useCallback( | |||
| async (targetFolderId: string) => { | |||
| const ret = await moveFile({ | |||
| src_file_ids: sourceFileIds, | |||
| dest_file_id: targetFolderId, | |||
| }); | |||
| if (ret === 0) { | |||
| setSelectedRowKeys([]); | |||
| hideMoveFileModal(); | |||
| } | |||
| return ret; | |||
| }, | |||
| [moveFile, hideMoveFileModal, sourceFileIds, setSelectedRowKeys], | |||
| ); | |||
| const handleShowMoveFileModal = useCallback( | |||
| (ids: string[]) => { | |||
| setSourceFileIds(ids); | |||
| showMoveFileModal(); | |||
| }, | |||
| [showMoveFileModal], | |||
| ); | |||
| return { | |||
| initialValue: '', | |||
| moveFileLoading: loading, | |||
| onMoveFileOk, | |||
| moveFileVisible, | |||
| hideMoveFileModal, | |||
| showMoveFileModal: handleShowMoveFileModal, | |||
| }; | |||
| }; | |||
| @@ -9,6 +9,7 @@ import { | |||
| useGetRowSelection, | |||
| useHandleConnectToKnowledge, | |||
| useHandleCreateFolder, | |||
| useHandleMoveFile, | |||
| useHandleUploadFile, | |||
| useNavigateToOtherFolder, | |||
| useRenameCurrentFile, | |||
| @@ -23,6 +24,7 @@ import { getExtension } from '@/utils/document-util'; | |||
| import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; | |||
| import FolderCreateModal from './folder-create-modal'; | |||
| import styles from './index.less'; | |||
| import FileMovingModal from './move-file-modal'; | |||
| const { Text } = Typography; | |||
| @@ -61,7 +63,13 @@ const FileManager = () => { | |||
| initialValue, | |||
| connectToKnowledgeLoading, | |||
| } = useHandleConnectToKnowledge(); | |||
| // const { pagination } = useGetFilesPagination(); | |||
| const { | |||
| showMoveFileModal, | |||
| moveFileVisible, | |||
| onMoveFileOk, | |||
| hideMoveFileModal, | |||
| moveFileLoading, | |||
| } = useHandleMoveFile(setSelectedRowKeys); | |||
| const { pagination, data, searchString, handleInputChange, loading } = | |||
| useFetchFileList(); | |||
| const columns: ColumnsType<IFile> = [ | |||
| @@ -139,6 +147,7 @@ const FileManager = () => { | |||
| console.info(record); | |||
| }} | |||
| showRenameModal={showFileRenameModal} | |||
| showMoveFileModal={showMoveFileModal} | |||
| showConnectToKnowledgeModal={showConnectToKnowledgeModal} | |||
| setSelectedRowKeys={setSelectedRowKeys} | |||
| ></ActionCell> | |||
| @@ -155,6 +164,7 @@ const FileManager = () => { | |||
| showFolderCreateModal={showFolderCreateModal} | |||
| showFileUploadModal={showFileUploadModal} | |||
| setSelectedRowKeys={setSelectedRowKeys} | |||
| showMoveFileModal={showMoveFileModal} | |||
| ></FileToolbar> | |||
| <Table | |||
| dataSource={data?.files} | |||
| @@ -191,6 +201,14 @@ const FileManager = () => { | |||
| onOk={onConnectToKnowledgeOk} | |||
| loading={connectToKnowledgeLoading} | |||
| ></ConnectToKnowledgeModal> | |||
| {moveFileVisible && ( | |||
| <FileMovingModal | |||
| visible={moveFileVisible} | |||
| hideModal={hideMoveFileModal} | |||
| onOk={onMoveFileOk} | |||
| loading={moveFileLoading} | |||
| ></FileMovingModal> | |||
| )} | |||
| </section> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,64 @@ | |||
| import { useFetchPureFileList } from '@/hooks/file-manager-hooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import type { GetProp, TreeSelectProps } from 'antd'; | |||
| import { TreeSelect } from 'antd'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| type DefaultOptionType = GetProp<TreeSelectProps, 'treeData'>[number]; | |||
| interface IProps { | |||
| value?: string; | |||
| onChange?: (value: string) => void; | |||
| } | |||
| const AsyncTreeSelect = ({ value, onChange }: IProps) => { | |||
| const { fetchList } = useFetchPureFileList(); | |||
| const [treeData, setTreeData] = useState<Omit<DefaultOptionType, 'label'>[]>( | |||
| [], | |||
| ); | |||
| const onLoadData: TreeSelectProps['loadData'] = useCallback( | |||
| async ({ id }) => { | |||
| const ret = await fetchList(id); | |||
| if (ret.retcode === 0) { | |||
| setTreeData((tree) => { | |||
| return tree.concat( | |||
| ret.data.files | |||
| .filter((x: IFile) => x.type === 'folder') | |||
| .map((x: IFile) => ({ | |||
| id: x.id, | |||
| pId: x.parent_id, | |||
| value: x.id, | |||
| title: x.name, | |||
| isLeaf: false, | |||
| })), | |||
| ); | |||
| }); | |||
| } | |||
| }, | |||
| [fetchList], | |||
| ); | |||
| const handleChange = (newValue: string) => { | |||
| onChange?.(newValue); | |||
| }; | |||
| useEffect(() => { | |||
| onLoadData?.({ id: '', props: '' }); | |||
| }, [onLoadData]); | |||
| return ( | |||
| <TreeSelect | |||
| treeDataSimpleMode | |||
| style={{ width: '100%' }} | |||
| value={value} | |||
| dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} | |||
| placeholder="Please select" | |||
| onChange={handleChange} | |||
| loadData={onLoadData} | |||
| treeData={treeData} | |||
| /> | |||
| ); | |||
| }; | |||
| export default AsyncTreeSelect; | |||
| @@ -0,0 +1,54 @@ | |||
| import { IModalManagerChildrenProps } from '@/components/modal-manager'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { Form, Modal } from 'antd'; | |||
| import AsyncTreeSelect from './async-tree-select'; | |||
| interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { | |||
| loading: boolean; | |||
| onOk: (id: string) => void; | |||
| } | |||
| const FileMovingModal = ({ visible, hideModal, loading, onOk }: IProps) => { | |||
| const [form] = Form.useForm(); | |||
| const { t } = useTranslate('fileManager'); | |||
| type FieldType = { | |||
| name?: string; | |||
| }; | |||
| const handleOk = async () => { | |||
| const ret = await form.validateFields(); | |||
| return onOk(ret.name); | |||
| }; | |||
| return ( | |||
| <Modal | |||
| title={t('move', { keyPrefix: 'common' })} | |||
| open={visible} | |||
| onOk={handleOk} | |||
| onCancel={hideModal} | |||
| okButtonProps={{ loading }} | |||
| confirmLoading={loading} | |||
| width={600} | |||
| > | |||
| <Form | |||
| name="basic" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| <Form.Item<FieldType> | |||
| label={t('destinationFolder')} | |||
| name="name" | |||
| rules={[{ required: true, message: t('pleaseSelect') }]} | |||
| > | |||
| <AsyncTreeSelect></AsyncTreeSelect> | |||
| </Form.Item> | |||
| </Form> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default FileMovingModal; | |||
| @@ -13,6 +13,7 @@ const { | |||
| connectFileToKnowledge, | |||
| get_document_file, | |||
| getFile, | |||
| moveFile, | |||
| } = api; | |||
| const methods = { | |||
| @@ -49,6 +50,10 @@ const methods = { | |||
| method: 'get', | |||
| responseType: 'blob', | |||
| }, | |||
| moveFile: { | |||
| url: moveFile, | |||
| method: 'post', | |||
| }, | |||
| } as const; | |||
| const fileManagerService = registerServer<keyof typeof methods>( | |||
| @@ -79,6 +79,7 @@ export default { | |||
| createFolder: `${api_host}/file/create`, | |||
| connectFileToKnowledge: `${api_host}/file2document/convert`, | |||
| getFile: `${api_host}/file/get`, | |||
| moveFile: `${api_host}/file/mv`, | |||
| // system | |||
| getSystemVersion: `${api_host}/system/version`, | |||