feat: upload file in FileManager #345 (#529)

### What problem does this PR solve?

feat: upload file in FileManager #345 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-04-25 08:46:18 +08:00 committed by GitHub
parent b06d6395bb
commit 51e7697df7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 412 additions and 50 deletions

View File

@ -1,4 +1,8 @@
import { IFileListRequestBody } from '@/interfaces/request/file-manager'; import {
IConnectRequestBody,
IFileListRequestBody,
} from '@/interfaces/request/file-manager';
import { UploadFile } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'umi'; import { useDispatch, useSelector } from 'umi';
@ -94,3 +98,47 @@ export const useSelectParentFolderList = () => {
); );
return parentFolderList.toReversed(); return parentFolderList.toReversed();
}; };
export const useUploadFile = () => {
const dispatch = useDispatch();
const uploadFile = useCallback(
(file: UploadFile, parentId: string, path: string) => {
try {
return dispatch<any>({
type: 'fileManager/uploadFile',
payload: {
file,
parentId,
path,
},
});
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
},
[dispatch],
);
return uploadFile;
};
export const useConnectToKnowledge = () => {
const dispatch = useDispatch();
const uploadFile = useCallback(
(payload: IConnectRequestBody) => {
try {
return dispatch<any>({
type: 'fileManager/connectFileToKnowledge',
payload,
});
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
},
[dispatch],
);
return uploadFile;
};

View File

@ -3,7 +3,7 @@ export interface IFile {
create_time: number; create_time: number;
created_by: string; created_by: string;
id: string; id: string;
kb_ids: string[]; kbs_info: { kb_id: string; kb_name: string }[];
location: string; location: string;
name: string; name: string;
parent_id: string; parent_id: string;

View File

@ -3,3 +3,12 @@ import { IPaginationRequestBody } from './base';
export interface IFileListRequestBody extends IPaginationRequestBody { export interface IFileListRequestBody extends IPaginationRequestBody {
parent_id?: string; // folder id parent_id?: string; // folder id
} }
interface BaseRequestBody {
parentId: string;
}
export interface IConnectRequestBody extends BaseRequestBody {
fileIds: string[];
kbIds: string[];
}

View File

@ -382,7 +382,7 @@ export default {
passwordDescription: passwordDescription:
'Please enter your current password to change your password.', 'Please enter your current password to change your password.',
model: 'Model Providers', model: 'Model Providers',
modelDescription: 'Manage your account settings and preferences here.', modelDescription: 'Set the model parameter and API Key here.',
team: 'Team', team: 'Team',
logout: 'Log out', logout: 'Log out',
username: 'Username', username: 'Username',

View File

@ -352,7 +352,7 @@ export default {
password: '密碼', password: '密碼',
passwordDescription: '請輸入您當前的密碼以更改您的密碼。', passwordDescription: '請輸入您當前的密碼以更改您的密碼。',
model: '模型提供商', model: '模型提供商',
modelDescription: '在此管理您的帳戶設置和首選項。', modelDescription: '在此設置模型參數和 API Key。',
team: '團隊', team: '團隊',
logout: '登出', logout: '登出',
username: '使用者名稱', username: '使用者名稱',

View File

@ -369,7 +369,7 @@ export default {
password: '密码', password: '密码',
passwordDescription: '请输入您当前的密码以更改您的密码。', passwordDescription: '请输入您当前的密码以更改您的密码。',
model: '模型提供商', model: '模型提供商',
modelDescription: '在此管理您的帐户设置和首选项。', modelDescription: '在此设置模型参数和 API Key。',
team: '团队', team: '团队',
logout: '登出', logout: '登出',
username: '用户名', username: '用户名',

View File

@ -17,9 +17,15 @@ interface IProps {
record: IFile; record: IFile;
setCurrentRecord: (record: any) => void; setCurrentRecord: (record: any) => void;
showRenameModal: (record: IFile) => void; showRenameModal: (record: IFile) => void;
showConnectToKnowledgeModal: (ids: string[]) => void;
} }
const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => { const ActionCell = ({
record,
setCurrentRecord,
showRenameModal,
showConnectToKnowledgeModal,
}: IProps) => {
const documentId = record.id; const documentId = record.id;
const beingUsed = false; const beingUsed = false;
const { t } = useTranslate('knowledgeDetails'); const { t } = useTranslate('knowledgeDetails');
@ -41,9 +47,17 @@ const ActionCell = ({ record, setCurrentRecord, showRenameModal }: IProps) => {
showRenameModal(record); showRenameModal(record);
}; };
const onShowConnectToKnowledgeModal = () => {
showConnectToKnowledgeModal([documentId]);
};
return ( return (
<Space size={0}> <Space size={0}>
<Button type="text" className={styles.iconButton}> <Button
type="text"
className={styles.iconButton}
onClick={onShowConnectToKnowledgeModal}
>
<ToolOutlined size={20} /> <ToolOutlined size={20} />
</Button> </Button>

View File

@ -0,0 +1,58 @@
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { IModalProps } from '@/interfaces/common';
import { Form, Modal, Select, SelectProps } from 'antd';
const ConnectToKnowledgeModal = ({
visible,
hideModal,
onOk,
}: IModalProps<string[]>) => {
const [form] = Form.useForm();
const { list } = useFetchKnowledgeList();
const options: SelectProps['options'] = list?.map((item) => ({
label: item.name,
value: item.id,
}));
const handleOk = async () => {
const values = await form.getFieldsValue();
const knowledgeIds = values.knowledgeIds ?? [];
if (knowledgeIds.length > 0) {
return onOk?.(knowledgeIds);
}
};
return (
<Modal
title="Add to Knowledge Base"
open={visible}
onOk={handleOk}
onCancel={hideModal}
>
<Form form={form}>
<Form.Item
name="knowledgeIds"
noStyle
rules={[
{
required: true,
message: 'Please select your favourite colors!',
type: 'array',
},
]}
>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
placeholder="Please select"
options={options}
/>
</Form.Item>
</Form>
</Modal>
);
};
export default ConnectToKnowledgeModal;

View File

@ -32,6 +32,7 @@ import styles from './index.less';
interface IProps { interface IProps {
selectedRowKeys: string[]; selectedRowKeys: string[];
showFolderCreateModal: () => void; showFolderCreateModal: () => void;
showFileUploadModal: () => void;
} }
const itemRender: BreadcrumbProps['itemRender'] = ( const itemRender: BreadcrumbProps['itemRender'] = (
@ -48,7 +49,11 @@ const itemRender: BreadcrumbProps['itemRender'] = (
); );
}; };
const FileToolbar = ({ selectedRowKeys, showFolderCreateModal }: IProps) => { const FileToolbar = ({
selectedRowKeys,
showFolderCreateModal,
showFileUploadModal,
}: 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);
@ -59,6 +64,7 @@ const FileToolbar = ({ selectedRowKeys, showFolderCreateModal }: IProps) => {
return [ return [
{ {
key: '1', key: '1',
onClick: showFileUploadModal,
label: ( label: (
<div> <div>
<Button type="link"> <Button type="link">
@ -85,7 +91,7 @@ const FileToolbar = ({ selectedRowKeys, showFolderCreateModal }: IProps) => {
// disabled: true, // disabled: true,
}, },
]; ];
}, [t, showFolderCreateModal]); }, [t, showFolderCreateModal, showFileUploadModal]);
const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys); const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys);

View File

@ -1,61 +1,124 @@
import { IModalProps } from '@/interfaces/common';
import { InboxOutlined } from '@ant-design/icons'; import { InboxOutlined } from '@ant-design/icons';
import { Modal, Segmented, Upload, UploadProps, message } from 'antd'; import {
import { useState } from 'react'; Flex,
Modal,
Segmented,
Tabs,
TabsProps,
Upload,
UploadFile,
UploadProps,
} from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import { useHandleUploadFile } from '../hooks';
const { Dragger } = Upload; const { Dragger } = Upload;
const FileUploadModal = () => { const FileUpload = ({
const [isModalOpen, setIsModalOpen] = useState(false); directory,
fileList,
setFileList,
}: {
directory: boolean;
fileList: UploadFile[];
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
}) => {
const props: UploadProps = { const props: UploadProps = {
name: 'file',
multiple: true, multiple: true,
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload', onRemove: (file) => {
onChange(info) { const index = fileList.indexOf(file);
const { status } = info.file; const newFileList = fileList.slice();
if (status !== 'uploading') { newFileList.splice(index, 1);
console.log(info.file, info.fileList); setFileList(newFileList);
}
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) { beforeUpload: (file) => {
console.log('Dropped files', e.dataTransfer.files); setFileList((pre) => {
return [...pre, file];
});
return false;
}, },
directory,
fileList,
}; };
const handleOk = () => { return (
setIsModalOpen(false); <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>
);
};
const FileUploadModal = ({ visible, hideModal }: IModalProps<any>) => {
const [value, setValue] = useState<string | number>('local');
const { onFileUploadOk, fileUploadLoading } = useHandleUploadFile();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);
const onOk = () => {
onFileUploadOk([...fileList, ...directoryFileList]);
}; };
const handleCancel = () => { const items: TabsProps['items'] = [
setIsModalOpen(false); {
}; key: '1',
label: 'File',
children: (
<FileUpload
directory={false}
fileList={fileList}
setFileList={setFileList}
></FileUpload>
),
},
{
key: '2',
label: 'Directory',
children: (
<FileUpload
directory
fileList={directoryFileList}
setFileList={setDirectoryFileList}
></FileUpload>
),
},
];
return ( return (
<> <>
<Modal <Modal
title="File upload" title="File upload"
open={isModalOpen} open={visible}
onOk={handleOk} onOk={onOk}
onCancel={handleCancel} onCancel={hideModal}
confirmLoading={fileUploadLoading}
> >
<Segmented options={['Local uploads', 'S3 uploads']} block /> <Flex gap={'large'} vertical>
<Dragger {...props}> <Segmented
<p className="ant-upload-drag-icon"> options={[
<InboxOutlined /> { label: 'Local uploads', value: 'local' },
</p> { label: 'S3 uploads', value: 's3' },
<p className="ant-upload-text"> ]}
Click or drag file to this area to upload block
</p> value={value}
<p className="ant-upload-hint"> onChange={setValue}
Support for a single or bulk upload. Strictly prohibited from />
uploading company data or other banned files. {value === 'local' ? (
</p> <Tabs defaultActiveKey="1" items={items} />
</Dragger> ) : (
'coming soon'
)}
</Flex>
</Modal> </Modal>
</> </>
); );

View File

@ -4,6 +4,7 @@ import {
useTranslate, useTranslate,
} from '@/hooks/commonHooks'; } from '@/hooks/commonHooks';
import { import {
useConnectToKnowledge,
useCreateFolder, useCreateFolder,
useFetchFileList, useFetchFileList,
useFetchParentFolderList, useFetchParentFolderList,
@ -11,11 +12,14 @@ import {
useRenameFile, useRenameFile,
useSelectFileList, useSelectFileList,
useSelectParentFolderList, useSelectParentFolderList,
useUploadFile,
} from '@/hooks/fileManagerHooks'; } from '@/hooks/fileManagerHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { Pagination } from '@/interfaces/common'; import { Pagination } from '@/interfaces/common';
import { IFile } from '@/interfaces/database/file-manager'; import { IFile } from '@/interfaces/database/file-manager';
import { getFilePathByWebkitRelativePath } from '@/utils/fileUtil';
import { PaginationProps } from 'antd'; import { PaginationProps } from 'antd';
import { UploadFile } from 'antd/lib';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useNavigate, useSearchParams, useSelector } from 'umi'; import { useDispatch, useNavigate, useSearchParams, useSelector } from 'umi';
@ -250,3 +254,87 @@ export const useHandleDeleteFile = (fileIds: string[]) => {
export const useSelectFileListLoading = () => { export const useSelectFileListLoading = () => {
return useOneNamespaceEffectsLoading('fileManager', ['listFile']); return useOneNamespaceEffectsLoading('fileManager', ['listFile']);
}; };
export const useHandleUploadFile = () => {
const {
visible: fileUploadVisible,
hideModal: hideFileUploadModal,
showModal: showFileUploadModal,
} = useSetModalState();
const uploadFile = useUploadFile();
const id = useGetFolderId();
const onFileUploadOk = useCallback(
async (fileList: UploadFile[]) => {
console.info('fileList', fileList);
if (fileList.length > 0) {
const ret = await uploadFile(
fileList[0],
id,
getFilePathByWebkitRelativePath(fileList[0] as any),
);
if (ret === 0) {
hideFileUploadModal();
}
}
},
[uploadFile, hideFileUploadModal, id],
);
const loading = useOneNamespaceEffectsLoading('fileManager', ['uploadFile']);
return {
fileUploadLoading: loading,
onFileUploadOk,
fileUploadVisible,
hideFileUploadModal,
showFileUploadModal,
};
};
export const useHandleConnectToKnowledge = () => {
const {
visible: connectToKnowledgeVisible,
hideModal: hideConnectToKnowledgeModal,
showModal: showConnectToKnowledgeModal,
} = useSetModalState();
const connectToKnowledge = useConnectToKnowledge();
const id = useGetFolderId();
const [fileIds, setFileIds] = useState<string[]>([]);
const onConnectToKnowledgeOk = useCallback(
async (knowledgeIds: string[]) => {
const ret = await connectToKnowledge({
parentId: id,
fileIds,
kbIds: knowledgeIds,
});
if (ret === 0) {
hideConnectToKnowledgeModal();
}
},
[connectToKnowledge, hideConnectToKnowledgeModal, id, fileIds],
);
const loading = useOneNamespaceEffectsLoading('fileManager', [
'connectFileToKnowledge',
]);
const handleShowConnectToKnowledgeModal = useCallback(
(ids: string[]) => {
setFileIds(ids);
showConnectToKnowledgeModal();
},
[showConnectToKnowledgeModal],
);
return {
connectToKnowledgeLoading: loading,
onConnectToKnowledgeOk,
connectToKnowledgeVisible,
hideConnectToKnowledgeModal,
showConnectToKnowledgeModal: handleShowConnectToKnowledgeModal,
};
};

View File

@ -7,13 +7,17 @@ import ActionCell from './action-cell';
import FileToolbar from './file-toolbar'; import FileToolbar from './file-toolbar';
import { import {
useGetRowSelection, useGetRowSelection,
useHandleConnectToKnowledge,
useHandleCreateFolder, useHandleCreateFolder,
useHandleUploadFile,
useNavigateToOtherFolder, useNavigateToOtherFolder,
useRenameCurrentFile, useRenameCurrentFile,
useSelectFileListLoading, useSelectFileListLoading,
} from './hooks'; } from './hooks';
import RenameModal from '@/components/rename-modal'; import RenameModal from '@/components/rename-modal';
import ConnectToKnowledgeModal from './connect-to-knowledge-modal';
import FileUploadModal from './file-upload-modal';
import FolderCreateModal from './folder-create-modal'; import FolderCreateModal from './folder-create-modal';
import styles from './index.less'; import styles from './index.less';
@ -37,6 +41,14 @@ const FileManager = () => {
folderCreateLoading, folderCreateLoading,
onFolderCreateOk, onFolderCreateOk,
} = useHandleCreateFolder(); } = useHandleCreateFolder();
const { fileUploadVisible, hideFileUploadModal, showFileUploadModal } =
useHandleUploadFile();
const {
connectToKnowledgeVisible,
hideConnectToKnowledgeModal,
showConnectToKnowledgeModal,
onConnectToKnowledgeOk,
} = useHandleConnectToKnowledge();
const columns: ColumnsType<IFile> = [ const columns: ColumnsType<IFile> = [
{ {
@ -64,6 +76,17 @@ const FileManager = () => {
return formatDate(text); return formatDate(text);
}, },
}, },
{
title: 'kbs_info',
dataIndex: 'kbs_info',
key: 'kbs_info',
render(value) {
console.info(value);
return Array.isArray(value)
? value?.map((x) => x.kb_name).join(',')
: '';
},
},
{ {
title: 'Location', title: 'Location',
dataIndex: 'location', dataIndex: 'location',
@ -80,6 +103,7 @@ const FileManager = () => {
console.info(record); console.info(record);
}} }}
showRenameModal={showFileRenameModal} showRenameModal={showFileRenameModal}
showConnectToKnowledgeModal={showConnectToKnowledgeModal}
></ActionCell> ></ActionCell>
), ),
}, },
@ -90,6 +114,7 @@ const FileManager = () => {
<FileToolbar <FileToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]} selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showFolderCreateModal={showFolderCreateModal} showFolderCreateModal={showFolderCreateModal}
showFileUploadModal={showFileUploadModal}
></FileToolbar> ></FileToolbar>
<Table <Table
dataSource={fileList} dataSource={fileList}
@ -111,6 +136,15 @@ const FileManager = () => {
hideModal={hideFolderCreateModal} hideModal={hideFolderCreateModal}
onOk={onFolderCreateOk} onOk={onFolderCreateOk}
></FolderCreateModal> ></FolderCreateModal>
<FileUploadModal
visible={fileUploadVisible}
hideModal={hideFileUploadModal}
></FileUploadModal>
<ConnectToKnowledgeModal
visible={connectToKnowledgeVisible}
hideModal={hideConnectToKnowledgeModal}
onOk={onConnectToKnowledgeOk}
></ConnectToKnowledgeModal>
</section> </section>
); );
}; };

