### What problem does this PR solve? fix: cannot save the system model setting #468 feat: rename file in FileManager feat: add FileManager feat: override useSelector type ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.3.1
| @@ -27,7 +27,7 @@ export default defineConfig({ | |||
| devtool: 'source-map', | |||
| proxy: { | |||
| '/v1': { | |||
| target: 'http://123.60.95.134:9380/', | |||
| target: 'http://192.168.200.233:9380/', | |||
| changeOrigin: true, | |||
| // pathRewrite: { '^/v1': '/v1' }, | |||
| }, | |||
| @@ -0,0 +1,138 @@ | |||
| // This file is generated by Umi automatically | |||
| // DO NOT CHANGE IT MANUALLY! | |||
| type CSSModuleClasses = { readonly [key: string]: string }; | |||
| declare module '*.css' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| declare module '*.scss' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| declare module '*.sass' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| declare module '*.less' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| declare module '*.styl' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| declare module '*.stylus' { | |||
| const classes: CSSModuleClasses; | |||
| export default classes; | |||
| } | |||
| // images | |||
| declare module '*.jpg' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.jpeg' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.png' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.gif' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.svg' { | |||
| import * as React from 'react'; | |||
| export const ReactComponent: React.FunctionComponent< | |||
| React.SVGProps<SVGSVGElement> & { title?: string } | |||
| >; | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.ico' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.webp' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.avif' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| // media | |||
| declare module '*.mp4' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.webm' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.ogg' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.mp3' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.wav' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.flac' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.aac' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| // fonts | |||
| declare module '*.woff' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.woff2' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.eot' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.ttf' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.otf' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| // other | |||
| declare module '*.wasm' { | |||
| const initWasm: ( | |||
| options: WebAssembly.Imports, | |||
| ) => Promise<WebAssembly.Exports>; | |||
| export default initWasm; | |||
| } | |||
| declare module '*.webmanifest' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.pdf' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| declare module '*.txt' { | |||
| const src: string; | |||
| export default src; | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| import { IFileListRequestBody } from '@/interfaces/request/file-manager'; | |||
| import { useCallback } from 'react'; | |||
| import { useDispatch, useSelector } from 'umi'; | |||
| export const useFetchFileList = () => { | |||
| const dispatch = useDispatch(); | |||
| const fetchFileList = useCallback( | |||
| (payload: IFileListRequestBody) => { | |||
| return dispatch<any>({ | |||
| type: 'fileManager/listFile', | |||
| payload, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| return fetchFileList; | |||
| }; | |||
| export const useRemoveFile = () => { | |||
| const dispatch = useDispatch(); | |||
| const removeFile = useCallback( | |||
| (fileIds: string[]) => { | |||
| return dispatch<any>({ | |||
| type: 'fileManager/removeFile', | |||
| payload: { fileIds }, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| return removeFile; | |||
| }; | |||
| export const useRenameFile = () => { | |||
| const dispatch = useDispatch(); | |||
| const renameFile = useCallback( | |||
| (fileId: string, name: string) => { | |||
| return dispatch<any>({ | |||
| type: 'fileManager/renameFile', | |||
| payload: { fileId, name }, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| return renameFile; | |||
| }; | |||
| export const useFetchParentFolderList = () => { | |||
| const dispatch = useDispatch(); | |||
| const fetchParentFolderList = useCallback( | |||
| (fileId: string) => { | |||
| return dispatch<any>({ | |||
| type: 'fileManager/getAllParentFolder', | |||
| payload: { fileId }, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| return fetchParentFolderList; | |||
| }; | |||
| export const useSelectFileList = () => { | |||
| const fileList = useSelector((state) => state.fileManager.fileList); | |||
| return fileList; | |||
| }; | |||
| export const useSelectParentFolderList = () => { | |||
| const parentFolderList = useSelector( | |||
| (state) => state.fileManager.parentFolderList, | |||
| ); | |||
| return parentFolderList.toReversed(); | |||
| }; | |||
| @@ -0,0 +1,30 @@ | |||
| export interface IFile { | |||
| create_date: string; | |||
| create_time: number; | |||
| created_by: string; | |||
| id: string; | |||
| kb_ids: string[]; | |||
| location: string; | |||
| name: string; | |||
| parent_id: string; | |||
| size: number; | |||
| tenant_id: string; | |||
| type: string; | |||
| update_date: string; | |||
| update_time: number; | |||
| } | |||
| export interface IFolder { | |||
| create_date: string; | |||
| create_time: number; | |||
| created_by: string; | |||
| id: string; | |||
| location: string; | |||
| name: string; | |||
| parent_id: string; | |||
| size: number; | |||
| tenant_id: string; | |||
| type: string; | |||
| update_date: string; | |||
| update_time: number; | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| export interface IPaginationRequestBody { | |||
| keywords?: string; | |||
| page?: number; | |||
| page_size?: number; // name|create|doc_num|create_time|update_time,default:create_time | |||
| orderby?: string; | |||
| desc?: string; | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| import { IPaginationRequestBody } from './base'; | |||
| export interface IFileListRequestBody extends IPaginationRequestBody { | |||
| parent_id?: string; // folder id | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import { ReactComponent as StarIon } from '@/assets/svg/chat-star.svg'; | |||
| // import { ReactComponent as FileIcon } from '@/assets/svg/file-management.svg'; | |||
| import { ReactComponent as KnowledgeBaseIcon } from '@/assets/svg/knowledge-base.svg'; | |||
| import { ReactComponent as Logo } from '@/assets/svg/logo.svg'; | |||
| import { useTranslate } from '@/hooks/commonHooks'; | |||
| @@ -182,7 +182,14 @@ const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => { | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [handleDelete, handleRunClick, handleCancelClick, t]); | |||
| }, [ | |||
| handleDelete, | |||
| handleRunClick, | |||
| handleCancelClick, | |||
| t, | |||
| handleDisableClick, | |||
| handleEnableClick, | |||
| ]); | |||
| return ( | |||
| <div className={styles.filter}> | |||
| @@ -0,0 +1,91 @@ | |||
| import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { downloadFile } from '@/utils/fileUtil'; | |||
| import { | |||
| DeleteOutlined, | |||
| DownloadOutlined, | |||
| EditOutlined, | |||
| ToolOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { Button, Space, Tooltip } from 'antd'; | |||
| import { useRemoveFile } from '@/hooks/fileManagerHooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| record: IFile; | |||
| setCurrentRecord: (record: any) => void; | |||
| showRenameModal: (record: IFile) => void; | |||
| } | |||
| 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 onDownloadDocument = () => { | |||
| downloadFile({ | |||
| url: `${api_host}/document/get/${documentId}`, | |||
| filename: record.name, | |||
| }); | |||
| }; | |||
| const setRecord = () => { | |||
| setCurrentRecord(record); | |||
| }; | |||
| const onShowRenameModal = () => { | |||
| setRecord(); | |||
| showRenameModal(record); | |||
| }; | |||
| return ( | |||
| <Space size={0}> | |||
| <Button type="text" className={styles.iconButton}> | |||
| <ToolOutlined size={20} /> | |||
| </Button> | |||
| <Tooltip title={t('rename', { keyPrefix: 'common' })}> | |||
| <Button | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={onShowRenameModal} | |||
| className={styles.iconButton} | |||
| > | |||
| <EditOutlined size={20} /> | |||
| </Button> | |||
| </Tooltip> | |||
| <Button | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={onRmDocument} | |||
| className={styles.iconButton} | |||
| > | |||
| <DeleteOutlined size={20} /> | |||
| </Button> | |||
| <Button | |||
| type="text" | |||
| disabled={beingUsed} | |||
| onClick={onDownloadDocument} | |||
| className={styles.iconButton} | |||
| > | |||
| <DownloadOutlined size={20} /> | |||
| </Button> | |||
| </Space> | |||
| ); | |||
| }; | |||
| export default ActionCell; | |||
| @@ -0,0 +1,153 @@ | |||
| import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg'; | |||
| import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks'; | |||
| import { | |||
| DownOutlined, | |||
| FileOutlined, | |||
| FileTextOutlined, | |||
| PlusOutlined, | |||
| SearchOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { | |||
| Breadcrumb, | |||
| BreadcrumbProps, | |||
| Button, | |||
| Dropdown, | |||
| Flex, | |||
| Input, | |||
| MenuProps, | |||
| Space, | |||
| } from 'antd'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { | |||
| useFetchDocumentListOnMount, | |||
| useGetPagination, | |||
| useHandleSearchChange, | |||
| useSelectBreadcrumbItems, | |||
| } from './hooks'; | |||
| import { useRemoveFile } from '@/hooks/fileManagerHooks'; | |||
| import { Link } from 'umi'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| selectedRowKeys: string[]; | |||
| } | |||
| const itemRender: BreadcrumbProps['itemRender'] = ( | |||
| currentRoute, | |||
| params, | |||
| items, | |||
| ) => { | |||
| const isLast = currentRoute?.path === items[items.length - 1]?.path; | |||
| return isLast ? ( | |||
| <span>{currentRoute.title}</span> | |||
| ) : ( | |||
| <Link to={`${currentRoute.path}`}>{currentRoute.title}</Link> | |||
| ); | |||
| }; | |||
| const FileToolbar = ({ selectedRowKeys }: 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(() => { | |||
| return [ | |||
| { | |||
| key: '1', | |||
| label: ( | |||
| <div> | |||
| <Button type="link"> | |||
| <Space> | |||
| <FileTextOutlined /> | |||
| {t('localFiles')} | |||
| </Space> | |||
| </Button> | |||
| </div> | |||
| ), | |||
| }, | |||
| { type: 'divider' }, | |||
| { | |||
| key: '2', | |||
| label: ( | |||
| <div> | |||
| <Button type="link"> | |||
| <FileOutlined /> | |||
| {t('emptyFiles')} | |||
| </Button> | |||
| </div> | |||
| ), | |||
| // disabled: true, | |||
| }, | |||
| ]; | |||
| }, [t]); | |||
| const handleDelete = useCallback(() => { | |||
| showDeleteConfirm({ | |||
| onOk: () => { | |||
| return removeDocument(selectedRowKeys); | |||
| }, | |||
| }); | |||
| }, [removeDocument, showDeleteConfirm, selectedRowKeys]); | |||
| const disabled = selectedRowKeys.length === 0; | |||
| const items: MenuProps['items'] = useMemo(() => { | |||
| return [ | |||
| { | |||
| key: '4', | |||
| onClick: handleDelete, | |||
| label: ( | |||
| <Flex gap={10}> | |||
| <span className={styles.deleteIconWrapper}> | |||
| <DeleteIcon width={18} /> | |||
| </span> | |||
| <b>{t('delete', { keyPrefix: 'common' })}</b> | |||
| </Flex> | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [handleDelete, t]); | |||
| return ( | |||
| <div className={styles.filter}> | |||
| <Breadcrumb items={breadcrumbItems} itemRender={itemRender} /> | |||
| <Space> | |||
| <Dropdown | |||
| menu={{ items }} | |||
| placement="bottom" | |||
| arrow={false} | |||
| disabled={disabled} | |||
| > | |||
| <Button> | |||
| <Space> | |||
| <b> {t('bulk')}</b> | |||
| <DownOutlined /> | |||
| </Space> | |||
| </Button> | |||
| </Dropdown> | |||
| <Input | |||
| placeholder={t('searchFiles')} | |||
| value={searchString} | |||
| style={{ width: 220 }} | |||
| allowClear | |||
| onChange={handleInputChange} | |||
| prefix={<SearchOutlined />} | |||
| /> | |||
| <Dropdown menu={{ items: actionItems }} trigger={['click']}> | |||
| <Button type="primary" icon={<PlusOutlined />}> | |||
| {t('addFile')} | |||
| </Button> | |||
| </Dropdown> | |||
| </Space> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default FileToolbar; | |||
| @@ -0,0 +1,193 @@ | |||
| import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; | |||
| import { | |||
| useFetchFileList, | |||
| useFetchParentFolderList, | |||
| useRenameFile, | |||
| useSelectFileList, | |||
| useSelectParentFolderList, | |||
| } from '@/hooks/fileManagerHooks'; | |||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | |||
| import { Pagination } from '@/interfaces/common'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { PaginationProps } from 'antd'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { useDispatch, useNavigate, useSearchParams, useSelector } from 'umi'; | |||
| export const useGetFolderId = () => { | |||
| const [searchParams] = useSearchParams(); | |||
| const id = searchParams.get('folderId') as string; | |||
| return id; | |||
| }; | |||
| export const useFetchDocumentListOnMount = () => { | |||
| const fetchDocumentList = useFetchFileList(); | |||
| const fileList = useSelectFileList(); | |||
| const id = useGetFolderId(); | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| fetchDocumentList({ parent_id: id }); | |||
| }, [dispatch, fetchDocumentList, id]); | |||
| return { fetchDocumentList, fileList }; | |||
| }; | |||
| export const useGetPagination = ( | |||
| fetchDocumentList: (payload: IFile) => any, | |||
| ) => { | |||
| const dispatch = useDispatch(); | |||
| const kFModel = useSelector((state: any) => state.kFModel); | |||
| const { t } = useTranslate('common'); | |||
| const setPagination = useCallback( | |||
| (pageNumber = 1, pageSize?: number) => { | |||
| const pagination: Pagination = { | |||
| current: pageNumber, | |||
| } as Pagination; | |||
| if (pageSize) { | |||
| pagination.pageSize = pageSize; | |||
| } | |||
| dispatch({ | |||
| type: 'kFModel/setPagination', | |||
| payload: pagination, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| const onPageChange: PaginationProps['onChange'] = useCallback( | |||
| (pageNumber: number, pageSize: number) => { | |||
| setPagination(pageNumber, pageSize); | |||
| fetchDocumentList(); | |||
| }, | |||
| [fetchDocumentList, setPagination], | |||
| ); | |||
| const pagination: PaginationProps = useMemo(() => { | |||
| return { | |||
| showQuickJumper: true, | |||
| total: kFModel.total, | |||
| showSizeChanger: true, | |||
| current: kFModel.pagination.current, | |||
| pageSize: kFModel.pagination.pageSize, | |||
| pageSizeOptions: [1, 2, 10, 20, 50, 100], | |||
| onChange: onPageChange, | |||
| showTotal: (total) => `${t('total')} ${total}`, | |||
| }; | |||
| }, [kFModel, onPageChange, t]); | |||
| return { | |||
| pagination, | |||
| setPagination, | |||
| total: kFModel.total, | |||
| searchString: kFModel.searchString, | |||
| }; | |||
| }; | |||
| export const useHandleSearchChange = (setPagination: () => void) => { | |||
| const dispatch = useDispatch(); | |||
| const throttledGetDocumentList = useCallback(() => { | |||
| dispatch({ | |||
| type: 'kFModel/throttledGetDocumentList', | |||
| }); | |||
| }, [dispatch]); | |||
| const handleInputChange = useCallback( | |||
| (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { | |||
| const value = e.target.value; | |||
| dispatch({ type: 'kFModel/setSearchString', payload: value }); | |||
| setPagination(); | |||
| throttledGetDocumentList(); | |||
| }, | |||
| [setPagination, throttledGetDocumentList, dispatch], | |||
| ); | |||
| return { handleInputChange }; | |||
| }; | |||
| export const useGetRowSelection = () => { | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const rowSelection = { | |||
| selectedRowKeys, | |||
| onChange: (newSelectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(newSelectedRowKeys); | |||
| }, | |||
| }; | |||
| return rowSelection; | |||
| }; | |||
| export const useNavigateToOtherFolder = () => { | |||
| const navigate = useNavigate(); | |||
| const navigateToOtherFolder = useCallback( | |||
| (folderId: string) => { | |||
| navigate(`/file?folderId=${folderId}`); | |||
| }, | |||
| [navigate], | |||
| ); | |||
| return navigateToOtherFolder; | |||
| }; | |||
| export const useRenameCurrentFile = () => { | |||
| const [file, setFile] = useState<IFile>({} as IFile); | |||
| const { | |||
| visible: fileRenameVisible, | |||
| hideModal: hideFileRenameModal, | |||
| showModal: showFileRenameModal, | |||
| } = useSetModalState(); | |||
| const renameFile = useRenameFile(); | |||
| const onFileRenameOk = useCallback( | |||
| async (name: string) => { | |||
| const ret = await renameFile(file.id, name); | |||
| if (ret === 0) { | |||
| hideFileRenameModal(); | |||
| } | |||
| }, | |||
| [renameFile, file, hideFileRenameModal], | |||
| ); | |||
| const loading = useOneNamespaceEffectsLoading('fileManager', ['renameFile']); | |||
| const handleShowFileRenameModal = useCallback( | |||
| async (record: IFile) => { | |||
| setFile(record); | |||
| showFileRenameModal(); | |||
| }, | |||
| [showFileRenameModal], | |||
| ); | |||
| return { | |||
| fileRenameLoading: loading, | |||
| initialFileName: file.name, | |||
| onFileRenameOk, | |||
| fileRenameVisible, | |||
| hideFileRenameModal, | |||
| showFileRenameModal: handleShowFileRenameModal, | |||
| }; | |||
| }; | |||
| export const useSelectBreadcrumbItems = () => { | |||
| const parentFolderList = useSelectParentFolderList(); | |||
| const id = useGetFolderId(); | |||
| const fetchParentFolderList = useFetchParentFolderList(); | |||
| useEffect(() => { | |||
| if (id) { | |||
| fetchParentFolderList(id); | |||
| } | |||
| }, [id, fetchParentFolderList]); | |||
| return parentFolderList.length === 1 | |||
| ? [] | |||
| : parentFolderList.map((x) => ({ | |||
| title: x.name === '/' ? 'root' : x.name, | |||
| path: `/file?folderId=${x.id}`, | |||
| })); | |||
| }; | |||
| @@ -0,0 +1,18 @@ | |||
| .fileManagerWrapper { | |||
| flex-basis: 100%; | |||
| padding: 32px; | |||
| } | |||
| .filter { | |||
| height: 32px; | |||
| display: flex; | |||
| margin: 10px 0; | |||
| justify-content: space-between; | |||
| padding: 24px 0; | |||
| align-items: center; | |||
| } | |||
| .deleteIconWrapper { | |||
| width: 22px; | |||
| text-align: center; | |||
| } | |||
| @@ -0,0 +1,99 @@ | |||
| import { useSelectFileList } from '@/hooks/fileManagerHooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Button, Table } from 'antd'; | |||
| import { ColumnsType } from 'antd/es/table'; | |||
| import ActionCell from './action-cell'; | |||
| import FileToolbar from './file-toolbar'; | |||
| import { | |||
| useGetRowSelection, | |||
| useNavigateToOtherFolder, | |||
| useRenameCurrentFile, | |||
| } from './hooks'; | |||
| import RenameModal from '@/components/rename-modal'; | |||
| import styles from './index.less'; | |||
| const FileManager = () => { | |||
| const fileList = useSelectFileList(); | |||
| const rowSelection = useGetRowSelection(); | |||
| const navigateToOtherFolder = useNavigateToOtherFolder(); | |||
| const { | |||
| fileRenameVisible, | |||
| fileRenameLoading, | |||
| hideFileRenameModal, | |||
| showFileRenameModal, | |||
| initialFileName, | |||
| onFileRenameOk, | |||
| } = useRenameCurrentFile(); | |||
| const columns: ColumnsType<IFile> = [ | |||
| { | |||
| title: 'Name', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render(value, record) { | |||
| return record.type === 'folder' ? ( | |||
| <Button | |||
| type={'link'} | |||
| onClick={() => navigateToOtherFolder(record.id)} | |||
| > | |||
| {value} | |||
| </Button> | |||
| ) : ( | |||
| value | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: 'Upload Date', | |||
| dataIndex: 'create_date', | |||
| key: 'create_date', | |||
| render(text) { | |||
| return formatDate(text); | |||
| }, | |||
| }, | |||
| { | |||
| title: 'Location', | |||
| dataIndex: 'location', | |||
| key: 'location', | |||
| }, | |||
| { | |||
| title: 'Action', | |||
| dataIndex: 'action', | |||
| key: 'action', | |||
| render: (text, record) => ( | |||
| <ActionCell | |||
| record={record} | |||
| setCurrentRecord={(record: any) => { | |||
| console.info(record); | |||
| }} | |||
| showRenameModal={showFileRenameModal} | |||
| ></ActionCell> | |||
| ), | |||
| }, | |||
| ]; | |||
| return ( | |||
| <section className={styles.fileManagerWrapper}> | |||
| <FileToolbar | |||
| selectedRowKeys={rowSelection.selectedRowKeys as string[]} | |||
| ></FileToolbar> | |||
| <Table | |||
| dataSource={fileList} | |||
| columns={columns} | |||
| rowKey={'id'} | |||
| rowSelection={rowSelection} | |||
| /> | |||
| <RenameModal | |||
| visible={fileRenameVisible} | |||
| hideModal={hideFileRenameModal} | |||
| onOk={onFileRenameOk} | |||
| initialName={initialFileName} | |||
| loading={fileRenameLoading} | |||
| ></RenameModal> | |||
| </section> | |||
| ); | |||
| }; | |||
| export default FileManager; | |||
| @@ -0,0 +1,65 @@ | |||
| import { IFile, IFolder } from '@/interfaces/database/file-manager'; | |||
| import fileManagerService from '@/services/fileManagerService'; | |||
| import { DvaModel } from 'umi'; | |||
| export interface FileManagerModelState { | |||
| fileList: IFile[]; | |||
| parentFolderList: IFolder[]; | |||
| } | |||
| const model: DvaModel<FileManagerModelState> = { | |||
| namespace: 'fileManager', | |||
| state: { fileList: [], parentFolderList: [] }, | |||
| reducers: { | |||
| setFileList(state, { payload }) { | |||
| return { ...state, fileList: payload }; | |||
| }, | |||
| setParentFolderList(state, { payload }) { | |||
| return { ...state, parentFolderList: payload }; | |||
| }, | |||
| }, | |||
| effects: { | |||
| *removeFile({ payload = {} }, { call, put }) { | |||
| const { data } = yield call(fileManagerService.removeFile, payload); | |||
| const { retcode } = data; | |||
| if (retcode === 0) { | |||
| yield put({ | |||
| type: 'listFile', | |||
| payload: data.data?.files ?? [], | |||
| }); | |||
| } | |||
| }, | |||
| *listFile({ payload = {} }, { call, put }) { | |||
| const { data } = yield call(fileManagerService.listFile, payload); | |||
| const { retcode, data: res } = data; | |||
| if (retcode === 0 && Array.isArray(res.files)) { | |||
| yield put({ | |||
| type: 'setFileList', | |||
| payload: res.files, | |||
| }); | |||
| } | |||
| }, | |||
| *renameFile({ payload = {} }, { call, put }) { | |||
| const { data } = yield call(fileManagerService.renameFile, payload); | |||
| if (data.retcode === 0) { | |||
| yield put({ type: 'listFile' }); | |||
| } | |||
| return data.retcode; | |||
| }, | |||
| *getAllParentFolder({ payload = {} }, { call, put }) { | |||
| const { data } = yield call( | |||
| fileManagerService.getAllParentFolder, | |||
| payload, | |||
| ); | |||
| if (data.retcode === 0) { | |||
| yield put({ | |||
| type: 'setParentFolderList', | |||
| payload: data.data?.parent_folders ?? [], | |||
| }); | |||
| } | |||
| return data.retcode; | |||
| }, | |||
| }, | |||
| }; | |||
| export default model; | |||
| @@ -1,50 +0,0 @@ | |||
| import { UploadOutlined } from '@ant-design/icons'; | |||
| import { Button, Upload } from 'antd'; | |||
| import React, { useEffect, useState } from 'react'; | |||
| const File: React.FC = () => { | |||
| const [fileList, setFileList] = useState([ | |||
| { | |||
| uid: '0', | |||
| name: 'xxx.png', | |||
| status: 'uploading', | |||
| percent: 10, | |||
| }, | |||
| ]); | |||
| const obj = { | |||
| uid: '-1', | |||
| name: 'yyy.png', | |||
| status: 'done', | |||
| url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', | |||
| thumbUrl: | |||
| 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', | |||
| }; | |||
| useEffect(() => { | |||
| const timer = setInterval(() => { | |||
| setFileList((fileList: any) => { | |||
| const percent = fileList[0]?.percent; | |||
| if (percent + 10 >= 100) { | |||
| clearInterval(timer); | |||
| return [obj]; | |||
| } | |||
| const list = [{ ...fileList[0], percent: percent + 10 }]; | |||
| console.log(list); | |||
| return list; | |||
| }); | |||
| }, 300); | |||
| }, []); | |||
| return ( | |||
| <> | |||
| <Upload | |||
| action="https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188" | |||
| listType="picture" | |||
| fileList={[...fileList]} | |||
| multiple | |||
| > | |||
| <Button icon={<UploadOutlined />}>Upload</Button> | |||
| </Upload> | |||
| </> | |||
| ); | |||
| }; | |||
| export default File; | |||
| @@ -167,20 +167,22 @@ const Login = () => { | |||
| Sign in with Google | |||
| </div> | |||
| </Button> */} | |||
| <Button | |||
| block | |||
| size="large" | |||
| onClick={toGoogle} | |||
| style={{ marginTop: 15 }} | |||
| > | |||
| <div> | |||
| <Icon | |||
| icon="local:github" | |||
| style={{ verticalAlign: 'middle', marginRight: 5 }} | |||
| /> | |||
| Sign in with Github | |||
| </div> | |||
| </Button> | |||
| {location.host === 'demo.ragflow.io' && ( | |||
| <Button | |||
| block | |||
| size="large" | |||
| onClick={toGoogle} | |||
| style={{ marginTop: 15 }} | |||
| > | |||
| <div> | |||
| <Icon | |||
| icon="local:github" | |||
| style={{ verticalAlign: 'middle', marginRight: 5 }} | |||
| /> | |||
| Sign in with Github | |||
| </div> | |||
| </Button> | |||
| )} | |||
| </> | |||
| )} | |||
| </Form> | |||
| @@ -82,7 +82,7 @@ const routes = [ | |||
| }, | |||
| { | |||
| path: '/file', | |||
| component: '@/pages/file', | |||
| component: '@/pages/file-manager', | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -0,0 +1,36 @@ | |||
| import api from '@/utils/api'; | |||
| import registerServer from '@/utils/registerServer'; | |||
| import request from '@/utils/request'; | |||
| const { listFile, removeFile, uploadFile, renameFile, getAllParentFolder } = | |||
| api; | |||
| const methods = { | |||
| listFile: { | |||
| url: listFile, | |||
| method: 'get', | |||
| }, | |||
| removeFile: { | |||
| url: removeFile, | |||
| method: 'post', | |||
| }, | |||
| uploadFile: { | |||
| url: uploadFile, | |||
| method: 'post', | |||
| }, | |||
| renameFile: { | |||
| url: renameFile, | |||
| method: 'post', | |||
| }, | |||
| getAllParentFolder: { | |||
| url: getAllParentFolder, | |||
| method: 'get', | |||
| }, | |||
| } as const; | |||
| const fileManagerService = registerServer<keyof typeof methods>( | |||
| methods, | |||
| request, | |||
| ); | |||
| export default fileManagerService; | |||
| @@ -66,4 +66,11 @@ export default { | |||
| createExternalConversation: `${api_host}/api/new_conversation`, | |||
| getExternalConversation: `${api_host}/api/conversation`, | |||
| completeExternalConversation: `${api_host}/api/completion`, | |||
| // file manager | |||
| listFile: `${api_host}/file/list`, | |||
| uploadFile: `${api_host}/file/upload`, | |||
| removeFile: `${api_host}/file/rm`, | |||
| renameFile: `${api_host}/file/rename`, | |||
| getAllParentFolder: `${api_host}/file/all_parent_folder`, | |||
| }; | |||
| @@ -5,11 +5,18 @@ export const isFormData = (data: unknown): data is FormData => { | |||
| return data instanceof FormData; | |||
| }; | |||
| const excludedFields = ['img2txt_id']; | |||
| const isExcludedField = (key: string) => { | |||
| return excludedFields.includes(key); | |||
| }; | |||
| export const convertTheKeysOfTheObjectToSnake = (data: unknown) => { | |||
| if (isObject(data) && !isFormData(data)) { | |||
| return Object.keys(data).reduce<Record<string, any>>((pre, cur) => { | |||
| const value = (data as Record<string, any>)[cur]; | |||
| pre[isFormData(value) ? cur : snakeCase(cur)] = value; | |||
| pre[isFormData(value) || isExcludedField(cur) ? cur : snakeCase(cur)] = | |||
| value; | |||
| return pre; | |||
| }, {}); | |||
| } | |||
| @@ -1,8 +1,39 @@ | |||
| import 'umi/typings'; | |||
| import { ChunkModelState } from '@/pages/add-knowledge/components/knowledge-chunk/model'; | |||
| import { KFModelState } from '@/pages/add-knowledge/components/knowledge-file/model'; | |||
| import { KSModelState } from '@/pages/add-knowledge/components/knowledge-setting/model'; | |||
| import { TestingModelState } from '@/pages/add-knowledge/components/knowledge-testing/model'; | |||
| import { kAModelState } from '@/pages/add-knowledge/model'; | |||
| import { ChatModelState } from '@/pages/chat/model'; | |||
| import { FileManagerModelState } from '@/pages/file-manager/model'; | |||
| import { KnowledgeModelState } from '@/pages/knowledge/model'; | |||
| import { LoginModelState } from '@/pages/login/model'; | |||
| import { SettingModelState } from '@/pages/user-setting/model'; | |||
| declare module 'lodash'; | |||
| // declare type Nullable<T> = T | null; invalid | |||
| function useSelector<TState = RootState, TSelected = unknown>( | |||
| selector: (state: TState) => TSelected, | |||
| equalityFn?: (left: TSelected, right: TSelected) => boolean, | |||
| ): TSelected; | |||
| export interface RootState { | |||
| // loading: Loading; | |||
| fileManager: FileManagerModelState; | |||
| chatModel: ChatModelState; | |||
| loginModel: LoginModelState; | |||
| knowledgeModel: KnowledgeModelState; | |||
| settingModel: SettingModelState; | |||
| kFModel: KFModelState; | |||
| kAModel: kAModelState; | |||
| chunkModel: ChunkModelState; | |||
| kSModel: KSModelState; | |||
| testingModel: TestingModelState; | |||
| } | |||
| declare global { | |||
| type Nullable<T> = T | null; | |||
| } | |||
| declare module 'umi' { | |||
| export { useSelector }; | |||
| } | |||