### 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
| 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], | ||||
| 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], | ||||
| 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); | ||||
| 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 { | ||||
| 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 { | ||||
| 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 showDeleteConfirm = useShowDeleteConfirm(); | |||||
| const onRmDocument = () => { | |||||
| if (!beingUsed) { | |||||
| showDeleteConfirm({ | |||||
| onOk: () => { | |||||
| return removeDocument([documentId]); | |||||
| }, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| const { handleRemoveFile } = useHandleDeleteFile([documentId]); | |||||
| const onDownloadDocument = () => { | const onDownloadDocument = () => { | ||||
| downloadFile({ | downloadFile({ | ||||
| <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} /> |
| 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'; | ||||
| 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'] = ( | ||||
| ); | ); | ||||
| }; | }; | ||||
| 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(() => { | ||||
| { type: 'divider' }, | { type: 'divider' }, | ||||
| { | { | ||||
| key: '2', | key: '2', | ||||
| onClick: showFolderCreateModal, | |||||
| label: ( | label: ( | ||||
| <div> | <div> | ||||
| <Button type="link"> | <Button type="link"> | ||||
| <FileOutlined /> | |||||
| {t('emptyFiles')} | |||||
| <FolderOpenOutlined /> | |||||
| New Folder | |||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| ), | ), | ||||
| // disabled: true, | // 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; | const disabled = selectedRowKeys.length === 0; | ||||
| 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}> | ||||
| ), | ), | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [handleDelete, t]); | |||||
| }, [handleRemoveFile, t]); | |||||
| return ( | return ( | ||||
| <div className={styles.filter}> | <div className={styles.filter}> |
| 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; |
| 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; |
| 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, | ||||
| 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(); | ||||
| 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']); | |||||
| }; |
| 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, | ||||
| initialFileName, | initialFileName, | ||||
| onFileRenameOk, | onFileRenameOk, | ||||
| } = useRenameCurrentFile(); | } = useRenameCurrentFile(); | ||||
| const { | |||||
| folderCreateModalVisible, | |||||
| showFolderCreateModal, | |||||
| hideFolderCreateModal, | |||||
| folderCreateLoading, | |||||
| onFolderCreateOk, | |||||
| } = useHandleCreateFolder(); | |||||
| const columns: ColumnsType<IFile> = [ | const columns: ColumnsType<IFile> = [ | ||||
| { | { | ||||
| <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} | ||||
| initialName={initialFileName} | initialName={initialFileName} | ||||
| loading={fileRenameLoading} | loading={fileRenameLoading} | ||||
| ></RenameModal> | ></RenameModal> | ||||
| <FolderCreateModal | |||||
| loading={folderCreateLoading} | |||||
| visible={folderCreateModalVisible} | |||||
| hideModal={hideFolderCreateModal} | |||||
| onOk={onFolderCreateOk} | |||||
| ></FolderCreateModal> | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| }; | }; |
| 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 { | ||||
| }, | }, | ||||
| 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 }, | |||||
| }); | }); | ||||
| } | } | ||||
| }, | }, | ||||
| } | } | ||||
| }, | }, | ||||
| *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) { | |||||
| 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) { | if (data.retcode === 0) { | ||||
| yield put({ type: 'listFile' }); | |||||
| yield put({ | |||||
| type: 'listFile', | |||||
| payload: { parentId: payload.parentId }, | |||||
| }); | |||||
| } | } | ||||
| return data.retcode; | return data.retcode; | ||||
| }, | }, |
| 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 } = | |||||
| api; | |||||
| const { | |||||
| listFile, | |||||
| removeFile, | |||||
| uploadFile, | |||||
| renameFile, | |||||
| getAllParentFolder, | |||||
| createFolder, | |||||
| } = api; | |||||
| const methods = { | const methods = { | ||||
| listFile: { | listFile: { | ||||
| 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>( |
| 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`, | |||||
| }; | }; |