feat: add batch operations for document list (#302)

### What problem does this PR solve?

document list needs to be batch operated


Issue link: #301

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2024-04-10 17:17:41 +08:00
committed by GitHub
parent 1ff5d9d55b
commit 533ac3b534
16 changed files with 341 additions and 143 deletions

View File

@ -219,7 +219,7 @@ const KnowledgeUploadFile = () => {
const runSelectedDocument = () => {
const ids = fileListRef.current.map((x) => x.response.id);
runDocumentByIds(ids);
runDocumentByIds({ doc_ids: ids, run: 1 });
};
const handleNextClick = () => {

View File

@ -0,0 +1,222 @@
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
import { ReactComponent as DeleteIcon } from '@/assets/svg/delete.svg';
import { ReactComponent as DisableIcon } from '@/assets/svg/disable.svg';
import { ReactComponent as EnableIcon } from '@/assets/svg/enable.svg';
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
import { useShowDeleteConfirm, useTranslate } from '@/hooks/commonHooks';
import {
useRemoveDocument,
useRunDocument,
useSetDocumentStatus,
} from '@/hooks/documentHooks';
import { useGetKnowledgeSearchParams } from '@/hooks/routeHook';
import {
DownOutlined,
FileOutlined,
FileTextOutlined,
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, Flex, Input, MenuProps, Space } from 'antd';
import { useCallback, useMemo } from 'react';
import {
useFetchDocumentListOnMount,
useGetPagination,
useHandleSearchChange,
useNavigateToOtherPage,
} from './hooks';
import styles from './index.less';
interface IProps {
selectedRowKeys: string[];
showCreateModal(): void;
}
const DocumentToolbar = ({ selectedRowKeys, showCreateModal }: IProps) => {
const { t } = useTranslate('knowledgeDetails');
const { fetchDocumentList } = useFetchDocumentListOnMount();
const { setPagination, searchString } = useGetPagination(fetchDocumentList);
const { handleInputChange } = useHandleSearchChange(setPagination);
const removeDocument = useRemoveDocument();
const showDeleteConfirm = useShowDeleteConfirm();
const { linkToUploadPage } = useNavigateToOtherPage();
const runDocumentByIds = useRunDocument();
const { knowledgeId } = useGetKnowledgeSearchParams();
const changeStatus = useSetDocumentStatus();
const actionItems: MenuProps['items'] = useMemo(() => {
return [
{
key: '1',
onClick: linkToUploadPage,
label: (
<div>
<Button type="link">
<Space>
<FileTextOutlined />
{t('localFiles')}
</Space>
</Button>
</div>
),
},
{ type: 'divider' },
{
key: '2',
onClick: showCreateModal,
label: (
<div>
<Button type="link">
<FileOutlined />
{t('emptyFiles')}
</Button>
</div>
),
// disabled: true,
},
];
}, [linkToUploadPage, showCreateModal, t]);
const handleDelete = useCallback(() => {
showDeleteConfirm({
onOk: () => {
selectedRowKeys.forEach((id) => {
removeDocument(id);
});
},
});
}, [removeDocument, showDeleteConfirm, selectedRowKeys]);
const runDocument = useCallback(
(run: number) => {
runDocumentByIds({
doc_ids: selectedRowKeys,
run,
knowledgeBaseId: knowledgeId,
});
},
[runDocumentByIds, selectedRowKeys, knowledgeId],
);
const handleRunClick = useCallback(() => {
runDocument(1);
}, [runDocument]);
const handleCancelClick = useCallback(() => {
runDocument(2);
}, [runDocument]);
const onChangeStatus = useCallback(
(enabled: boolean) => {
selectedRowKeys.forEach((id) => {
changeStatus(enabled, id);
});
},
[selectedRowKeys, changeStatus],
);
const handleEnableClick = useCallback(() => {
onChangeStatus(true);
}, [onChangeStatus]);
const handleDisableClick = useCallback(() => {
onChangeStatus(false);
}, [onChangeStatus]);
const disabled = selectedRowKeys.length === 0;
const items: MenuProps['items'] = useMemo(() => {
return [
{
key: '0',
onClick: handleEnableClick,
label: (
<Flex gap={10}>
<EnableIcon></EnableIcon>
<b>{t('enabled')}</b>
</Flex>
),
},
{
key: '1',
onClick: handleDisableClick,
label: (
<Flex gap={10}>
<DisableIcon></DisableIcon>
<b>{t('disabled')}</b>
</Flex>
),
},
{ type: 'divider' },
{
key: '2',
onClick: handleRunClick,
label: (
<Flex gap={10}>
<RunIcon></RunIcon>
<b>{t('run')}</b>
</Flex>
),
},
{
key: '3',
onClick: handleCancelClick,
label: (
<Flex gap={10}>
<CancelIcon />
<b>{t('cancel')}</b>
</Flex>
),
},
{ type: 'divider' },
{
key: '4',
onClick: handleDelete,
label: (
<Flex gap={10}>
<span className={styles.deleteIconWrapper}>
<DeleteIcon width={18} />
</span>
<b>{t('delete', { keyPrefix: 'common' })}</b>
</Flex>
),
},
];
}, [handleDelete, handleRunClick, handleCancelClick, t]);
return (
<div className={styles.filter}>
<Dropdown
menu={{ items }}
placement="bottom"
arrow={false}
disabled={disabled}
>
<Button>
<Space>
<b> {t('bulk')}</b>
<DownOutlined />
</Space>
</Button>
</Dropdown>
<Space>
<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 DocumentToolbar;

View File

@ -1,4 +1,4 @@
import { useSetModalState } from '@/hooks/commonHooks';
import { useSetModalState, useTranslate } from '@/hooks/commonHooks';
import {
useCreateDocument,
useFetchDocumentList,
@ -11,7 +11,7 @@ import { useFetchTenantInfo } from '@/hooks/userSettingHook';
import { Pagination } from '@/interfaces/common';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import { PaginationProps } from 'antd';
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi';
import { KnowledgeRouteKey } from './constant';
@ -43,6 +43,7 @@ export const useFetchDocumentListOnMount = () => {
export const useGetPagination = (fetchDocumentList: () => void) => {
const dispatch = useDispatch();
const kFModel = useSelector((state: any) => state.kFModel);
const { t } = useTranslate('common');
const setPagination = useCallback(
(pageNumber = 1, pageSize?: number) => {
@ -77,8 +78,9 @@ export const useGetPagination = (fetchDocumentList: () => void) => {
pageSize: kFModel.pagination.pageSize,
pageSizeOptions: [1, 2, 10, 20, 50, 100],
onChange: onPageChange,
showTotal: (total) => `${t('total')} ${total}`,
};
}, [kFModel, onPageChange]);
}, [kFModel, onPageChange, t]);
return {
pagination,
@ -227,3 +229,16 @@ export const useChangeDocumentParser = (documentId: string) => {
showChangeParserModal,
};
};
export const useGetRowSelection = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const rowSelection = {
selectedRowKeys,
onChange: (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
},
};
return rowSelection;
};

View File

@ -8,15 +8,13 @@
display: flex;
margin: 10px 0;
justify-content: space-between;
padding: 24px 20px;
padding: 24px 0;
align-items: center;
}
// .search {
// flex: 1;
// }
// .operate {
// width: 200px;
// }
.deleteIconWrapper {
width: 22px;
text-align: center;
}
.img {

View File

@ -4,36 +4,21 @@ import {
useSelectDocumentList,
useSetDocumentStatus,
} from '@/hooks/documentHooks';
import { useSetSelectedRecord } from '@/hooks/logicHooks';
import { useSelectParserList } from '@/hooks/userSettingHook';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { getExtension } from '@/utils/documentUtils';
import {
FileOutlined,
FileTextOutlined,
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import {
Button,
Divider,
Dropdown,
Flex,
Input,
Space,
Switch,
Table,
Tag,
} from 'antd';
import { Divider, Flex, Switch, Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import CreateFileModal from './create-file-modal';
import DocumentToolbar from './document-toolbar';
import {
useChangeDocumentParser,
useCreateEmptyDocument,
useFetchDocumentListOnMount,
useGetPagination,
useHandleSearchChange,
useGetRowSelection,
useNavigateToOtherPage,
useRenameDocument,
} from './hooks';
@ -41,20 +26,15 @@ import ParsingActionCell from './parsing-action-cell';
import ParsingStatusCell from './parsing-status-cell';
import RenameModal from './rename-modal';
import { useSetSelectedRecord } from '@/hooks/logicHooks';
import { useTranslation } from 'react-i18next';
import styles from './index.less';
const KnowledgeFile = () => {
const data = useSelectDocumentList();
const { fetchDocumentList } = useFetchDocumentListOnMount();
const parserList = useSelectParserList();
const { pagination, setPagination, total, searchString } =
useGetPagination(fetchDocumentList);
const { pagination } = useGetPagination(fetchDocumentList);
const onChangeStatus = useSetDocumentStatus();
const { linkToUploadPage, toChunk } = useNavigateToOtherPage();
const { handleInputChange } = useHandleSearchChange(setPagination);
const { toChunk } = useNavigateToOtherPage();
const { currentRecord, setRecord } = useSetSelectedRecord();
const {
renameLoading,
@ -81,38 +61,7 @@ const KnowledgeFile = () => {
keyPrefix: 'knowledgeDetails',
});
const actionItems: MenuProps['items'] = useMemo(() => {
return [
{
key: '1',
onClick: linkToUploadPage,
label: (
<div>
<Button type="link">
<Space>
<FileTextOutlined />
{t('localFiles')}
</Space>
</Button>
</div>
),
},
{ type: 'divider' },
{
key: '2',
onClick: showCreateModal,
label: (
<div>
<Button type="link">
<FileOutlined />
{t('emptyFiles')}
</Button>
</div>
),
// disabled: true,
},
];
}, [linkToUploadPage, showCreateModal, t]);
const rowSelection = useGetRowSelection();
const columns: ColumnsType<IKnowledgeFile> = [
{
@ -161,7 +110,7 @@ const KnowledgeFile = () => {
render: (_, { status, id }) => (
<>
<Switch
defaultChecked={status === '1'}
checked={status === '1'}
onChange={(e) => {
onChangeStatus(e, id);
}}
@ -201,36 +150,17 @@ const KnowledgeFile = () => {
<h3>{t('dataset')}</h3>
<p>{t('datasetDescription')}</p>
<Divider></Divider>
<div className={styles.filter}>
<Space>
<h3>{t('total', { keyPrefix: 'common' })}</h3>
<Tag color="purple">
{total} {t('files')}
</Tag>
</Space>
<Space>
<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>
<DocumentToolbar
selectedRowKeys={rowSelection.selectedRowKeys as string[]}
showCreateModal={showCreateModal}
></DocumentToolbar>
<Table
rowKey="id"
columns={finalColumns}
dataSource={data}
// loading={loading}
pagination={pagination}
rowSelection={rowSelection}
scroll={{ scrollToFirstRowOnChange: true, x: 1300, y: 'fill' }}
/>
<CreateFileModal

View File

@ -30,12 +30,12 @@ const ParsingActionCell = ({
const documentId = record.id;
const isRunning = isParserRunning(record.run);
const { t } = useTranslate('knowledgeDetails');
const removeDocument = useRemoveDocument(documentId);
const removeDocument = useRemoveDocument();
const showDeleteConfirm = useShowDeleteConfirm();
const onRmDocument = () => {
if (!isRunning) {
showDeleteConfirm({ onOk: removeDocument });
showDeleteConfirm({ onOk: () => removeDocument(documentId) });
}
};

View File

@ -18,8 +18,7 @@
.operationIcon {
text-align: center;
margin-right: 20%;
width: 20px;
display: flex;
&:hover {
cursor: pointer;
}

View File

@ -1,19 +1,19 @@
import { ReactComponent as CancelIcon } from '@/assets/svg/cancel.svg';
import { ReactComponent as RefreshIcon } from '@/assets/svg/refresh.svg';
import { ReactComponent as RunIcon } from '@/assets/svg/run.svg';
import { useTranslate } from '@/hooks/commonHooks';
import { useRunDocument } from '@/hooks/documentHooks';
import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { CloseCircleOutlined } from '@ant-design/icons';
import { Badge, DescriptionsProps, Flex, Popover, Space, Tag } from 'antd';
import { useTranslation } from 'react-i18next';
import reactStringReplace from 'react-string-replace';
import { useDispatch } from 'umi';
import { RunningStatus, RunningStatusMap } from '../constant';
import { isParserRunning } from '../utils';
import styles from './index.less';
const iconMap = {
[RunningStatus.UNSTART]: RunIcon,
[RunningStatus.RUNNING]: CloseCircleOutlined,
[RunningStatus.RUNNING]: CancelIcon,
[RunningStatus.CANCEL]: RefreshIcon,
[RunningStatus.DONE]: RefreshIcon,
[RunningStatus.FAIL]: RefreshIcon,
@ -78,10 +78,10 @@ const PopoverContent = ({ record }: IProps) => {
};
export const ParsingStatusCell = ({ record }: IProps) => {
const dispatch = useDispatch();
const text = record.run;
const runningStatus = RunningStatusMap[text];
const { t } = useTranslation();
const runDocumentByIds = useRunDocument();
const isRunning = isParserRunning(text);
@ -90,18 +90,15 @@ export const ParsingStatusCell = ({ record }: IProps) => {
const label = t(`knowledgeDetails.runningStatus${text}`);
const handleOperationIconClick = () => {
dispatch({
type: 'kFModel/document_run',
payload: {
doc_ids: [record.id],
run: isRunning ? 2 : 1,
knowledgeBaseId: record.kb_id,
},
runDocumentByIds({
doc_ids: [record.id],
run: isRunning ? 2 : 1,
knowledgeBaseId: record.kb_id,
});
};
return (
<Flex justify={'space-between'}>
<Flex justify={'space-between'} align="center">
<Popover content={<PopoverContent record={record}></PopoverContent>}>
<Tag color={runningStatus.color}>
{isRunning ? (