feat: Move files in file manager #1826 (#1837)

### 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)
This commit is contained in:
balibabu 2024-08-07 10:12:11 +08:00 committed by GitHub
parent 4c2906d6fd
commit c55e9d16da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 292 additions and 24 deletions

View File

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

After

Width:  |  Height:  |  Size: 877 B

View File

@ -28,6 +28,23 @@ export interface IListResult {
loading: boolean; 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 => { export const useFetchFileList = (): ResponseType<any> & IListResult => {
const { searchString, handleInputChange } = useHandleSearchChange(); const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter(); const { pagination, setPagination } = useGetPaginationWithRouter();
@ -225,3 +242,31 @@ export const useConnectToKnowledge = () => {
return { data, loading, connectFileToKnowledge: mutateAsync }; 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 };
};

View File

@ -26,6 +26,7 @@ export default {
download: 'Download', download: 'Download',
close: 'Close', close: 'Close',
preview: 'Preview', preview: 'Preview',
move: 'Move',
}, },
login: { login: {
login: 'Sign in', login: 'Sign in',
@ -564,6 +565,7 @@ The above is the content you need to summarize.`,
fileError: 'File error', fileError: 'File error',
uploadLimit: uploadLimit:
'The file size cannot exceed 10M, and the total number of files cannot exceed 128', 'The file size cannot exceed 10M, and the total number of files cannot exceed 128',
destinationFolder: 'Destination folder',
}, },
flow: { flow: {
cite: 'Cite', cite: 'Cite',

View File

@ -26,6 +26,7 @@ export default {
download: '下載', download: '下載',
close: '關閉', close: '關閉',
preview: '預覽', preview: '預覽',
move: '移動',
}, },
login: { login: {
login: '登入', login: '登入',
@ -524,6 +525,7 @@ export default {
preview: '預覽', preview: '預覽',
fileError: '文件錯誤', fileError: '文件錯誤',
uploadLimit: '文件大小不能超過10M文件總數不超過128個', uploadLimit: '文件大小不能超過10M文件總數不超過128個',
destinationFolder: '目標資料夾',
}, },
flow: { flow: {
cite: '引用', cite: '引用',

View File

@ -26,6 +26,7 @@ export default {
download: '下载', download: '下载',
close: '关闭', close: '关闭',
preview: '预览', preview: '预览',
move: '移动',
}, },
login: { login: {
login: '登录', login: '登录',
@ -542,6 +543,7 @@ export default {
preview: '预览', preview: '预览',
fileError: '文件错误', fileError: '文件错误',
uploadLimit: '文件大小不能超过10M文件总数不超过128个', uploadLimit: '文件大小不能超过10M文件总数不超过128个',
destinationFolder: '目标文件夹',
}, },
flow: { flow: {
flow: '工作流', flow: '工作流',

View File

@ -1,6 +1,12 @@
import NewDocumentLink from '@/components/new-document-link';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { IFile } from '@/interfaces/database/file-manager'; import { IFile } from '@/interfaces/database/file-manager';
import { api_host } from '@/utils/api'; import { api_host } from '@/utils/api';
import {
getExtension,
isSupportedPreviewDocumentType,
} from '@/utils/document-util';
import { downloadFile } from '@/utils/file-util'; import { downloadFile } from '@/utils/file-util';
import { import {
DeleteOutlined, DeleteOutlined,
@ -11,18 +17,13 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button, Space, Tooltip } from 'antd'; import { Button, Space, Tooltip } from 'antd';
import { useHandleDeleteFile } from '../hooks'; import { useHandleDeleteFile } from '../hooks';
import NewDocumentLink from '@/components/new-document-link';
import {
getExtension,
isSupportedPreviewDocumentType,
} from '@/utils/document-util';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
record: IFile; record: IFile;
setCurrentRecord: (record: any) => void; setCurrentRecord: (record: any) => void;
showRenameModal: (record: IFile) => void; showRenameModal: (record: IFile) => void;
showMoveFileModal: (ids: string[]) => void;
showConnectToKnowledgeModal: (record: IFile) => void; showConnectToKnowledgeModal: (record: IFile) => void;
setSelectedRowKeys(keys: string[]): void; setSelectedRowKeys(keys: string[]): void;
} }
@ -33,6 +34,7 @@ const ActionCell = ({
showRenameModal, showRenameModal,
showConnectToKnowledgeModal, showConnectToKnowledgeModal,
setSelectedRowKeys, setSelectedRowKeys,
showMoveFileModal,
}: IProps) => { }: IProps) => {
const documentId = record.id; const documentId = record.id;
const beingUsed = false; const beingUsed = false;
@ -64,6 +66,10 @@ const ActionCell = ({
showConnectToKnowledgeModal(record); showConnectToKnowledgeModal(record);
}; };
const onShowMoveFileModal = () => {
showMoveFileModal([documentId]);
};
return ( return (
<Space size={0}> <Space size={0}>
{isKnowledgeBase || ( {isKnowledgeBase || (
@ -90,6 +96,18 @@ const ActionCell = ({
</Button> </Button>
</Tooltip> </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 || ( {isKnowledgeBase || (
<Tooltip title={t('delete', { keyPrefix: 'common' })}> <Tooltip title={t('delete', { keyPrefix: 'common' })}>
<Button <Button

View File

@ -17,13 +17,14 @@ import {
MenuProps, MenuProps,
Space, Space,
} from 'antd'; } from 'antd';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { import {
useHandleBreadcrumbClick, useHandleBreadcrumbClick,
useHandleDeleteFile, useHandleDeleteFile,
useSelectBreadcrumbItems, useSelectBreadcrumbItems,
} from './hooks'; } from './hooks';
import SvgIcon from '@/components/svg-icon';
import { import {
IListResult, IListResult,
useFetchParentFolderList, useFetchParentFolderList,
@ -36,6 +37,7 @@ interface IProps
showFolderCreateModal: () => void; showFolderCreateModal: () => void;
showFileUploadModal: () => void; showFileUploadModal: () => void;
setSelectedRowKeys: (keys: string[]) => void; setSelectedRowKeys: (keys: string[]) => void;
showMoveFileModal: (ids: string[]) => void;
} }
const FileToolbar = ({ const FileToolbar = ({
@ -45,6 +47,7 @@ const FileToolbar = ({
setSelectedRowKeys, setSelectedRowKeys,
searchString, searchString,
handleInputChange, handleInputChange,
showMoveFileModal,
}: IProps) => { }: IProps) => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
const breadcrumbItems = useSelectBreadcrumbItems(); const breadcrumbItems = useSelectBreadcrumbItems();
@ -111,6 +114,10 @@ const FileToolbar = ({
setSelectedRowKeys, setSelectedRowKeys,
); );
const handleShowMoveFileModal = useCallback(() => {
showMoveFileModal(selectedRowKeys);
}, [selectedRowKeys, showMoveFileModal]);
const disabled = selectedRowKeys.length === 0; const disabled = selectedRowKeys.length === 0;
const items: MenuProps['items'] = useMemo(() => { const items: MenuProps['items'] = useMemo(() => {
@ -127,8 +134,20 @@ const FileToolbar = ({
</Flex> </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 ( return (
<div className={styles.filter}> <div className={styles.filter}>

View File

@ -21,24 +21,12 @@ const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => {
return onOk(ret.name); return onOk(ret.name);
}; };
const handleCancel = () => {
hideModal();
};
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
return ( return (
<Modal <Modal
title={t('newFolder', { keyPrefix: 'fileManager' })} title={t('newFolder', { keyPrefix: 'fileManager' })}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={hideModal}
okButtonProps={{ loading }} okButtonProps={{ loading }}
confirmLoading={loading} confirmLoading={loading}
> >
@ -47,8 +35,6 @@ const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => {
labelCol={{ span: 4 }} labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }} wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }} style={{ maxWidth: 600 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off" autoComplete="off"
form={form} form={form}
> >

View File

@ -4,6 +4,7 @@ import {
useCreateFolder, useCreateFolder,
useDeleteFile, useDeleteFile,
useFetchParentFolderList, useFetchParentFolderList,
useMoveFile,
useRenameFile, useRenameFile,
useUploadFile, useUploadFile,
} from '@/hooks/file-manager-hooks'; } from '@/hooks/file-manager-hooks';
@ -246,3 +247,48 @@ export const useHandleBreadcrumbClick = () => {
return { handleBreadcrumbClick }; 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,
};
};

View File

@ -9,6 +9,7 @@ import {
useGetRowSelection, useGetRowSelection,
useHandleConnectToKnowledge, useHandleConnectToKnowledge,
useHandleCreateFolder, useHandleCreateFolder,
useHandleMoveFile,
useHandleUploadFile, useHandleUploadFile,
useNavigateToOtherFolder, useNavigateToOtherFolder,
useRenameCurrentFile, useRenameCurrentFile,
@ -23,6 +24,7 @@ import { getExtension } from '@/utils/document-util';
import ConnectToKnowledgeModal from './connect-to-knowledge-modal'; import ConnectToKnowledgeModal from './connect-to-knowledge-modal';
import FolderCreateModal from './folder-create-modal'; import FolderCreateModal from './folder-create-modal';
import styles from './index.less'; import styles from './index.less';
import FileMovingModal from './move-file-modal';
const { Text } = Typography; const { Text } = Typography;
@ -61,7 +63,13 @@ const FileManager = () => {
initialValue, initialValue,
connectToKnowledgeLoading, connectToKnowledgeLoading,
} = useHandleConnectToKnowledge(); } = useHandleConnectToKnowledge();
// const { pagination } = useGetFilesPagination(); const {
showMoveFileModal,
moveFileVisible,
onMoveFileOk,
hideMoveFileModal,
moveFileLoading,
} = useHandleMoveFile(setSelectedRowKeys);
const { pagination, data, searchString, handleInputChange, loading } = const { pagination, data, searchString, handleInputChange, loading } =
useFetchFileList(); useFetchFileList();
const columns: ColumnsType<IFile> = [ const columns: ColumnsType<IFile> = [
@ -139,6 +147,7 @@ const FileManager = () => {
console.info(record); console.info(record);
}} }}
showRenameModal={showFileRenameModal} showRenameModal={showFileRenameModal}
showMoveFileModal={showMoveFileModal}
showConnectToKnowledgeModal={showConnectToKnowledgeModal} showConnectToKnowledgeModal={showConnectToKnowledgeModal}
setSelectedRowKeys={setSelectedRowKeys} setSelectedRowKeys={setSelectedRowKeys}
></ActionCell> ></ActionCell>
@ -155,6 +164,7 @@ const FileManager = () => {
showFolderCreateModal={showFolderCreateModal} showFolderCreateModal={showFolderCreateModal}
showFileUploadModal={showFileUploadModal} showFileUploadModal={showFileUploadModal}
setSelectedRowKeys={setSelectedRowKeys} setSelectedRowKeys={setSelectedRowKeys}
showMoveFileModal={showMoveFileModal}
></FileToolbar> ></FileToolbar>
<Table <Table
dataSource={data?.files} dataSource={data?.files}
@ -191,6 +201,14 @@ const FileManager = () => {
onOk={onConnectToKnowledgeOk} onOk={onConnectToKnowledgeOk}
loading={connectToKnowledgeLoading} loading={connectToKnowledgeLoading}
></ConnectToKnowledgeModal> ></ConnectToKnowledgeModal>
{moveFileVisible && (
<FileMovingModal
visible={moveFileVisible}
hideModal={hideMoveFileModal}
onOk={onMoveFileOk}
loading={moveFileLoading}
></FileMovingModal>
)}
</section> </section>
); );
}; };

View File

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

View File

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

View File

@ -13,6 +13,7 @@ const {
connectFileToKnowledge, connectFileToKnowledge,
get_document_file, get_document_file,
getFile, getFile,
moveFile,
} = api; } = api;
const methods = { const methods = {
@ -49,6 +50,10 @@ const methods = {
method: 'get', method: 'get',
responseType: 'blob', responseType: 'blob',
}, },
moveFile: {
url: moveFile,
method: 'post',
},
} as const; } as const;
const fileManagerService = registerServer<keyof typeof methods>( const fileManagerService = registerServer<keyof typeof methods>(

View File

@ -79,6 +79,7 @@ export default {
createFolder: `${api_host}/file/create`, createFolder: `${api_host}/file/create`,
connectFileToKnowledge: `${api_host}/file2document/convert`, connectFileToKnowledge: `${api_host}/file2document/convert`,
getFile: `${api_host}/file/get`, getFile: `${api_host}/file/get`,
moveFile: `${api_host}/file/mv`,
// system // system
getSystemVersion: `${api_host}/system/version`, getSystemVersion: `${api_host}/system/version`,