Feat: Batch operations on documents in a dataset #3221 (#7352)

### 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:
balibabu
2025-04-27 17:00:41 +08:00
committed by GitHub
parent 43e507d554
commit 6a45d93005
12 changed files with 181 additions and 203 deletions

View File

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

View File

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

View File

@ -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({});
}
},
},
];

View File

@ -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) => {