mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-23 06:46:40 +08:00
### 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)
This commit is contained in:
0
web/src/pages/file-manager/action-cell/index.less
Normal file
0
web/src/pages/file-manager/action-cell/index.less
Normal file
91
web/src/pages/file-manager/action-cell/index.tsx
Normal file
91
web/src/pages/file-manager/action-cell/index.tsx
Normal file
@ -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;
|
||||
153
web/src/pages/file-manager/file-toolbar.tsx
Normal file
153
web/src/pages/file-manager/file-toolbar.tsx
Normal file
@ -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;
|
||||
193
web/src/pages/file-manager/hooks.ts
Normal file
193
web/src/pages/file-manager/hooks.ts
Normal file
@ -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}`,
|
||||
}));
|
||||
};
|
||||
18
web/src/pages/file-manager/index.less
Normal file
18
web/src/pages/file-manager/index.less
Normal file
@ -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;
|
||||
}
|
||||
99
web/src/pages/file-manager/index.tsx
Normal file
99
web/src/pages/file-manager/index.tsx
Normal file
@ -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;
|
||||
65
web/src/pages/file-manager/model.ts
Normal file
65
web/src/pages/file-manager/model.ts
Normal file
@ -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;
|
||||
Reference in New Issue
Block a user