瀏覽代碼

Feat: Deleting files in batches. #3221 (#7234)

### What problem does this PR solve?
Feat: Deleting files in batches. #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.19.0
balibabu 6 月之前
父節點
當前提交
1366712560
沒有連結到貢獻者的電子郵件帳戶。

+ 47
- 0
web/src/components/bulk-operate-bar.tsx 查看文件

@@ -0,0 +1,47 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { ReactNode, useCallback } from 'react';
import { ConfirmDeleteDialog } from './confirm-delete-dialog';

export type BulkOperateItemType = {
id: string;
label: ReactNode;
icon: ReactNode;
onClick(): void;
};

type BulkOperateBarProps = { list: BulkOperateItemType[] };

export function BulkOperateBar({ list }: BulkOperateBarProps) {
const isDeleteItem = useCallback((id: string) => {
return id === 'delete';
}, []);

return (
<Card className="mb-4">
<CardContent className="p-1">
<ul className="flex gap-2">
{list.map((x) => (
<li
key={x.id}
className={cn({ ['text-text-delete-red']: isDeleteItem(x.id) })}
>
<ConfirmDeleteDialog
hidden={!isDeleteItem(x.id)}
onOk={x.onClick}
>
<Button
variant={'ghost'}
onClick={isDeleteItem(x.id) ? () => {} : x.onClick}
>
{x.icon} {x.label}
</Button>
</ConfirmDeleteDialog>
</li>
))}
</ul>
</CardContent>
</Card>
);
}

+ 6
- 0
web/src/components/confirm-delete-dialog.tsx 查看文件

@@ -16,15 +16,21 @@ interface IProps {
title?: string;
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
hidden?: boolean;
}

export function ConfirmDeleteDialog({
children,
title,
onOk,
hidden = false,
}: IProps & PropsWithChildren) {
const { t } = useTranslation();

if (hidden) {
return children;
}

return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>

+ 94
- 2
web/src/hooks/use-file-request.ts 查看文件

@@ -1,9 +1,18 @@
import { IFolder } from '@/interfaces/database/file-manager';
import {
IFetchFileListResult,
IFolder,
} from '@/interfaces/database/file-manager';
import fileManagerService from '@/services/file-manager-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { useDebounce } from 'ahooks';
import { PaginationProps, message } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'umi';
import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { useSetPaginationParams } from './route-hook';

export const enum FileApiAction {
@@ -12,6 +21,7 @@ export const enum FileApiAction {
MoveFile = 'moveFile',
CreateFolder = 'createFolder',
FetchParentFolderList = 'fetchParentFolderList',
DeleteFile = 'deleteFile',
}

export const useGetFolderId = () => {
@@ -136,3 +146,85 @@ export const useFetchParentFolderList = () => {

return data;
};

export interface IListResult {
searchString: string;
handleInputChange: React.ChangeEventHandler<HTMLInputElement>;
pagination: PaginationProps;
setPagination: (pagination: { page: number; pageSize: number }) => void;
loading: boolean;
}

export const useFetchFileList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
const id = useGetFolderId();
const debouncedSearchString = useDebounce(searchString, { wait: 500 });

const { data, isFetching: loading } = useQuery<IFetchFileListResult>({
queryKey: [
FileApiAction.FetchFileList,
{
id,
debouncedSearchString,
...pagination,
},
],
initialData: { files: [], parent_folder: {} as IFolder, total: 0 },
gcTime: 0,
queryFn: async () => {
const { data } = await fileManagerService.listFile({
parent_id: id,
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
});

return data?.data;
},
});

const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
setPagination({ page: 1 });
handleInputChange(e);
},
[handleInputChange, setPagination],
);

return {
...data,
searchString,
handleInputChange: onInputChange,
pagination: { ...pagination, total: data?.total },
setPagination,
loading,
};
};