View File

@ -56,6 +56,20 @@ const model: DvaModel<FileManagerModelState> = {
} }
return data.retcode; return data.retcode;
}, },
*uploadFile({ payload = {} }, { call, put }) {
const formData = new FormData();
formData.append('parent_id', payload.parentId);
formData.append('file', payload.file);
formData.append('path', payload.path);
const { data } = yield call(fileManagerService.uploadFile, formData);
if (data.retcode === 0) {
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
});
}
return data.retcode;
},
*createFolder({ payload = {} }, { call, put }) { *createFolder({ payload = {} }, { call, put }) {
const { data } = yield call(fileManagerService.createFolder, payload); const { data } = yield call(fileManagerService.createFolder, payload);
if (data.retcode === 0) { if (data.retcode === 0) {
@ -79,6 +93,19 @@ const model: DvaModel<FileManagerModelState> = {
} }
return data.retcode; return data.retcode;
}, },
*connectFileToKnowledge({ payload = {} }, { call, put }) {
const { data } = yield call(
fileManagerService.connectFileToKnowledge,
omit(payload, 'parentId'),
);
if (data.retcode === 0) {
yield put({
type: 'listFile',
payload: { parentId: payload.parentId },
});
}
return data.retcode;
},
}, },
}; };
export default model; export default model;

