Browse Source

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)
tags/v0.10.0
balibabu 1 year ago
parent
commit
c55e9d16da
No account linked to committer's email address

+ 6
- 0
web/src/assets/svg/move.svg View File

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

+ 45
- 0
web/src/hooks/file-manager-hooks.ts View File

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();


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

+ 2
- 0
web/src/locales/en.ts View File

download: 'Download', download: 'Download',
close: 'Close', close: 'Close',
preview: 'Preview', preview: 'Preview',
move: 'Move',
}, },
login: { login: {
login: 'Sign in', login: 'Sign in',
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',

+ 2
- 0
web/src/locales/zh-traditional.ts View File

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

+ 2
- 0
web/src/locales/zh.ts View File

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

+ 24
- 6
web/src/pages/file-manager/action-cell/index.tsx View File

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,
} 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;
} }
showRenameModal, showRenameModal,
showConnectToKnowledgeModal, showConnectToKnowledgeModal,
setSelectedRowKeys, setSelectedRowKeys,
showMoveFileModal,
}: IProps) => { }: IProps) => {
const documentId = record.id; const documentId = record.id;
const beingUsed = false; const beingUsed = false;
showConnectToKnowledgeModal(record); showConnectToKnowledgeModal(record);
}; };


const onShowMoveFileModal = () => {
showMoveFileModal([documentId]);
};

return ( return (
<Space size={0}> <Space size={0}>
{isKnowledgeBase || ( {isKnowledgeBase || (
</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

+ 21
- 2
web/src/pages/file-manager/file-toolbar.tsx View File

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,
showFolderCreateModal: () => void; showFolderCreateModal: () => void;
showFileUploadModal: () => void; showFileUploadModal: () => void;
setSelectedRowKeys: (keys: string[]) => void; setSelectedRowKeys: (keys: string[]) => void;
showMoveFileModal: (ids: string[]) => void;
} }


const FileToolbar = ({ 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();
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(() => {
</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}>

+ 1
- 15
web/src/pages/file-manager/folder-create-modal/index.tsx View File

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

+ 46
- 0
web/src/pages/file-manager/hooks.ts View File

useCreateFolder, useCreateFolder,
useDeleteFile, useDeleteFile,
useFetchParentFolderList, useFetchParentFolderList,
useMoveFile,
useRenameFile, useRenameFile,
useUploadFile, useUploadFile,
} from '@/hooks/file-manager-hooks'; } from '@/hooks/file-manager-hooks';


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

+ 19
- 1
web/src/pages/file-manager/index.tsx View File

useGetRowSelection, useGetRowSelection,
useHandleConnectToKnowledge, useHandleConnectToKnowledge,
useHandleCreateFolder, useHandleCreateFolder,
useHandleMoveFile,
useHandleUploadFile, useHandleUploadFile,
useNavigateToOtherFolder, useNavigateToOtherFolder,
useRenameCurrentFile, useRenameCurrentFile,
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;


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> = [
console.info(record); console.info(record);
}} }}
showRenameModal={showFileRenameModal} showRenameModal={showFileRenameModal}
showMoveFileModal={showMoveFileModal}
showConnectToKnowledgeModal={showConnectToKnowledgeModal} showConnectToKnowledgeModal={showConnectToKnowledgeModal}
setSelectedRowKeys={setSelectedRowKeys} setSelectedRowKeys={setSelectedRowKeys}
></ActionCell> ></ActionCell>
showFolderCreateModal={showFolderCreateModal} showFolderCreateModal={showFolderCreateModal}
showFileUploadModal={showFileUploadModal} showFileUploadModal={showFileUploadModal}
setSelectedRowKeys={setSelectedRowKeys} setSelectedRowKeys={setSelectedRowKeys}
showMoveFileModal={showMoveFileModal}
></FileToolbar> ></FileToolbar>
<Table <Table
dataSource={data?.files} dataSource={data?.files}
onOk={onConnectToKnowledgeOk} onOk={onConnectToKnowledgeOk}
loading={connectToKnowledgeLoading} loading={connectToKnowledgeLoading}
></ConnectToKnowledgeModal> ></ConnectToKnowledgeModal>
{moveFileVisible && (
<FileMovingModal
visible={moveFileVisible}
hideModal={hideMoveFileModal}
onOk={onMoveFileOk}
loading={moveFileLoading}
></FileMovingModal>
)}
</section> </section>
); );
}; };

+ 64
- 0
web/src/pages/file-manager/move-file-modal/async-tree-select.tsx View File

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;

+ 54
- 0
web/src/pages/file-manager/move-file-modal/index.tsx View File

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;

+ 5
- 0
web/src/services/file-manager-service.ts View File

connectFileToKnowledge, connectFileToKnowledge,
get_document_file, get_document_file,
getFile, getFile,
moveFile,
} = api; } = api;


const methods = { 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>(

+ 1
- 0
web/src/utils/api.ts View File

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`,

Loading…
Cancel
Save