export const useDeleteFile = () => {
const { setPaginationParams } = useSetPaginationParams();
const queryClient = useQueryClient();
const { t } = useTranslation();

const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [FileApiAction.DeleteFile],
mutationFn: async (params: { fileIds: string[]; parentId: string }) => {
const { data } = await fileManagerService.removeFile(params);
if (data.code === 0) {
message.success(t('message.deleted'));
setPaginationParams(1); // TODO: There should be a better way to paginate the request list
queryClient.invalidateQueries({
queryKey: [FileApiAction.FetchFileList],
});
}
return data.code;
},
});

return { data, loading, deleteFile: mutateAsync };
};

+ 6
- 0
web/src/interfaces/database/file-manager.ts 查看文件

@@ -31,3 +31,9 @@ export interface IFolder {
update_time: number;
source_type: string;
}

export type IFetchFileListResult = {
files: IFile[];
parent_folder: IFolder;
total: number;
};

+ 5
- 1
web/src/pages/dataset/dataset/index.tsx 查看文件

@@ -1,8 +1,10 @@
import { BulkOperateBar } from '@/components/bulk-operate-bar';
import { FileUploadDialog } from '@/components/file-upload-dialog';
import ListFilterBar from '@/components/list-filter-bar';
import { Button } from '@/components/ui/button';
import { Upload } from 'lucide-react';
import { DatasetTable } from './dataset-table';
import { useBulkOperateDataset } from './use-bulk-operate-dataset';
import { useHandleUploadDocument } from './use-upload-document';

