diff --git a/web/src/components/bulk-operate-bar.tsx b/web/src/components/bulk-operate-bar.tsx new file mode 100644 index 000000000..a2ea76eda --- /dev/null +++ b/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 ( + + + + + + ); +} diff --git a/web/src/components/confirm-delete-dialog.tsx b/web/src/components/confirm-delete-dialog.tsx index c7c5c3efc..8a63fbf5d 100644 --- a/web/src/components/confirm-delete-dialog.tsx +++ b/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 ( {children} diff --git a/web/src/hooks/use-file-request.ts b/web/src/hooks/use-file-request.ts index 34bdbbf85..9146f5e5a 100644 --- a/web/src/hooks/use-file-request.ts +++ b/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; + 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({ + 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 = 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 }; +}; diff --git a/web/src/interfaces/database/file-manager.ts b/web/src/interfaces/database/file-manager.ts index bfcafa355..1a58e98bb 100644 --- a/web/src/interfaces/database/file-manager.ts +++ b/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; +}; diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index 3bcc99a9b..aa31b97d7 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/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 (
@@ -25,8 +29,8 @@ export default function Dataset() { Upload file + - {documentUploadVisible && ( , + onClick: () => {}, + }, + { + id: 'disabled', + label: t('knowledgeDetails.disabled'), + icon: , + onClick: () => {}, + }, + { + id: 'run', + label: t('knowledgeDetails.run'), + icon: , + onClick: () => {}, + }, + { + id: 'cancel', + label: t('knowledgeDetails.cancel'), + icon: , + onClick: () => {}, + }, + { + id: 'delete', + label: t('common.delete'), + icon: , + onClick: () => {}, + }, + ]; + + return { list }; +} diff --git a/web/src/pages/datasets/index.tsx b/web/src/pages/datasets/index.tsx index e1db3cebb..ce9f7e359 100644 --- a/web/src/pages/datasets/index.tsx +++ b/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() { > - Create dataset
{kbs.map((dataset) => { diff --git a/web/src/pages/files/action-cell.tsx b/web/src/pages/files/action-cell.tsx index f6b7d4663..e4893778a 100644 --- a/web/src/pages/files/action-cell.tsx +++ b/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, 'row'> & Pick & Pick & - Pick; + UseMoveDocumentShowType; export function ActionCell({ row, diff --git a/web/src/pages/files/files-table.tsx b/web/src/pages/files/files-table.tsx index 85eb4b156..65f5950cd 100644 --- a/web/src/pages/files/files-table.tsx +++ b/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, + 'files' | 'loading' | 'pagination' | 'setPagination' | 'total' +> & { + rowSelection: RowSelectionState; + setRowSelection: OnChangeFn; +} & UseMoveDocumentShowType; + +export function FilesTable({ + files, + total, + pagination, + setPagination, + loading, + rowSelection, + setRowSelection, + showMoveFileModal, +}: FilesTableProps) { const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState( [], ); const [columnVisibility, setColumnVisibility] = React.useState({}); - 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[] = [ { 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() {
- {table.getFilteredSelectedRowModel().rows.length} of {data?.total}{' '} - row(s) selected. + {table.getFilteredSelectedRowModel().rows.length} of {total} row(s) + selected.
); } diff --git a/web/src/pages/files/hooks.ts b/web/src/pages/files/hooks.ts index 99fedb024..171a25e29 100644 --- a/web/src/pages/files/hooks.ts +++ b/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, diff --git a/web/src/pages/files/index.tsx b/web/src/pages/files/index.tsx index 3226f6a08..b20b7a607 100644 --- a/web/src/pages/files/index.tsx +++ b/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({}); + + const { list } = useBulkOperateFile({ + files, + rowSelection, + showMoveFileModal, + setRowSelection, + }); + const leftPanel = (
@@ -42,7 +77,12 @@ export default function Files() { return (
- +
); } diff --git a/web/src/pages/files/use-bulk-operate-file.tsx b/web/src/pages/files/use-bulk-operate-file.tsx new file mode 100644 index 000000000..a4f9f6cc4 --- /dev/null +++ b/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; +} & 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: , + onClick: () => { + showMoveFileModal(selectedIds); + }, + }, + { + id: 'delete', + label: t('common.delete'), + icon: , + onClick: async () => { + const code = await handleRemoveFile(selectedIds); + if (code === 0) { + setRowSelection({}); + } + }, + }, + ]; + + return { list }; +} diff --git a/web/src/pages/files/use-delete-file.ts b/web/src/pages/files/use-delete-file.ts new file mode 100644 index 000000000..bf3fed528 --- /dev/null +++ b/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 }; +}; diff --git a/web/src/pages/files/use-move-file.ts b/web/src/pages/files/use-move-file.ts index 5b2294a0d..d73061b77 100644 --- a/web/src/pages/files/use-move-file.ts +++ b/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([]); +export const useHandleMoveFile = () => { + const { + visible: moveFileVisible, + hideModal: hideMoveFileModal, + showModal: showMoveFileModal, + } = useSetModalState(); + const { moveFile, loading } = useMoveFile(); + const [sourceFileIds, setSourceFileIds] = useState([]); - 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; + +export type UseMoveDocumentShowType = Pick< + ReturnType, + 'showMoveFileModal' +>;