mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-21 13:32:49 +08:00
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:
@ -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 = () => {
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -18,8 +18,7 @@
|
||||
|
||||
.operationIcon {
|
||||
text-align: center;
|
||||
margin-right: 20%;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user