export default function Dataset() {
@@ -13,6 +15,8 @@ export default function Dataset() {
onDocumentUploadOk,
documentUploadLoading,
} = useHandleUploadDocument();
const { list } = useBulkOperateDataset();

return (
<section className="p-8">
<ListFilterBar title="Files">
@@ -25,8 +29,8 @@ export default function Dataset() {
Upload file
</Button>
</ListFilterBar>
<BulkOperateBar list={list}></BulkOperateBar>
<DatasetTable></DatasetTable>

{documentUploadVisible && (
<FileUploadDialog
hideModal={hideDocumentUploadModal}

+ 41
- 0
web/src/pages/dataset/dataset/use-bulk-operate-dataset.tsx 查看文件

@@ -0,0 +1,41 @@
import { Ban, CircleCheck, CircleX, Play, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';

export function useBulkOperateDataset() {
const { t } = useTranslation();

const list = [
{
id: 'enabled',
label: t('knowledgeDetails.enabled'),
icon: <CircleCheck />,
onClick: () => {},
},
{
id: 'disabled',
label: t('knowledgeDetails.disabled'),
icon: <Ban />,
onClick: () => {},
},
{
id: 'run',
label: t('knowledgeDetails.run'),
icon: <Play />,
onClick: () => {},
},
{
id: 'cancel',
label: t('knowledgeDetails.cancel'),
icon: <CircleX />,
onClick: () => {},
},
{
id: 'delete',
label: t('common.delete'),
icon: <Trash2 />,
onClick: () => {},
},
];

return { list };
}

+ 3
- 1
web/src/pages/datasets/index.tsx 查看文件

@@ -5,6 +5,7 @@ import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request';
import { pick } from 'lodash';
import { Plus } from 'lucide-react';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { DatasetCard } from './dataset-card';
import { DatasetCreatingDialog } from './dataset-creating-dialog';
import { DatasetsFilterPopover } from './datasets-filter-popover';
@@ -13,6 +14,7 @@ import { useSaveKnowledge } from './hooks';
import { useRenameDataset } from './use-rename-dataset';

export default function Datasets() {
const { t } = useTranslation();
const {
visible,
hideModal,
@@ -63,8 +65,8 @@ export default function Datasets() {
>
<Button variant={'tertiary'} size={'sm'} onClick={showModal}>
<Plus className="mr-2 h-4 w-4" />
{t('knowledgeList.createKnowledgeBase')}
</Button>
Create dataset
</ListFilterBar>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8">
{kbs.map((dataset) => {

+ 2
- 2
web/src/pages/files/action-cell.tsx 查看文件

@@ -17,12 +17,12 @@ import {
UseHandleConnectToKnowledgeReturnType,
UseRenameCurrentFileReturnType,
} from './hooks';
import { UseMoveDocumentReturnType } from './use-move-file';
import { UseMoveDocumentShowType } from './use-move-file';

type IProps = Pick<CellContext<IFile, unknown>, 'row'> &
Pick<UseHandleConnectToKnowledgeReturnType, 'showConnectToKnowledgeModal'> &
Pick<UseRenameCurrentFileReturnType, 'showFileRenameModal'> &
Pick<UseMoveDocumentReturnType, 'showMoveFileModal'>;
UseMoveDocumentShowType;

export function ActionCell({
row,

+ 26
- 26
web/src/pages/files/files-table.tsx 查看文件

@@ -3,6 +3,8 @@
import {
ColumnDef,
ColumnFiltersState,
OnChangeFn,
RowSelectionState,
SortingState,
VisibilityState,
flexRender,
@@ -33,7 +35,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useFetchFileList } from '@/hooks/file-manager-hooks';
import { useFetchFileList } from '@/hooks/use-file-request';
import { IFile } from '@/interfaces/database/file-manager';
import { cn } from '@/lib/utils';
import { formatFileSize } from '@/utils/common-util';
@@ -44,18 +46,33 @@ import { useTranslation } from 'react-i18next';
import { ActionCell } from './action-cell';
import { useHandleConnectToKnowledge, useRenameCurrentFile } from './hooks';
import { LinkToDatasetDialog } from './link-to-dataset-dialog';
import { MoveDialog } from './move-dialog';
import { useHandleMoveFile } from './use-move-file';
import { UseMoveDocumentShowType } from './use-move-file';
import { useNavigateToOtherFolder } from './use-navigate-to-folder';

export function FilesTable() {
type FilesTableProps = Pick<
ReturnType<typeof useFetchFileList>,
'files' | 'loading' | 'pagination' | 'setPagination' | 'total'
> & {
rowSelection: RowSelectionState;
setRowSelection: OnChangeFn<RowSelectionState>;
} & UseMoveDocumentShowType;

export function FilesTable({
files,
total,
pagination,
setPagination,
loading,
rowSelection,
setRowSelection,
showMoveFileModal,
}: FilesTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const { t } = useTranslation('translation', {
keyPrefix: 'fileManager',
});
@@ -77,16 +94,6 @@ export function FilesTable() {
fileRenameLoading,
} = useRenameCurrentFile();

const {
showMoveFileModal,
moveFileVisible,
onMoveFileOk,
hideMoveFileModal,
moveFileLoading,
} = useHandleMoveFile();

const { pagination, data, loading, setPagination } = useFetchFileList();

const columns: ColumnDef<IFile>[] = [
{
id: 'select',
@@ -244,7 +251,7 @@ export function FilesTable() {
}, [pagination]);

const table = useReactTable({
data: data?.files || [],
data: files || [],
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
@@ -277,7 +284,7 @@ export function FilesTable() {
rowSelection,
pagination: currentPagination,
},
rowCount: data?.total ?? 0,
rowCount: total ?? 0,
debugTable: true,
});

@@ -333,8 +340,8 @@ export function FilesTable() {
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '}
row(s) selected.
{table.getFilteredSelectedRowModel().rows.length} of {total} row(s)
selected.
</div>
<div className="space-x-2">
<Button
@@ -371,13 +378,6 @@ export function FilesTable() {
loading={fileRenameLoading}
></RenameDialog>
)}
{moveFileVisible && (
<MoveDialog
hideModal={hideMoveFileModal}
onOk={onMoveFileOk}
loading={moveFileLoading}
></MoveDialog>
)}
</div>
);
}

+ 1
- 25
web/src/pages/files/hooks.ts 查看文件

@@ -1,7 +1,6 @@
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import {
useConnectToKnowledge,
useDeleteFile,
useRenameFile,
} from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager';
@@ -77,29 +76,6 @@ export type UseRenameCurrentFileReturnType = ReturnType<
typeof useRenameCurrentFile
>;

export const useHandleDeleteFile = (
fileIds: string[],
setSelectedRowKeys: (keys: string[]) => void,
) => {
const { deleteFile: removeDocument } = useDeleteFile();
const showDeleteConfirm = useShowDeleteConfirm();
const parentId = useGetFolderId();

const handleRemoveFile = () => {
showDeleteConfirm({
onOk: async () => {
const code = await removeDocument({ fileIds, parentId });
if (code === 0) {
setSelectedRowKeys([]);
}
return;
},
});
};

return { handleRemoveFile };
};

export const useHandleConnectToKnowledge = () => {
const {
visible: connectToKnowledgeVisible,

+ 60
- 2
web/src/pages/files/index.tsx 查看文件

@@ -1,3 +1,4 @@
import { BulkOperateBar } from '@/components/bulk-operate-bar';
import { FileUploadDialog } from '@/components/file-upload-dialog';
import ListFilterBar from '@/components/list-filter-bar';
import { Button } from '@/components/ui/button';
@@ -8,12 +9,19 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useFetchFileList } from '@/hooks/use-file-request';
import { RowSelectionState } from '@tanstack/react-table';
import { isEmpty } from 'lodash';
import { Upload } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CreateFolderDialog } from './create-folder-dialog';
import { FileBreadcrumb } from './file-breadcrumb';
import { FilesTable } from './files-table';
import { MoveDialog } from './move-dialog';
import { useBulkOperateFile } from './use-bulk-operate-file';
import { useHandleCreateFolder } from './use-create-folder';
import { useHandleMoveFile } from './use-move-file';
import { useHandleUploadFile } from './use-upload-file';

export default function Files() {
@@ -34,6 +42,33 @@ export default function Files() {
onFolderCreateOk,
} = useHandleCreateFolder();

const {
pagination,
files,
total,
loading,
setPagination,
searchString,
handleInputChange,
} = useFetchFileList();

const {
showMoveFileModal,
moveFileVisible,
onMoveFileOk,
hideMoveFileModal,
moveFileLoading,
} = useHandleMoveFile();

const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const { list } = useBulkOperateFile({
files,
rowSelection,
showMoveFileModal,
setRowSelection,
});

const leftPanel = (
<div>
<FileBreadcrumb></FileBreadcrumb>
@@ -42,7 +77,12 @@ export default function Files() {

return (
<section className="p-8">
<ListFilterBar leftPanel={leftPanel}>
<ListFilterBar
leftPanel={leftPanel}
searchString={searchString}
onSearchChange={handleInputChange}
showFilter={false}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'tertiary'} size={'sm'}>
@@ -61,7 +101,17 @@ export default function Files() {
</DropdownMenuContent>
</DropdownMenu>
</ListFilterBar>
<FilesTable></FilesTable>
{!isEmpty(rowSelection) && <BulkOperateBar list={list}></BulkOperateBar>}
<FilesTable
files={files}
total={total}
pagination={pagination}
setPagination={setPagination}
loading={loading}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
showMoveFileModal={showMoveFileModal}
></FilesTable>
{fileUploadVisible && (
<FileUploadDialog
hideModal={hideFileUploadModal}
@@ -77,6 +127,14 @@ export default function Files() {
onOk={onFolderCreateOk}
></CreateFolderDialog>
)}

{moveFileVisible && (
<MoveDialog
hideModal={hideMoveFileModal}
onOk={onMoveFileOk}
loading={moveFileLoading}
></MoveDialog>
)}
</section>
);
}

+ 53
- 0
web/src/pages/files/use-bulk-operate-file.tsx 查看文件

@@ -0,0 +1,53 @@
import { IFile } from '@/interfaces/database/file-manager';
import { OnChangeFn, RowSelectionState } from '@tanstack/react-table';
import { FolderInput, Trash2 } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleDeleteFile } from './use-delete-file';
import { UseMoveDocumentShowType } from './use-move-file';

export function useBulkOperateFile({
files,
rowSelection,
showMoveFileModal,
setRowSelection,
}: {
files: IFile[];
rowSelection: RowSelectionState;
setRowSelection: OnChangeFn<RowSelectionState>;
} & UseMoveDocumentShowType) {
const { t } = useTranslation();

const selectedIds = useMemo(() => {
const indexes = Object.keys(rowSelection);
return files
.filter((x, idx) => indexes.some((y) => Number(y) === idx))
.map((x) => x.id);
}, [files, rowSelection]);

const { handleRemoveFile } = useHandleDeleteFile();

const list = [
{
id: 'move',
label: t('common.move'),
icon: <FolderInput />,
onClick: () => {
showMoveFileModal(selectedIds);
},
},
{
id: 'delete',
label: t('common.delete'),
icon: <Trash2 />,
onClick: async () => {
const code = await handleRemoveFile(selectedIds);
if (code === 0) {
setRowSelection({});
}
},
},
];

return { list };
}

+ 19
- 0
web/src/pages/files/use-delete-file.ts 查看文件

@@ -0,0 +1,19 @@
import { useDeleteFile } from '@/hooks/use-file-request';
import { useCallback } from 'react';
import { useGetFolderId } from './hooks';

export const useHandleDeleteFile = () => {
const { deleteFile: removeDocument } = useDeleteFile();
const parentId = useGetFolderId();

const handleRemoveFile = useCallback(
async (fileIds: string[]) => {
const code = await removeDocument({ fileIds, parentId });

return code;
},
[parentId, removeDocument],
);

return { handleRemoveFile };
};

+ 42
- 39
web/src/pages/files/use-move-file.ts 查看文件

@@ -2,49 +2,52 @@ import { useSetModalState } from '@/hooks/common-hooks';
import { useMoveFile } from '@/hooks/use-file-request';
import { useCallback, useState } from 'react';

export const useHandleMoveFile = () =>
// setSelectedRowKeys: (keys: string[]) => void,
{
const {
visible: moveFileVisible,
hideModal: hideMoveFileModal,
showModal: showMoveFileModal,
} = useSetModalState();
const { moveFile, loading } = useMoveFile();
const [sourceFileIds, setSourceFileIds] = useState<string[]>([]);
export const useHandleMoveFile = () => {
const {
visible: moveFileVisible,
hideModal: hideMoveFileModal,
showModal: showMoveFileModal,
} = useSetModalState();
const { moveFile, loading } = useMoveFile();
const [sourceFileIds, setSourceFileIds] = useState<string[]>([]);

const onMoveFileOk = useCallback(
async (targetFolderId: string) => {
const ret = await moveFile({
src_file_ids: sourceFileIds,
dest_file_id: targetFolderId,
});
const onMoveFileOk = useCallback(
async (targetFolderId: string) => {
const ret = await moveFile({
src_file_ids: sourceFileIds,
dest_file_id: targetFolderId,
});

if (ret === 0) {
// setSelectedRowKeys([]);
hideMoveFileModal();
}
return ret;
},
[moveFile, hideMoveFileModal, sourceFileIds],
);
if (ret === 0) {
// setSelectedRowKeys([]);
hideMoveFileModal();
}
return ret;
},
[moveFile, hideMoveFileModal, sourceFileIds],
);

const handleShowMoveFileModal = useCallback(
(ids: string[]) => {
setSourceFileIds(ids);
showMoveFileModal();
},
[showMoveFileModal],
);
const handleShowMoveFileModal = useCallback(
(ids: string[]) => {
setSourceFileIds(ids);
showMoveFileModal();
},
[showMoveFileModal],
);

return {
initialValue: '',
moveFileLoading: loading,
onMoveFileOk,
moveFileVisible,
hideMoveFileModal,
showMoveFileModal: handleShowMoveFileModal,
};
return {
initialValue: '',
moveFileLoading: loading,
onMoveFileOk,
moveFileVisible,
hideMoveFileModal,
showMoveFileModal: handleShowMoveFileModal,
};
};

export type UseMoveDocumentReturnType = ReturnType<typeof useHandleMoveFile>;

export type UseMoveDocumentShowType = Pick<
ReturnType<typeof useHandleMoveFile>,
'showMoveFileModal'
>;

Loading…
取消
儲存