### 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)tags/v0.3.1
| @@ -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); | |||
| @@ -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} /> | |||
| @@ -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}> | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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']); | |||
| }; | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| }, | |||
| @@ -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>( | |||
| @@ -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`, | |||
| }; | |||