From 2d228dbf7f9ab7b989e51665499f689cef8fb5ea Mon Sep 17 00:00:00 2001
From: balibabu <cike8899@users.noreply.github.com>
Date: Wed, 24 Apr 2024 11:07:22 +0800
Subject: [PATCH] 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)
---
 web/src/hooks/fileManagerHooks.ts             | 24 +++++--
 .../pages/file-manager/action-cell/index.tsx  | 21 ++----
 web/src/pages/file-manager/file-toolbar.tsx   | 32 ++++-----
 .../file-manager/file-upload-modal/index.tsx  | 64 ++++++++++++++++++
 .../folder-create-modal/index.tsx             | 67 +++++++++++++++++++
 web/src/pages/file-manager/hooks.ts           | 63 ++++++++++++++++-
 web/src/pages/file-manager/index.tsx          | 19 ++++++
 web/src/pages/file-manager/model.ts           | 27 ++++++--
 web/src/services/fileManagerService.ts        | 14 +++-
 web/src/utils/api.ts                          |  1 +
 10 files changed, 285 insertions(+), 47 deletions(-)
 create mode 100644 web/src/pages/file-manager/file-upload-modal/index.tsx
 create mode 100644 web/src/pages/file-manager/folder-create-modal/index.tsx

diff --git a/web/src/hooks/fileManagerHooks.ts b/web/src/hooks/fileManagerHooks.ts
index a9fd72c..8410aa7 100644
--- a/web/src/hooks/fileManagerHooks.ts
+++ b/web/src/hooks/fileManagerHooks.ts
@@ -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);
 
diff --git a/web/src/pages/file-manager/action-cell/index.tsx b/web/src/pages/file-manager/action-cell/index.tsx
index b6e44a2..518a507 100644
--- a/web/src/pages/file-manager/action-cell/index.tsx
+++ b/web/src/pages/file-manager/action-cell/index.tsx
@@ -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} />
diff --git a/web/src/pages/file-manager/file-toolbar.tsx b/web/src/pages/file-manager/file-toolbar.tsx
index cd01e62..72f4e38 100644
--- a/web/src/pages/file-manager/file-toolbar.tsx
+++ b/web/src/pages/file-manager/file-toolbar.tsx
@@ -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}>
diff --git a/web/src/pages/file-manager/file-upload-modal/index.tsx b/web/src/pages/file-manager/file-upload-modal/index.tsx
new file mode 100644
index 0000000..f457292
--- /dev/null
+++ b/web/src/pages/file-manager/file-upload-modal/index.tsx
@@ -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;
diff --git a/web/src/pages/file-manager/folder-create-modal/index.tsx b/web/src/pages/file-manager/folder-create-modal/index.tsx
new file mode 100644
index 0000000..e16511f
--- /dev/null
+++ b/web/src/pages/file-manager/folder-create-modal/index.tsx
@@ -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;
diff --git a/web/src/pages/file-manager/hooks.ts b/web/src/pages/file-manager/hooks.ts
index 6a43c33..af09cf5 100644
--- a/web/src/pages/file-manager/hooks.ts
+++ b/web/src/pages/file-manager/hooks.ts
@@ -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']);
+};
diff --git a/web/src/pages/file-manager/index.tsx b/web/src/pages/file-manager/index.tsx
index 83dee7a..829d810 100644
--- a/web/src/pages/file-manager/index.tsx
+++ b/web/src/pages/file-manager/index.tsx
@@ -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>
   );
 };
diff --git a/web/src/pages/file-manager/model.ts b/web/src/pages/file-manager/model.ts
index e58faa9..e145755 100644
--- a/web/src/pages/file-manager/model.ts
+++ b/web/src/pages/file-manager/model.ts
@@ -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',
+          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' });
+        yield put({
+          type: 'listFile',
+          payload: { parentId: payload.parentId },
+        });
       }
       return data.retcode;
     },
diff --git a/web/src/services/fileManagerService.ts b/web/src/services/fileManagerService.ts
index 9edcc2f..01e152e 100644
--- a/web/src/services/fileManagerService.ts
+++ b/web/src/services/fileManagerService.ts
@@ -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>(
diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts
index f449f93..8b9847e 100644
--- a/web/src/utils/api.ts
+++ b/web/src/utils/api.ts
@@ -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`,
 };
-- 
GitLab