mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-26 08:56:47 +08:00
### What problem does this PR solve? Feat: Batch operations on documents in a dataset #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -24,6 +24,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { UseRowSelectionType } from '@/hooks/logic-hooks/use-row-selection';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { getExtension } from '@/utils/document-util';
|
||||
import { useMemo } from 'react';
|
||||
@ -36,12 +37,15 @@ import { useSaveMeta } from './use-save-meta';
|
||||
export type DatasetTableProps = Pick<
|
||||
ReturnType<typeof useFetchDocumentList>,
|
||||
'documents' | 'setPagination' | 'pagination'
|
||||
>;
|
||||
> &
|
||||
Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'>;
|
||||
|
||||
export function DatasetTable({
|
||||
documents,
|
||||
pagination,
|
||||
setPagination,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
}: DatasetTableProps) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
@ -49,7 +53,6 @@ export function DatasetTable({
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
|
||||
const {
|
||||
changeParserLoading,
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
|
||||
import { useFetchDocumentList } from '@/hooks/use-document-request';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -28,7 +29,7 @@ export default function Dataset() {
|
||||
onDocumentUploadOk,
|
||||
documentUploadLoading,
|
||||
} = useHandleUploadDocument();
|
||||
const { list } = useBulkOperateDataset();
|
||||
|
||||
const {
|
||||
searchString,
|
||||
documents,
|
||||
@ -48,6 +49,15 @@ export default function Dataset() {
|
||||
showCreateModal,
|
||||
} = useCreateEmptyDocument();
|
||||
|
||||
const { rowSelection, rowSelectionIsEmpty, setRowSelection } =
|
||||
useRowSelection();
|
||||
|
||||
const { list } = useBulkOperateDataset({
|
||||
documents,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="p-8">
|
||||
<ListFilterBar
|
||||
@ -76,11 +86,13 @@ export default function Dataset() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ListFilterBar>
|
||||
<BulkOperateBar list={list}></BulkOperateBar>
|
||||
{rowSelectionIsEmpty || <BulkOperateBar list={list}></BulkOperateBar>}
|
||||
<DatasetTable
|
||||
documents={documents}
|
||||
pagination={pagination}
|
||||
setPagination={setPagination}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
></DatasetTable>
|
||||
{documentUploadVisible && (
|
||||
<FileUploadDialog
|
||||
|
||||
@ -1,39 +1,131 @@
|
||||
import {
|
||||
UseRowSelectionType,
|
||||
useSelectedIds,
|
||||
} from '@/hooks/logic-hooks/use-row-selection';
|
||||
import {
|
||||
useRemoveDocument,
|
||||
useRunDocument,
|
||||
useSetDocumentStatus,
|
||||
} from '@/hooks/use-document-request';
|
||||
import { IDocumentInfo } from '@/interfaces/database/document';
|
||||
import { Ban, CircleCheck, CircleX, Play, Trash2 } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { DocumentType, RunningStatus } from './constant';
|
||||
|
||||
export function useBulkOperateDataset() {
|
||||
export function useBulkOperateDataset({
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
documents,
|
||||
}: Pick<UseRowSelectionType, 'rowSelection' | 'setRowSelection'> & {
|
||||
documents: IDocumentInfo[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { selectedIds: selectedRowKeys } = useSelectedIds(
|
||||
rowSelection,
|
||||
documents,
|
||||
);
|
||||
|
||||
const { runDocumentByIds } = useRunDocument();
|
||||
const { setDocumentStatus } = useSetDocumentStatus();
|
||||
const { removeDocument } = useRemoveDocument();
|
||||
|
||||
const runDocument = useCallback(
|
||||
(run: number) => {
|
||||
const nonVirtualKeys = selectedRowKeys.filter(
|
||||
(x) =>
|
||||
!documents.some((y) => x === y.id && y.type === DocumentType.Virtual),
|
||||
);
|
||||
|
||||
if (nonVirtualKeys.length === 0) {
|
||||
toast.error(t('Please select a non-empty file list'));
|
||||
return;
|
||||
}
|
||||
runDocumentByIds({
|
||||
documentIds: nonVirtualKeys,
|
||||
run,
|
||||
shouldDelete: false,
|
||||
});
|
||||
},
|
||||
[documents, runDocumentByIds, selectedRowKeys, t],
|
||||
);
|
||||
|
||||
const handleRunClick = useCallback(() => {
|
||||
runDocument(1);
|
||||
}, [runDocument]);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
runDocument(2);
|
||||
}, [runDocument]);
|
||||
|
||||
const onChangeStatus = useCallback(
|
||||
(enabled: boolean) => {
|
||||
selectedRowKeys.forEach((id) => {
|
||||
setDocumentStatus({ status: enabled, documentId: id });
|
||||
});
|
||||
},
|
||||
[selectedRowKeys, setDocumentStatus],
|
||||
);
|
||||
|
||||
const handleEnableClick = useCallback(() => {
|
||||
onChangeStatus(true);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const handleDisableClick = useCallback(() => {
|
||||
onChangeStatus(false);
|
||||
}, [onChangeStatus]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const deletedKeys = selectedRowKeys.filter(
|
||||
(x) =>
|
||||
!documents
|
||||
.filter((y) => y.run === RunningStatus.RUNNING)
|
||||
.some((y) => y.id === x),
|
||||
);
|
||||
if (deletedKeys.length === 0) {
|
||||
toast.error(t('theDocumentBeingParsedCannotBeDeleted'));
|
||||
return;
|
||||
}
|
||||
|
||||
return removeDocument(deletedKeys);
|
||||
}, [selectedRowKeys, removeDocument, documents, t]);
|
||||
|
||||
const list = [
|
||||
{
|
||||
id: 'enabled',
|
||||
label: t('knowledgeDetails.enabled'),
|
||||
icon: <CircleCheck />,
|
||||
onClick: () => {},
|
||||
onClick: handleEnableClick,
|
||||
},
|
||||
{
|
||||
id: 'disabled',
|
||||
label: t('knowledgeDetails.disabled'),
|
||||
icon: <Ban />,
|
||||
onClick: () => {},
|
||||
onClick: handleDisableClick,
|
||||
},
|
||||
{
|
||||
id: 'run',
|
||||
label: t('knowledgeDetails.run'),
|
||||
icon: <Play />,
|
||||
onClick: () => {},
|
||||
onClick: handleRunClick,
|
||||
},
|
||||
{
|
||||
id: 'cancel',
|
||||
label: t('knowledgeDetails.cancel'),
|
||||
icon: <CircleX />,
|
||||
onClick: () => {},
|
||||
onClick: handleCancelClick,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <Trash2 />,
|
||||
onClick: () => {},
|
||||
onClick: async () => {
|
||||
const code = await handleDelete();
|
||||
if (code === 0) {
|
||||
setRowSelection({});
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useSecondPathName } from '@/hooks/route-hook';
|
||||
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { formatDate } from '@/utils/date';
|
||||
import { Banknote, LayoutGrid, User } from 'lucide-react';
|
||||
import { useHandleMenuClick } from './hooks';
|
||||
|
||||
@ -16,15 +18,6 @@ const items = [
|
||||
{ icon: Banknote, label: 'Settings', key: Routes.DatasetSetting },
|
||||
];
|
||||
|
||||
const dataset = {
|
||||
id: 1,
|
||||
title: 'Legal knowledge base',
|
||||
files: '1,242 files',
|
||||
size: '152 MB',
|
||||
created: '12.02.2024',
|
||||
image: 'https://github.com/shadcn.png',
|
||||
};
|
||||
|
||||
export function SideBar() {
|
||||
const pathName = useSecondPathName();
|
||||
const { handleMenuClick } = useHandleMenuClick();
|
||||
@ -33,16 +26,18 @@ export function SideBar() {
|
||||
return (
|
||||
<aside className="w-60 relative border-r ">
|
||||
<div className="p-6 space-y-2 border-b">
|
||||
<div
|
||||
className="w-[70px] h-[70px] rounded-xl bg-cover"
|
||||
style={{ backgroundImage: `url(${dataset.image})` }}
|
||||
/>
|
||||
<Avatar className="size-20 rounded-lg">
|
||||
<AvatarImage src={data.avatar} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<h3 className="text-lg font-semibold mb-2">{data.name}</h3>
|
||||
<div className="text-sm opacity-80">
|
||||
{dataset.files} | {dataset.size}
|
||||
{data.doc_num} files | {data.chunk_num} chunks
|
||||
</div>
|
||||
<div className="text-sm opacity-80">
|
||||
Created {formatDate(data.create_time)}
|
||||
</div>
|
||||
<div className="text-sm opacity-80">Created {dataset.created}</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{items.map((item, itemIdx) => {
|
||||
|
||||
Reference in New Issue
Block a user