fix: cannot save the system model setting #468 (#508)

### 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:
balibabu
2024-04-23 17:46:56 +08:00
committed by GitHub
parent aa71462a9f
commit 6405041b4d
22 changed files with 990 additions and 70 deletions

View 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;

View 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;

View 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}`,
}));
};

View 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;
}

View 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;

View 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;