View File

@ -228,7 +228,7 @@ const UserSettingModel = () => {
<section className={styles.modelWrapper}> <section className={styles.modelWrapper}>
<SettingTitle <SettingTitle
title={t('model')} title={t('model')}
description={t('profileDescription')} description={t('modelDescription')}
showRightButton showRightButton
clickButton={showSystemSettingModal} clickButton={showSystemSettingModal}
></SettingTitle> ></SettingTitle>

View File

@ -9,6 +9,7 @@ const {
renameFile, renameFile,
getAllParentFolder, getAllParentFolder,
createFolder, createFolder,
connectFileToKnowledge,
} = api; } = api;
const methods = { const methods = {
@ -36,6 +37,10 @@ const methods = {
url: createFolder, url: createFolder,
method: 'post', method: 'post',
}, },
connectFileToKnowledge: {
url: connectFileToKnowledge,
method: 'post',
},
} as const; } as const;
const fileManagerService = registerServer<keyof typeof methods>( const fileManagerService = registerServer<keyof typeof methods>(

View File

@ -74,4 +74,5 @@ export default {
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`, createFolder: `${api_host}/file/create`,
connectFileToKnowledge: `${api_host}/file2document/convert`,
}; };

View File

@ -85,3 +85,12 @@ export const downloadFile = ({
downloadElement.click(); downloadElement.click();
document.body.removeChild(downloadElement); document.body.removeChild(downloadElement);
}; };
export const getFilePathByWebkitRelativePath = (file: File) => {
const path = file.webkitRelativePath;
return path;
// if (path !== '') {
// return path.slice(0, path.lastIndexOf('/'));
// }
// return path;
};