Przeglądaj źródła

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)
tags/v0.3.1
balibabu 1 rok temu
rodzic
commit
51e7697df7
No account linked to committer's email address

+ 49
- 1
web/src/hooks/fileManagerHooks.ts Wyświetl plik

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


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

+ 1
- 1
web/src/interfaces/database/file-manager.ts Wyświetl plik

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;

+ 9
- 0
web/src/interfaces/request/file-manager.ts Wyświetl plik

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[];
}

+ 1
- 1
web/src/locales/en.ts Wyświetl plik

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',

+ 1
- 1
web/src/locales/zh-traditional.ts Wyświetl plik

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

+ 1
- 1
web/src/locales/zh.ts Wyświetl plik

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

+ 16
- 2
web/src/pages/file-manager/action-cell/index.tsx Wyświetl plik

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');
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>



+ 58
- 0
web/src/pages/file-manager/connect-to-knowledge-modal/index.tsx Wyświetl plik

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;

+ 8
- 2
web/src/pages/file-manager/file-toolbar.tsx Wyświetl plik

interface IProps { interface IProps {
selectedRowKeys: string[]; selectedRowKeys: string[];
showFolderCreateModal: () => void; showFolderCreateModal: () => void;
showFileUploadModal: () => void;
} }


const itemRender: BreadcrumbProps['itemRender'] = ( 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);
return [ return [
{ {
key: '1', key: '1',
onClick: showFileUploadModal,
label: ( label: (
<div> <div>
<Button type="link"> <Button type="link">
// disabled: true, // disabled: true,
}, },
]; ];
}, [t, showFolderCreateModal]);
}, [t, showFolderCreateModal, showFileUploadModal]);


const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys); const { handleRemoveFile } = useHandleDeleteFile(selectedRowKeys);



+ 103
- 40
web/src/pages/file-manager/file-upload-modal/index.tsx Wyświetl plik

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 { useState } from 'react';
import {
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 [isModalOpen, setIsModalOpen] = useState(false);

const FileUpload = ({
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',
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.`);
}
onRemove: (file) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
}, },
onDrop(e) {
console.log('Dropped files', e.dataTransfer.files);
beforeUpload: (file) => {
setFileList((pre) => {
return [...pre, file];
});

return false;
}, },
directory,
fileList,
}; };


const handleOk = () => {
setIsModalOpen(false);
};
return (
<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 handleCancel = () => {
setIsModalOpen(false);
const onOk = () => {
onFileUploadOk([...fileList, ...directoryFileList]);
}; };


const items: TabsProps['items'] = [
{
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}
onOk={handleOk}
onCancel={handleCancel}
open={visible}
onOk={onOk}
onCancel={hideModal}
confirmLoading={fileUploadLoading}
> >
<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>
<Flex gap={'large'} vertical>
<Segmented
options={[
{ label: 'Local uploads', value: 'local' },
{ label: 'S3 uploads', value: 's3' },
]}
block
value={value}
onChange={setValue}
/>
{value === 'local' ? (
<Tabs defaultActiveKey="1" items={items} />
) : (
'coming soon'
)}
</Flex>
</Modal> </Modal>
</> </>
); );

+ 88
- 0
web/src/pages/file-manager/hooks.ts Wyświetl plik

useTranslate, useTranslate,
} from '@/hooks/commonHooks'; } from '@/hooks/commonHooks';
import { import {
useConnectToKnowledge,
useCreateFolder, useCreateFolder,
useFetchFileList, useFetchFileList,
useFetchParentFolderList, useFetchParentFolderList,
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';


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

+ 34
- 0
web/src/pages/file-manager/index.tsx Wyświetl plik

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


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> = [
{ {
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',
console.info(record); console.info(record);
}} }}
showRenameModal={showFileRenameModal} showRenameModal={showFileRenameModal}
showConnectToKnowledgeModal={showConnectToKnowledgeModal}
></ActionCell> ></ActionCell>
), ),
}, },
<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}
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>
); );
}; };

+ 27
- 0
web/src/pages/file-manager/model.ts Wyświetl plik

} }
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) {
} }
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;

+ 1
- 1
web/src/pages/user-setting/setting-model/index.tsx Wyświetl plik

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

+ 5
- 0
web/src/services/fileManagerService.ts Wyświetl plik

renameFile, renameFile,
getAllParentFolder, getAllParentFolder,
createFolder, createFolder,
connectFileToKnowledge,
} = api; } = api;


const methods = { 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>(

+ 1
- 0
web/src/utils/api.ts Wyświetl plik

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

+ 9
- 0
web/src/utils/fileUtil.ts Wyświetl plik

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

Ładowanie…
Anuluj
Zapisz