Feat: Adjust the operation cell of the table on the file management page and dataset page #3221. (#7526)

### What problem does this PR solve?

Feat: Adjust the operation cell of the table on the file management page
and dataset page #3221.
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-05-08 15:25:26 +08:00 committed by GitHub
parent 9d3dd13fef
commit 1657755b5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 163 additions and 115 deletions

11
web/package-lock.json generated
View File

@ -64,7 +64,7 @@
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lexical": "^0.23.1", "lexical": "^0.23.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.454.0", "lucide-react": "^0.508.0",
"mammoth": "^1.7.2", "mammoth": "^1.7.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8", "openai-speech-stream-player": "^1.0.8",
@ -22959,11 +22959,12 @@
} }
}, },
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "0.454.0", "version": "0.508.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.454.0.tgz", "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.508.0.tgz",
"integrity": "sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==", "integrity": "sha512-gcP16PnexqtOFrTtv98kVsGzTfnbPekzZiQfByi2S89xfk7E/4uKE1USZqccIp58v42LqkO7MuwpCqshwSrJCg==",
"license": "ISC",
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/lz-string": { "node_modules/lz-string": {

View File

@ -75,7 +75,7 @@
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lexical": "^0.23.1", "lexical": "^0.23.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.454.0", "lucide-react": "^0.508.0",
"mammoth": "^1.7.2", "mammoth": "^1.7.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8", "openai-speech-stream-player": "^1.0.8",

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,10 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { BrushCleaning } from 'lucide-react';
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
import { ConfirmDeleteDialog } from './confirm-delete-dialog'; import { ConfirmDeleteDialog } from './confirm-delete-dialog';
import { Separator } from './ui/separator';
export type BulkOperateItemType = { export type BulkOperateItemType = {
id: string; id: string;
@ -11,16 +13,21 @@ export type BulkOperateItemType = {
onClick(): void; onClick(): void;
}; };
type BulkOperateBarProps = { list: BulkOperateItemType[] }; type BulkOperateBarProps = { list: BulkOperateItemType[]; count: number };
export function BulkOperateBar({ list }: BulkOperateBarProps) { export function BulkOperateBar({ list, count }: BulkOperateBarProps) {
const isDeleteItem = useCallback((id: string) => { const isDeleteItem = useCallback((id: string) => {
return id === 'delete'; return id === 'delete';
}, []); }, []);
return ( return (
<Card className="mb-4"> <Card className="mb-4">
<CardContent className="p-1"> <CardContent className="p-1 pl-5 flex items-center gap-6">
<section className="text-text-sub-title-invert flex items-center gap-2">
<span>Selected: {count} Files</span>
<BrushCleaning className="size-3" />
</section>
<Separator orientation={'vertical'} className="h-3"></Separator>
<ul className="flex gap-2"> <ul className="flex gap-2">
{list.map((x) => ( {list.map((x) => (
<li <li

View File

@ -22,7 +22,7 @@ export function FileIcon({
return ( return (
<span className={cn('size-4', className)}> <span className={cn('size-4', className)}>
<IconFont <IconFont
name={isFolder ? 'file' : FileIconMap[getExtension(name)]} name={isFolder ? 'file-sub' : FileIconMap[getExtension(name)]}
></IconFont> ></IconFont>
</span> </span>
); );

View File

@ -11,6 +11,7 @@ interface IProps extends React.PropsWithChildren {
documentName: string; documentName: string;
documentId?: string; documentId?: string;
prefix?: string; prefix?: string;
className?: string;
} }
const NewDocumentLink = ({ const NewDocumentLink = ({
@ -21,6 +22,7 @@ const NewDocumentLink = ({
documentId, documentId,
documentName, documentName,
prefix = 'file', prefix = 'file',
className,
}: IProps) => { }: IProps) => {
let nextLink = link; let nextLink = link;
const extension = getExtension(documentName); const extension = getExtension(documentName);
@ -38,7 +40,8 @@ const NewDocumentLink = ({
} }
href={nextLink} href={nextLink}
rel="noreferrer" rel="noreferrer"
style={{ color, wordBreak: 'break-all' }} style={{ color: className ? '' : color, wordBreak: 'break-all' }}
className={className}
> >
{children} {children}
</a> </a>

View File

@ -11,7 +11,7 @@ const badgeVariants = cva(
default: default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary: secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 'border-transparent bg-background-card text-text-sub-title-invert hover:bg-secondary/80 rounded-md',
destructive: destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground', outline: 'text-foreground',

View File

@ -4,9 +4,14 @@ import { cn } from '@/lib/utils';
const Table = React.forwardRef< const Table = React.forwardRef<
HTMLTableElement, HTMLTableElement,
React.HTMLAttributes<HTMLTableElement> React.HTMLAttributes<HTMLTableElement> & { rootClassName?: string }
>(({ className, ...props }, ref) => ( >(({ className, rootClassName, ...props }, ref) => (
<div className="relative w-full overflow-auto rounded-2xl bg-background-card"> <div
className={cn(
'relative w-full overflow-auto rounded-2xl bg-background-card',
rootClassName,
)}
>
<table <table
ref={ref} ref={ref}
className={cn('w-full caption-bottom text-sm ', className)} className={cn('w-full caption-bottom text-sm ', className)}
@ -20,7 +25,11 @@ const TableHeader = React.forwardRef<
HTMLTableSectionElement, HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement> React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} /> <thead
ref={ref}
className={cn('[&_tr]:border-b top-0 sticky', className)}
{...props}
/>
)); ));
TableHeader.displayName = 'TableHeader'; TableHeader.displayName = 'TableHeader';

View File

@ -9,11 +9,16 @@ export function useRowSelection() {
setRowSelection({}); setRowSelection({});
}, []); }, []);
const selectedCount = useMemo(() => {
return Object.keys(rowSelection).length;
}, [rowSelection]);
return { return {
rowSelection, rowSelection,
setRowSelection, setRowSelection,
rowSelectionIsEmpty: isEmpty(rowSelection), rowSelectionIsEmpty: isEmpty(rowSelection),
clearRowSelection, clearRowSelection,
selectedCount,
}; };
} }

View File

@ -93,7 +93,7 @@ export const useFetchDocumentList = () => {
filterValue, filterValue,
], ],
initialData: { docs: [], total: 0 }, initialData: { docs: [], total: 0 },
refetchInterval: 15000, // refetchInterval: 15000,
enabled: !!knowledgeId || !!id, enabled: !!knowledgeId || !!id,
queryFn: async () => { queryFn: async () => {
const ret = await listDocument( const ret = await listDocument(

View File

@ -11,7 +11,7 @@ import { IDocumentInfo } from '@/interfaces/database/document';
import { formatFileSize } from '@/utils/common-util'; import { formatFileSize } from '@/utils/common-util';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { downloadDocument } from '@/utils/file-util'; import { downloadDocument } from '@/utils/file-util';
import { ArrowDownToLine, Pencil, ScrollText, Trash2 } from 'lucide-react'; import { ArrowDownToLine, FolderPen, ScrollText, Trash2 } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { UseRenameDocumentShowType } from './use-rename-document'; import { UseRenameDocumentShowType } from './use-rename-document';
import { isParserRunning } from './utils'; import { isParserRunning } from './utils';
@ -50,10 +50,18 @@ export function DatasetActionCell({
}, [record, showRenameModal]); }, [record, showRenameModal]);
return ( return (
<section className="flex gap-4 items-center"> <section className="flex gap-4 items-center text-text-sub-title-invert">
<Button
variant={'ghost'}
size={'sm'}
disabled={isRunning}
onClick={handleRename}
>
<FolderPen />
</Button>
<HoverCard> <HoverCard>
<HoverCardTrigger> <HoverCardTrigger>
<Button variant="ghost" size={'icon'} disabled={isRunning}> <Button variant="ghost" disabled={isRunning} size={'sm'}>
<ScrollText /> <ScrollText />
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
@ -77,27 +85,20 @@ export function DatasetActionCell({
</ul> </ul>
</HoverCardContent> </HoverCardContent>
</HoverCard> </HoverCard>
<Button
variant={'ghost'}
size={'icon'}
disabled={isRunning}
onClick={handleRename}
>
<Pencil />
</Button>
{isVirtualDocument || ( {isVirtualDocument || (
<Button <Button
variant={'ghost'} variant={'ghost'}
size={'icon'}
onClick={onDownloadDocument} onClick={onDownloadDocument}
disabled={isRunning} disabled={isRunning}
size={'sm'}
> >
<ArrowDownToLine /> <ArrowDownToLine />
</Button> </Button>
)} )}
<ConfirmDeleteDialog onOk={handleRemove}> <ConfirmDeleteDialog onOk={handleRemove}>
<Button variant={'ghost'} size={'icon'} disabled={isRunning}> <Button variant={'ghost'} size={'sm'} disabled={isRunning}>
<Trash2 className="text-text-delete-red" /> <Trash2 />
</Button> </Button>
</ConfirmDeleteDialog> </ConfirmDeleteDialog>
</section> </section>

View File

@ -119,7 +119,7 @@ export function DatasetTable({
return ( return (
<div className="w-full"> <div className="w-full">
<Table> <Table rootClassName="max-h-[82vh]">
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
@ -164,11 +164,7 @@ export function DatasetTable({
)} )}
</TableBody> </TableBody>
</Table> </Table>
<div className="flex items-center justify-end space-x-2 py-4"> <div className="flex items-center justify-end py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{pagination?.total} row(s) selected.
</div>
<div className="space-x-2"> <div className="space-x-2">
<RAGFlowPagination <RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')} {...pick(pagination, 'current', 'pageSize')}

View File

@ -50,7 +50,7 @@ export default function Dataset() {
showCreateModal, showCreateModal,
} = useCreateEmptyDocument(); } = useCreateEmptyDocument();
const { rowSelection, rowSelectionIsEmpty, setRowSelection } = const { rowSelection, rowSelectionIsEmpty, setRowSelection, selectedCount } =
useRowSelection(); useRowSelection();
const { list } = useBulkOperateDataset({ const { list } = useBulkOperateDataset({
@ -68,6 +68,15 @@ export default function Dataset() {
value={filterValue} value={filterValue}
onChange={handleFilterSubmit} onChange={handleFilterSubmit}
filters={filters} filters={filters}
leftPanel={
<div className="items-start">
<div className="pb-1">Dataset</div>
<div className="text-text-sub-title-invert text-sm">
Please wait for your files to finish parsing before starting an
AI-powered chat.
</div>
</div>
}
> >
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -87,7 +96,9 @@ export default function Dataset() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</ListFilterBar> </ListFilterBar>
{rowSelectionIsEmpty || <BulkOperateBar list={list}></BulkOperateBar>} {rowSelectionIsEmpty || (
<BulkOperateBar list={list} count={selectedCount}></BulkOperateBar>
)}
<DatasetTable <DatasetTable
documents={documents} documents={documents}
pagination={pagination} pagination={pagination}

View File

@ -56,10 +56,10 @@ export function ParsingStatusCell({
}, [record, showSetMetaModal]); }, [record, showSetMetaModal]);
return ( return (
<section className="flex gap-2 items-center "> <section className="flex gap-2 items-center">
<div> <div className="w-28 flex items-center justify-between">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger asChild>
<Button variant={'ghost'} size={'sm'}> <Button variant={'ghost'} size={'sm'}>
{parser_id} {parser_id}
</Button> </Button>
@ -73,8 +73,7 @@ export function ParsingStatusCell({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<Separator orientation="vertical" className="h-2.5" />
<Separator orientation="vertical" />
</div> </div>
<ConfirmDeleteDialog <ConfirmDeleteDialog
title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })} title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })}

View File

@ -1,13 +1,6 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import NewDocumentLink from '@/components/new-document-link'; import NewDocumentLink from '@/components/new-document-link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useDownloadFile } from '@/hooks/file-manager-hooks'; import { useDownloadFile } from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager'; import { IFile } from '@/interfaces/database/file-manager';
import { import {
@ -15,9 +8,15 @@ import {
isSupportedPreviewDocumentType, isSupportedPreviewDocumentType,
} from '@/utils/document-util'; } from '@/utils/document-util';
import { CellContext } from '@tanstack/react-table'; import { CellContext } from '@tanstack/react-table';
import { EllipsisVertical, Eye, Link2, Trash2 } from 'lucide-react'; import {
ArrowDownToLine,
Eye,
FolderInput,
FolderPen,
Link2,
Trash2,
} from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
UseHandleConnectToKnowledgeReturnType, UseHandleConnectToKnowledgeReturnType,
UseRenameCurrentFileReturnType, UseRenameCurrentFileReturnType,
@ -36,7 +35,6 @@ export function ActionCell({
showFileRenameModal, showFileRenameModal,
showMoveFileModal, showMoveFileModal,
}: IProps) { }: IProps) {
const { t } = useTranslation();
const record = row.original; const record = row.original;
const documentId = record.id; const documentId = record.id;
const { downloadFile } = useDownloadFile(); const { downloadFile } = useDownloadFile();
@ -63,33 +61,43 @@ export function ActionCell({
}, [record, showMoveFileModal]); }, [record, showMoveFileModal]);
return ( return (
<section className="flex gap-4 items-center"> <section className="flex gap-4 items-center text-text-sub-title-invert">
<Button <Button
variant="ghost" variant="ghost"
size={'icon'} size={'sm'}
onClick={handleShowConnectToKnowledgeModal} onClick={handleShowConnectToKnowledgeModal}
> >
<Link2 /> <Link2 />
</Button> </Button>
<ConfirmDeleteDialog> <Button variant="ghost" size={'sm'} onClick={handleShowMoveFileModal}>
<Button variant="ghost" size={'icon'}> <FolderInput />
<Trash2 className="text-text-delete-red" /> </Button>
<Button variant="ghost" size={'sm'} onClick={handleShowFileRenameModal}>
<FolderPen />
</Button>
{isFolder || (
<Button variant={'ghost'} size={'sm'} onClick={onDownloadDocument}>
<ArrowDownToLine />
</Button> </Button>
</ConfirmDeleteDialog> )}
{isSupportedPreviewDocumentType(extension) && ( {isSupportedPreviewDocumentType(extension) && (
<NewDocumentLink <NewDocumentLink
documentId={documentId} documentId={documentId}
documentName={record.name} documentName={record.name}
color="black" className="text-text-sub-title-invert"
> >
<Button variant={'ghost'} size={'icon'}> <Button variant={'ghost'} size={'sm'}>
<Eye /> <Eye />
</Button> </Button>
</NewDocumentLink> </NewDocumentLink>
)} )}
<DropdownMenu>
{/* <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size={'icon'}> <Button variant="ghost" size={'sm'}>
<EllipsisVertical /> <EllipsisVertical />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -108,7 +116,12 @@ export function ActionCell({
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu> */}
<ConfirmDeleteDialog>
<Button variant="ghost" size={'sm'}>
<Trash2 />
</Button>
</ConfirmDeleteDialog>
</section> </section>
); );
} }

View File

@ -304,12 +304,7 @@ export function FilesTable({
</TableBody> </TableBody>
</Table> </Table>
<div className="flex items-center justify-end space-x-2 py-4"> <div className="flex items-center justify-end py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of {total} row(s)
selected.
</div>
<div className="space-x-2"> <div className="space-x-2">
<RAGFlowPagination <RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')} {...pick(pagination, 'current', 'pageSize')}

View File

@ -20,6 +20,7 @@ import { MoveDialog } from './move-dialog';
import { useBulkOperateFile } from './use-bulk-operate-file'; import { useBulkOperateFile } from './use-bulk-operate-file';
import { useHandleCreateFolder } from './use-create-folder'; import { useHandleCreateFolder } from './use-create-folder';
import { useHandleMoveFile } from './use-move-file'; import { useHandleMoveFile } from './use-move-file';
import { useSelectBreadcrumbItems } from './use-navigate-to-folder';
import { useHandleUploadFile } from './use-upload-file'; import { useHandleUploadFile } from './use-upload-file';
export default function Files() { export default function Files() {
@ -55,6 +56,7 @@ export default function Files() {
setRowSelection, setRowSelection,
rowSelectionIsEmpty, rowSelectionIsEmpty,
clearRowSelection, clearRowSelection,
selectedCount,
} = useRowSelection(); } = useRowSelection();
const { const {
@ -72,9 +74,11 @@ export default function Files() {
setRowSelection, setRowSelection,
}); });
const breadcrumbItems = useSelectBreadcrumbItems();
const leftPanel = ( const leftPanel = (
<div> <div>
<FileBreadcrumb></FileBreadcrumb> {breadcrumbItems.length > 0 ? <FileBreadcrumb></FileBreadcrumb> : 'File'}
</div> </div>
); );
@ -85,6 +89,7 @@ export default function Files() {
searchString={searchString} searchString={searchString}
onSearchChange={handleInputChange} onSearchChange={handleInputChange}
showFilter={false} showFilter={false}
icon={'file'}
> >
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -104,7 +109,9 @@ export default function Files() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</ListFilterBar> </ListFilterBar>
{!rowSelectionIsEmpty && <BulkOperateBar list={list}></BulkOperateBar>} {!rowSelectionIsEmpty && (
<BulkOperateBar list={list} count={selectedCount}></BulkOperateBar>
)}
<FilesTable <FilesTable
files={files} files={files}
total={total} total={total}

View File

@ -7,12 +7,13 @@ import {
HoverCardTrigger, HoverCardTrigger,
} from '@/components/ui/hover-card'; } from '@/components/ui/hover-card';
import { IFile } from '@/interfaces/database/file-manager'; import { IFile } from '@/interfaces/database/file-manager';
import { Ellipsis } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
export function KnowledgeCell({ value }: { value: IFile['kbs_info'] }) { export function KnowledgeCell({ value }: { value: IFile['kbs_info'] }) {
const renderBadges = useCallback((list: IFile['kbs_info'] = []) => { const renderBadges = useCallback((list: IFile['kbs_info'] = []) => {
return list.map((x) => ( return list.map((x) => (
<Badge key={x.kb_id} className="" variant={'tertiary'}> <Badge key={x.kb_id} variant={'secondary'}>
{x.kb_name} {x.kb_name}
</Badge> </Badge>
)); ));
@ -25,8 +26,8 @@ export function KnowledgeCell({ value }: { value: IFile['kbs_info'] }) {
{value.length > 2 && ( {value.length > 2 && (
<HoverCard> <HoverCard>
<HoverCardTrigger> <HoverCardTrigger>
<Button variant={'icon'} size={'auto'}> <Button variant={'ghost'} size={'sm'}>
+{value.length - 2} <Ellipsis />
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="flex gap-2 flex-wrap"> <HoverCardContent className="flex gap-2 flex-wrap">