feat: create folder #345 (#518)

### What problem does this PR solve?

feat: create folder
feat: ensure that all files in the current folder can be correctly
requested after renaming the folder
#345 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-04-24 11:07:22 +08:00 committed by GitHub
parent 369400c483
commit 2d228dbf7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 285 additions and 47 deletions

View File

@ -22,10 +22,10 @@ export const useRemoveFile = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const removeFile = useCallback( const removeFile = useCallback(
(fileIds: string[]) => { (fileIds: string[], parentId: string) => {
return dispatch<any>({ return dispatch<any>({
type: 'fileManager/removeFile', type: 'fileManager/removeFile',
payload: { fileIds }, payload: { fileIds, parentId },
}); });
}, },
[dispatch], [dispatch],
@ -38,10 +38,10 @@ export const useRenameFile = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const renameFile = useCallback( const renameFile = useCallback(
(fileId: string, name: string) => { (fileId: string, name: string, parentId: string) => {
return dispatch<any>({ return dispatch<any>({
type: 'fileManager/renameFile', type: 'fileManager/renameFile',
payload: { fileId, name }, payload: { fileId, name, parentId },
}); });
}, },
[dispatch], [dispatch],
@ -66,6 +66,22 @@ export const useFetchParentFolderList = () => {
return fetchParentFolderList; return fetchParentFolderList;
}; };
export const useCreateFolder = () => {
const dispatch = useDispatch();
const createFolder = useCallback(
(parentId: string, name: string) => {
return dispatch<any>({
type: 'fileManager/createFolder',
payload: { parentId, name, type: 'folder' },
});
},
[dispatch],
);
return createFolder;
};
export const useSelectFileList = () => { export const useSelectFileList = () => {
const fileList = useSelector((state) => state.fileManager.fileList); const fileList = useSelector((state) => state.fileManager.fileList);

View File

@ -1,4 +1,5 @@
import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks'; import { useTranslate } from '@/hooks/commonHooks';
import { IFile } from '@/interfaces/database/file-manager';
import { api_host } from '@/utils/api'; import { api_host } from '@/utils/api';
import { downloadFile } from '@/utils/fileUtil'; import { downloadFile } from '@/utils/fileUtil';
import { import {
@ -8,9 +9,8 @@ import {
ToolOutlined, ToolOutlined,
} 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 { useRemoveFile } from '@/hooks/fileManagerHooks';
import { IFile } from '@/interfaces/database/file-manager';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
@ -23,18 +23,7 @@ const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => {
const documentId = record.id; const documentId = record.id;
const beingUsed = false; const beingUsed = false;
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
const removeDocument = useRemoveFile(); const { handleRemoveFile } = useHandleDeleteFile([documentId]);
const showDeleteConfirm = useShowDeleteConfirm();
const onRmDocument = () => {
if (!beingUsed) {
showDeleteConfirm({
onOk: () => {
return removeDocument([documentId]);
},
});
}
};
const onDownloadDocument = () => { const onDownloadDocument = () => {
downloadFile({ downloadFile({
@ -71,7 +60,7 @@ const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => {
<Button <Button
type="text" type="text"
disabled={beingUsed} disabled={beingUsed}
onClick={onRmDocument} onClick={handleRemoveFile}
className={styles.iconButton} className={styles.iconButton}
> >
<DeleteOutlined size={20} /> <DeleteOutlined size={20} />

View File

@ -1,9 +1,9 @@
import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg'; import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks'; import { useTranslate } from '@/hooks/commonHooks';
import { import {
DownOutlined, DownOutlined,
FileOutlined,
FileTextOutlined, FileTextOutlined,
FolderOpenOutlined,
PlusOutlined, PlusOutlined,
SearchOutlined, SearchOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@ -17,20 +17,21 @@ import {
MenuProps, MenuProps,
Space, Space,
} from 'antd'; } from 'antd';
import { useCallback, useMemo } from 'react'; import { useMemo } from 'react';
import { import {
useFetchDocumentListOnMount, useFetchDocumentListOnMount,
useGetPagination, useGetPagination,
useHandleDeleteFile,
useHandleSearchChange, useHandleSearchChange,
useSelectBreadcrumbItems, useSelectBreadcrumbItems,
} from './hooks'; } from './hooks';
import { useRemoveFile } from '@/hooks/fileManagerHooks';
import { Link } from 'umi'; import { Link } from 'umi';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
selectedRowKeys: string[]; selectedRowKeys: string[];
showFolderCreateModal: () => void;
} }
const itemRender: BreadcrumbProps['itemRender'] = ( const itemRender: BreadcrumbProps['itemRender'] = (
@ -47,13 +48,11 @@ const itemRender: BreadcrumbProps['itemRender'] = (
); );
}; };
const FileToolbar = ({ selectedRowKeys }: IProps) => { const FileToolbar = ({ selectedRowKeys, showFolderCreateModal }: IProps) => {
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
const { fetchDocumentList } = useFetchDocumentListOnMount(); const { fetchDocumentList } = useFetchDocumentListOnMount();
const { setPagination, searchString } = useGetPagination(fetchDocumentList); const { setPagination, searchString } = useGetPagination(fetchDocumentList);
const { handleInputChange } = useHandleSearchChange(setPagination); const { handleInputChange } = useHandleSearchChange(setPagination);
const removeDocument = useRemoveFile();
const showDeleteConfirm = useShowDeleteConfirm();
const breadcrumbItems = useSelectBreadcrumbItems(); const breadcrumbItems = useSelectBreadcrumbItems();
const actionItems: MenuProps['items'] = useMemo(() => { const actionItems: MenuProps['items'] = useMemo(() => {
@ -74,26 +73,21 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
{ type: 'divider' }, { type: 'divider' },
{ {
key: '2', key: '2',
onClick: showFolderCreateModal,
label: ( label: (
<div> <div>
<Button type="link"> <Button type="link">
<FileOutlined /> <FolderOpenOutlined />
{t('emptyFiles')} New Folder
</Button> </Button>
</div> </div>
), ),
// disabled: true, // disabled: true,
}, },
]; ];
}, [t]); }, [t, showFolderCreateModal]);
const handleDelete = useCallback(() => { const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys);
showDeleteConfirm({
onOk: () => {
return removeDocument(selectedRowKeys);
},
});
}, [removeDocument, showDeleteConfirm, selectedRowKeys]);
const disabled = selectedRowKeys.length === 0; const disabled = selectedRowKeys.length === 0;
@ -101,7 +95,7 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
return [ return [
{ {
key: '4', key: '4',
onClick: handleDelete, onClick: handleRemoveFile,
label: ( label: (
<Flex gap={10}> <Flex gap={10}>
<span className={styles.deleteIconWrapper}> <span className={styles.deleteIconWrapper}>
@ -112,7 +106,7 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
), ),
}, },
]; ];
}, [handleDelete, t]); }, [handleRemoveFile, t]);
return ( return (
<div className={styles.filter}> <div className={styles.filter}>

View File

@ -0,0 +1,64 @@
import { InboxOutlined } from '@ant-design/icons';
import { Modal, Segmented, Upload, UploadProps, message } from 'antd';
import { useState } from 'react';
const { Dragger } = Upload;
const FileUploadModal = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const props: UploadProps = {
name: 'file',
multiple: true,
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
onChange(info) {
const { status } = info.file;
if (status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (status === 'done') {
message.success(`${info.file.name} file uploaded successfully.`);
} else if (status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
onDrop(e) {
console.log('Dropped files', e.dataTransfer.files);
},
};
const handleOk = () => {
setIsModalOpen(false);
};
const handleCancel = () => {
setIsModalOpen(false);
};
return (
<>
<Modal
title="File upload"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
>
<Segmented options={['Local uploads', 'S3 uploads']} block />
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">
Click or drag file to this area to upload
</p>
<p className="ant-upload-hint">
Support for a single or bulk upload. Strictly prohibited from
uploading company data or other banned files.
</p>
</Dragger>
</Modal>
</>
);
};
export default FileUploadModal;

View File

@ -0,0 +1,67 @@
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { useTranslate } from '@/hooks/commonHooks';
import { Form, Input, Modal } from 'antd';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
onOk: (name: string) => void;
}
const FolderCreateModal = ({ visible, hideModal, loading, onOk }: IProps) => {
const [form] = Form.useForm();
const { t } = useTranslate('common');
type FieldType = {
name?: string;
};
const handleOk = async () => {
const ret = await form.validateFields();
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={'New Folder'}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
okButtonProps={{ loading }}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
label={t('name')}
name="name"
rules={[{ required: true, message: t('namePlaceholder') }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default FolderCreateModal;

View File

@ -1,7 +1,13 @@
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
import { import {
useSetModalState,
useShowDeleteConfirm,
useTranslate,
} from '@/hooks/commonHooks';
import {
useCreateFolder,
useFetchFileList, useFetchFileList,
useFetchParentFolderList, useFetchParentFolderList,
useRemoveFile,
useRenameFile, useRenameFile,
useSelectFileList, useSelectFileList,
useSelectParentFolderList, useSelectParentFolderList,
@ -144,7 +150,7 @@ export const useRenameCurrentFile = () => {
const onFileRenameOk = useCallback( const onFileRenameOk = useCallback(
async (name: string) => { async (name: string) => {
const ret = await renameFile(file.id, name); const ret = await renameFile(file.id, name, file.parent_id);
if (ret === 0) { if (ret === 0) {
hideFileRenameModal(); hideFileRenameModal();
@ -191,3 +197,56 @@ export const useSelectBreadcrumbItems = () => {
path: `/file?folderId=${x.id}`, path: `/file?folderId=${x.id}`,
})); }));
}; };
export const useHandleCreateFolder = () => {
const {
visible: folderCreateModalVisible,
hideModal: hideFolderCreateModal,
showModal: showFolderCreateModal,
} = useSetModalState();
const createFolder = useCreateFolder();
const id = useGetFolderId();
const onFolderCreateOk = useCallback(
async (name: string) => {
const ret = await createFolder(id, name);
if (ret === 0) {
hideFolderCreateModal();
}
},
[createFolder, hideFolderCreateModal, id],
);
const loading = useOneNamespaceEffectsLoading('fileManager', [
'createFolder',
]);
return {
folderCreateLoading: loading,
onFolderCreateOk,
folderCreateModalVisible,
hideFolderCreateModal,
showFolderCreateModal,
};
};
export const useHandleDeleteFile = (fileIds: string[]) => {
const removeDocument = useRemoveFile();
const showDeleteConfirm = useShowDeleteConfirm();
const parentId = useGetFolderId();
const handleRemoveFile = () => {
showDeleteConfirm({
onOk: () => {
return removeDocument(fileIds, parentId);
},
});
};
return { handleRemoveFile };
};
export const useSelectFileListLoading = () => {
return useOneNamespaceEffectsLoading('fileManager', ['listFile']);
};

View File

@ -7,16 +7,20 @@ import ActionCell from './action-cell';
import FileToolbar from './file-toolbar'; import FileToolbar from './file-toolbar';
import { import {
useGetRowSelection, useGetRowSelection,
useHandleCreateFolder,
useNavigateToOtherFolder, useNavigateToOtherFolder,
useRenameCurrentFile, useRenameCurrentFile,
useSelectFileListLoading,
} from './hooks'; } from './hooks';
import RenameModal from '@/components/rename-modal'; import RenameModal from '@/components/rename-modal';
import FolderCreateModal from './folder-create-modal';
import styles from './index.less'; import styles from './index.less';
const FileManager = () => { const FileManager = () => {
const fileList = useSelectFileList(); const fileList = useSelectFileList();
const rowSelection = useGetRowSelection(); const rowSelection = useGetRowSelection();
const loading = useSelectFileListLoading();
const navigateToOtherFolder = useNavigateToOtherFolder(); const navigateToOtherFolder = useNavigateToOtherFolder();
const { const {
fileRenameVisible, fileRenameVisible,
@ -26,6 +30,13 @@ const FileManager = () => {
initialFileName, initialFileName,
onFileRenameOk, onFileRenameOk,
} = useRenameCurrentFile(); } = useRenameCurrentFile();
const {
folderCreateModalVisible,
showFolderCreateModal,
hideFolderCreateModal,
folderCreateLoading,
onFolderCreateOk,
} = useHandleCreateFolder();
const columns: ColumnsType<IFile> = [ const columns: ColumnsType<IFile> = [
{ {
@ -78,12 +89,14 @@ const FileManager = () => {
<section className={styles.fileManagerWrapper}> <section className={styles.fileManagerWrapper}>
<FileToolbar <FileToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]} selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showFolderCreateModal={showFolderCreateModal}
></FileToolbar> ></FileToolbar>
<Table <Table
dataSource={fileList} dataSource={fileList}
columns={columns} columns={columns}
rowKey={'id'} rowKey={'id'}
rowSelection={rowSelection} rowSelection={rowSelection}
loading={loading}
/> />
<RenameModal <RenameModal
visible={fileRenameVisible} visible={fileRenameVisible}
@ -92,6 +105,12 @@ const FileManager = () => {
initialName={initialFileName} initialName={initialFileName}
loading={fileRenameLoading} loading={fileRenameLoading}
></RenameModal> ></RenameModal>
<FolderCreateModal
loading={folderCreateLoading}
visible={folderCreateModalVisible}
hideModal={hideFolderCreateModal}
onOk={onFolderCreateOk}
></FolderCreateModal>
</section> </section>
); );
}; };

View File

@ -1,5 +1,6 @@
import { IFile, IFolder } from '@/interfaces/database/file-manager'; import { IFile, IFolder } from '@/interfaces/database/file-manager';
import fileManagerService from '@/services/fileManagerService'; import fileManagerService from '@/services/fileManagerService';
import omit from 'lodash/omit';
import { DvaModel } from 'umi'; import { DvaModel } from 'umi';
export interface FileManagerModelState { export interface FileManagerModelState {
@ -20,12 +21,14 @@ const model: DvaModel<FileManagerModelState> = {
}, },
effects: { effects: {
*removeFile({ payload = {} }, { call, put }) { *removeFile({ payload = {} }, { call, put }) {
const { data } = yield call(fileManagerService.removeFile, payload); const { data } = yield call(fileManagerService.removeFile, {
fileIds: payload.fileIds,
});
const { retcode } = data; const { retcode } = data;
if (retcode === 0) { if (retcode === 0) {
yield put({ yield put({
type: 'listFile', type: 'listFile',
payload: data.data?.files ?? [], payload: { parentId: payload.parentId },
}); });
} }
}, },
@ -41,9 +44,25 @@ const model: DvaModel<FileManagerModelState> = {
} }
}, },
*renameFile({ payload = {} }, { call, put }) { *renameFile({ payload = {} }, { call, put }) {
const { data } = yield call(fileManagerService.renameFile, payload); const { data } = yield call(
fileManagerService.renameFile,
omit(payload, ['parentId']),
);
if (data.retcode === 0) { if (data.retcode === 0) {
yield put({ type: 'listFile' }); yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
});
}
return data.retcode;
},
*createFolder({ payload = {} }, { call, put }) {
const { data } = yield call(fileManagerService.createFolder, payload);
if (data.retcode === 0) {
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
});
} }
return data.retcode; return data.retcode;
}, },

View File

@ -2,8 +2,14 @@ import api from '@/utils/api';
import registerServer from '@/utils/registerServer'; import registerServer from '@/utils/registerServer';
import request from '@/utils/request'; import request from '@/utils/request';
const { listFile, removeFile, uploadFile, renameFile, getAllParentFolder } = const {
api; listFile,
removeFile,
uploadFile,
renameFile,
getAllParentFolder,
createFolder,
} = api;
const methods = { const methods = {
listFile: { listFile: {
@ -26,6 +32,10 @@ const methods = {
url: getAllParentFolder, url: getAllParentFolder,
method: 'get', method: 'get',
}, },
createFolder: {
url: createFolder,
method: 'post',
},
} as const; } as const;
const fileManagerService = registerServer<keyof typeof methods>( const fileManagerService = registerServer<keyof typeof methods>(

View File

@ -73,4 +73,5 @@ export default {
removeFile: `${api_host}/file/rm`, removeFile: `${api_host}/file/rm`,
renameFile: `${api_host}/file/rename`, renameFile: `${api_host}/file/rename`,
getAllParentFolder: `${api_host}/file/all_parent_folder`, getAllParentFolder: `${api_host}/file/all_parent_folder`,
createFolder: `${api_host}/file/create`,
}; };