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 removeFile = useCallback(
(fileIds: string[]) => {
(fileIds: string[], parentId: string) => {
return dispatch<any>({
type: 'fileManager/removeFile',
payload: { fileIds },
payload: { fileIds, parentId },
});
},
[dispatch],
@ -38,10 +38,10 @@ export const useRenameFile = () => {
const dispatch = useDispatch();
const renameFile = useCallback(
(fileId: string, name: string) => {
(fileId: string, name: string, parentId: string) => {
return dispatch<any>({
type: 'fileManager/renameFile',
payload: { fileId, name },
payload: { fileId, name, parentId },
});
},
[dispatch],
@ -66,6 +66,22 @@ export const useFetchParentFolderList = () => {
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 = () => {
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 { downloadFile } from '@/utils/fileUtil';
import {
@ -8,9 +9,8 @@ import {
ToolOutlined,
} from '@ant-design/icons';
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';
interface IProps {
@ -23,18 +23,7 @@ const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => {
const documentId = record.id;
const beingUsed = false;
const { t } = useTranslate('knowledgeDetails');
const removeDocument = useRemoveFile();
const showDeleteConfirm = useShowDeleteConfirm();
const onRmDocument = () => {
if (!beingUsed) {
showDeleteConfirm({
onOk: () => {
return removeDocument([documentId]);
},
});
}
};
const { handleRemoveFile } = useHandleDeleteFile([documentId]);
const onDownloadDocument = () => {
downloadFile({
@ -71,7 +60,7 @@ const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => {
<Button
type="text"
disabled={beingUsed}
onClick={onRmDocument}
onClick={handleRemoveFile}
className={styles.iconButton}
>
<DeleteOutlined size={20} />

View File

@ -1,9 +1,9 @@
import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks';
import { useTranslate } from '@/hooks/commonHooks';
import {
DownOutlined,
FileOutlined,
FileTextOutlined,
FolderOpenOutlined,
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
@ -17,20 +17,21 @@ import {
MenuProps,
Space,
} from 'antd';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import {
useFetchDocumentListOnMount,
useGetPagination,
useHandleDeleteFile,
useHandleSearchChange,
useSelectBreadcrumbItems,
} from './hooks';
import { useRemoveFile } from '@/hooks/fileManagerHooks';
import { Link } from 'umi';
import styles from './index.less';
interface IProps {
selectedRowKeys: string[];
showFolderCreateModal: () => void;
}
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 { fetchDocumentList } = useFetchDocumentListOnMount();
const { setPagination, searchString } = useGetPagination(fetchDocumentList);
const { handleInputChange } = useHandleSearchChange(setPagination);
const removeDocument = useRemoveFile();
const showDeleteConfirm = useShowDeleteConfirm();
const breadcrumbItems = useSelectBreadcrumbItems();
const actionItems: MenuProps['items'] = useMemo(() => {
@ -74,26 +73,21 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
{ type: 'divider' },
{
key: '2',
onClick: showFolderCreateModal,
label: (
<div>
<Button type="link">
<FileOutlined />
{t('emptyFiles')}
<FolderOpenOutlined />
New Folder
</Button>
</div>
),
// disabled: true,
},
];
}, [t]);
}, [t, showFolderCreateModal]);
const handleDelete = useCallback(() => {
showDeleteConfirm({
onOk: () => {
return removeDocument(selectedRowKeys);
},
});
}, [removeDocument, showDeleteConfirm, selectedRowKeys]);
const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys);
const disabled = selectedRowKeys.length === 0;
@ -101,7 +95,7 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
return [
{
key: '4',
onClick: handleDelete,
onClick: handleRemoveFile,
label: (
<Flex gap={10}>
<span className={styles.deleteIconWrapper}>
@ -112,7 +106,7 @@ const FileToolbar = ({ selectedRowKeys }: IProps) => {
),
},
];
}, [handleDelete, t]);
}, [handleRemoveFile, t]);
return (
<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 {
useSetModalState,
useShowDeleteConfirm,
useTranslate,
} from '@/hooks/commonHooks';
import {
useCreateFolder,
useFetchFileList,
useFetchParentFolderList,
useRemoveFile,
useRenameFile,
useSelectFileList,
useSelectParentFolderList,
@ -144,7 +150,7 @@ export const useRenameCurrentFile = () => {
const onFileRenameOk = useCallback(
async (name: string) => {
const ret = await renameFile(file.id, name);
const ret = await renameFile(file.id, name, file.parent_id);
if (ret === 0) {
hideFileRenameModal();
@ -191,3 +197,56 @@ export const useSelectBreadcrumbItems = () => {
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 {
useGetRowSelection,
useHandleCreateFolder,
useNavigateToOtherFolder,
useRenameCurrentFile,
useSelectFileListLoading,
} from './hooks';
import RenameModal from '@/components/rename-modal';
import FolderCreateModal from './folder-create-modal';
import styles from './index.less';
const FileManager = () => {
const fileList = useSelectFileList();
const rowSelection = useGetRowSelection();
const loading = useSelectFileListLoading();
const navigateToOtherFolder = useNavigateToOtherFolder();
const {
fileRenameVisible,
@ -26,6 +30,13 @@ const FileManager = () => {
initialFileName,
onFileRenameOk,
} = useRenameCurrentFile();
const {
folderCreateModalVisible,
showFolderCreateModal,
hideFolderCreateModal,
folderCreateLoading,
onFolderCreateOk,
} = useHandleCreateFolder();
const columns: ColumnsType<IFile> = [
{
@ -78,12 +89,14 @@ const FileManager = () => {
<section className={styles.fileManagerWrapper}>
<FileToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showFolderCreateModal={showFolderCreateModal}
></FileToolbar>
<Table
dataSource={fileList}
columns={columns}
rowKey={'id'}
rowSelection={rowSelection}
loading={loading}
/>
<RenameModal
visible={fileRenameVisible}
@ -92,6 +105,12 @@ const FileManager = () => {
initialName={initialFileName}
loading={fileRenameLoading}
></RenameModal>
<FolderCreateModal
loading={folderCreateLoading}
visible={folderCreateModalVisible}
hideModal={hideFolderCreateModal}
onOk={onFolderCreateOk}
></FolderCreateModal>
</section>
);
};

View File

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

View File

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

View File

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