diff --git a/surfsense_web/components/chat/DocumentsDataTable.tsx b/surfsense_web/components/chat/DocumentsDataTable.tsx index 2a28efcc3..f901c46e3 100644 --- a/surfsense_web/components/chat/DocumentsDataTable.tsx +++ b/surfsense_web/components/chat/DocumentsDataTable.tsx @@ -1,16 +1,16 @@ "use client"; import { - type ColumnDef, - type ColumnFiltersState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - type SortingState, - useReactTable, - type VisibilityState, + type ColumnDef, + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, } from "@tanstack/react-table"; import { ArrowUpDown, Calendar, FileText, Search } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; @@ -18,432 +18,462 @@ import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { Document, DocumentType } from "@/hooks/use-documents"; interface DocumentsDataTableProps { - documents: Document[]; - onSelectionChange: (documents: Document[]) => void; - onDone: () => void; - initialSelectedDocuments?: Document[]; - pageIndex?: number; - pageSize?: number; - onPageIndexChange?: (pageIndex: number) => void; - onPageSizeChange?: (pageSize: number) => void; - canNext?: boolean; + documents: Document[]; + onSelectionChange: (documents: Document[]) => void; + onDone: () => void; + initialSelectedDocuments?: Document[]; + pageIndex?: number; + pageSize?: number; + onPageIndexChange?: (pageIndex: number) => void; + onPageSizeChange?: (pageSize: number) => void; + canNext?: boolean; } // Combine EnumConnectorName with additional document types const DOCUMENT_TYPES: (string | "ALL")[] = [ - "ALL", - "FILE", - "EXTENSION", - "CRAWLED_URL", - "YOUTUBE_VIDEO", - ...Object.values(EnumConnectorName), + "ALL", + "FILE", + "EXTENSION", + "CRAWLED_URL", + "YOUTUBE_VIDEO", + ...Object.values(EnumConnectorName), ]; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - { - table.toggleAllPageRowsSelected(!!value); - }} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - { - row.toggleSelected(!!value); - }} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "title", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const title = row.getValue("title") as string; - return ( -
- {title} -
- ); - }, - }, - { - accessorKey: "document_type", - header: "Type", - cell: ({ row }) => { - const type = row.getValue("document_type") as DocumentType; - return ( -
- {getConnectorIcon(type)} -
- ); - }, - size: 80, - meta: { - className: "hidden sm:table-cell", - }, - }, - { - accessorKey: "content", - header: "Preview", - cell: ({ row }) => { - const content = row.getValue("content") as string; - return ( -
- {content.substring(0, 30)}... - {content.substring(0, 100)}... -
- ); - }, - enableSorting: false, - meta: { - className: "hidden md:table-cell", - }, - }, - { - accessorKey: "created_at", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return ( -
- - {date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} - - - {date.toLocaleDateString("en-US", { - month: "numeric", - day: "numeric", - })} - -
- ); - }, - size: 80, - }, + { + id: "select", + header: ({ table }) => ( + { + table.toggleAllPageRowsSelected(!!value); + }} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + { + row.toggleSelected(!!value); + }} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const title = row.getValue("title") as string; + return ( +
+ {title} +
+ ); + }, + }, + { + accessorKey: "document_type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("document_type") as DocumentType; + return ( +
+ {getConnectorIcon(type)} +
+ ); + }, + size: 80, + meta: { + className: "hidden sm:table-cell", + }, + }, + { + accessorKey: "content", + header: "Preview", + cell: ({ row }) => { + const content = row.getValue("content") as string; + return ( +
+ {content.substring(0, 30)}... + + {content.substring(0, 100)}... + +
+ ); + }, + enableSorting: false, + meta: { + className: "hidden md:table-cell", + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
+ + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + + {date.toLocaleDateString("en-US", { + month: "numeric", + day: "numeric", + })} + +
+ ); + }, + size: 80, + }, ]; export function DocumentsDataTable({ - documents, - onSelectionChange, - onDone, - initialSelectedDocuments = [], - pageIndex = 0, - pageSize = 100, - onPageIndexChange, - onPageSizeChange, - canNext = false, + documents, + onSelectionChange, + onDone, + initialSelectedDocuments = [], + pageIndex = 0, + pageSize = 100, + onPageIndexChange, + onPageSizeChange, + canNext = false, }: DocumentsDataTableProps) { - const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [documentTypeFilter, setDocumentTypeFilter] = useState("ALL"); + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [documentTypeFilter, setDocumentTypeFilter] = useState( + "ALL" + ); - // Memoize initial row selection to prevent infinite loops - const initialRowSelection = useMemo(() => { - if (!documents.length || !initialSelectedDocuments.length) return {}; + // Memoize initial row selection to prevent infinite loops + const initialRowSelection = useMemo(() => { + if (!documents.length || !initialSelectedDocuments.length) return {}; - const selection: Record = {}; - initialSelectedDocuments.forEach((selectedDoc) => { - // Find the document in the current documents array to get the correct row ID - const docInCurrentList = documents.find(doc => doc.id === selectedDoc.id); - if (docInCurrentList) { - selection[docInCurrentList.id.toString()] = true; - } - }); - return selection; - }, [documents, initialSelectedDocuments]); + const selection: Record = {}; + initialSelectedDocuments.forEach((selectedDoc) => { + // Find the document in the current documents array to get the correct row ID + const docInCurrentList = documents.find( + (doc) => doc.id === selectedDoc.id + ); + if (docInCurrentList) { + selection[docInCurrentList.id.toString()] = true; + } + }); + return selection; + }, [documents, initialSelectedDocuments]); - const [rowSelection, setRowSelection] = useState>(() => initialRowSelection); + const [rowSelection, setRowSelection] = useState>( + () => initialRowSelection + ); - // Update row selection when initial selection changes - useEffect(() => { - setRowSelection(initialRowSelection); - }, [initialRowSelection]); + // Update row selection when initial selection changes + useEffect(() => { + setRowSelection(initialRowSelection); + }, [initialRowSelection]); - const filteredDocuments = useMemo(() => { - if (documentTypeFilter === "ALL") return documents; - return documents.filter((doc) => doc.document_type === documentTypeFilter); - }, [documents, documentTypeFilter]); + const filteredDocuments = useMemo(() => { + if (documentTypeFilter === "ALL") return documents; + return documents.filter((doc) => doc.document_type === documentTypeFilter); + }, [documents, documentTypeFilter]); - const table = useReactTable({ - data: filteredDocuments, - columns, - getRowId: (row) => row.id.toString(), - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - // Disable internal pagination since we're handling it at the parent level - manualPagination: true, - pageCount: -1, - enableRowSelection: true, - state: { sorting, columnFilters, columnVisibility, rowSelection }, - }); + const table = useReactTable({ + data: filteredDocuments, + columns, + getRowId: (row) => row.id.toString(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + // Disable internal pagination since we're handling it at the parent level + manualPagination: true, + pageCount: -1, + enableRowSelection: true, + state: { sorting, columnFilters, columnVisibility, rowSelection }, + }); - useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows; - const selectedDocuments = selectedRows.map((row) => row.original); - onSelectionChange(selectedDocuments); - }, [rowSelection, table]); + useEffect(() => { + const selectedRows = table.getFilteredSelectedRowModel().rows; + const selectedDocuments = selectedRows.map((row) => row.original); + onSelectionChange(selectedDocuments); + }, [rowSelection, table]); - const handleClearAll = () => setRowSelection({}); + const handleClearAll = () => setRowSelection({}); - const handleSelectPage = () => { - const currentPageRows = table.getRowModel().rows; - const newSelection = { ...rowSelection }; - currentPageRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectPage = () => { + const currentPageRows = table.getRowModel().rows; + const newSelection = { ...rowSelection }; + currentPageRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const handleSelectAllFiltered = () => { - const allFilteredRows = table.getFilteredRowModel().rows; - const newSelection: Record = {}; - allFilteredRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectAllFiltered = () => { + const allFilteredRows = table.getFilteredRowModel().rows; + const newSelection: Record = {}; + allFilteredRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const selectedCount = table.getFilteredSelectedRowModel().rows.length; - const totalFiltered = table.getFilteredRowModel().rows.length; + const selectedCount = table.getFilteredSelectedRowModel().rows.length; + const totalFiltered = table.getFilteredRowModel().rows.length; + return ( +
+ {/* Header Controls */} +
+ {/* Search and Filter Row */} +
+
+ + + table.getColumn("title")?.setFilterValue(event.target.value) + } + className="pl-10 text-sm" + /> +
+ +
- return ( -
- {/* Header Controls */} -
- {/* Search and Filter Row */} -
-
- - table.getColumn("title")?.setFilterValue(event.target.value)} - className="pl-10 text-sm" - /> -
- -
+ {/* Action Controls Row */} +
+
+ + {selectedCount} of {totalFiltered} selected + +
+
+ + + + +
+
+ +
+
- {/* Action Controls Row */} -
-
- - {selectedCount} of {totalFiltered} selected - -
-
- - - - -
-
- -
-
+ {/* Table Container */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No documents found. + + + )} + +
+
+
- {/* Table Container */} -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No documents found. - - - )} - -
-
-
- - {/* Footer Pagination */} -
-
- Showing {pageIndex * pageSize + 1} to{" "} - {Math.min((pageIndex + 1) * pageSize, pageIndex * pageSize + documents.length)} of{" "} - {canNext - ? `${(pageIndex + 1) * pageSize}+` - : `${pageIndex * pageSize + documents.length}`}{" "} - documents -
-
- -
- Page - {pageIndex + 1} - {canNext && +} -
- -
-
-
- ); + {/* Footer Pagination */} +
+
+ Showing {pageIndex * pageSize + 1} to{" "} + {Math.min( + (pageIndex + 1) * pageSize, + pageIndex * pageSize + documents.length + )}{" "} + of{" "} + {canNext + ? `${(pageIndex + 1) * pageSize}+` + : `${pageIndex * pageSize + documents.length}`}{" "} + documents +
+
+ +
+ Page + {pageIndex + 1} + {canNext && +} +
+ +
+
+
+ ); }