From 878e829bdc8da815a075598082202f0a751306b0 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:19:29 +0530 Subject: [PATCH] feat: enhance document filters and table components with search functionality and improved loading states --- .../(manage)/components/DocumentTypeIcon.tsx | 10 +- .../(manage)/components/DocumentsFilters.tsx | 255 ++++++++------ .../components/DocumentsTableShell.tsx | 321 +++++++++++++----- .../(manage)/components/RowActions.tsx | 161 ++++----- 4 files changed, 480 insertions(+), 267 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx index 246cff1c0..b5d434e92 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx @@ -4,8 +4,8 @@ import type React from "react"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -export function getDocumentTypeIcon(type: string): React.ReactNode { - return getConnectorIcon(type); +export function getDocumentTypeIcon(type: string, className?: string): React.ReactNode { + return getConnectorIcon(type, className); } export function getDocumentTypeLabel(type: string): string { @@ -18,7 +18,7 @@ export function getDocumentTypeLabel(type: string): string { const MAX_LABEL_LENGTH = 28; export function DocumentTypeChip({ type, className }: { type: string; className?: string }) { - const icon = getDocumentTypeIcon(type); + const icon = getDocumentTypeIcon(type, "h-4 w-4"); const fullLabel = getDocumentTypeLabel(type); const truncatedLabel = fullLabel.length > MAX_LABEL_LENGTH ? `${fullLabel.slice(0, MAX_LABEL_LENGTH)}...` @@ -27,9 +27,9 @@ export function DocumentTypeChip({ type, className }: { type: string; className? const chip = ( - {icon} + {icon} {truncatedLabel} ); diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx index 87d349e38..2c3dc7eef 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx @@ -7,12 +7,14 @@ import { Columns3, FilePlus2, FileType, + ListFilter, + Search, SlidersHorizontal, Trash, } from "lucide-react"; import { motion } from "motion/react"; import { useTranslations } from "next-intl"; -import React, { useMemo, useRef } from "react"; +import React, { useMemo, useRef, useState } from "react"; import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { @@ -64,10 +66,20 @@ export function DocumentsFilters({ const { openDialog: openUploadDialog } = useDocumentUploadDialog(); const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom); + const [typeSearchQuery, setTypeSearchQuery] = useState(""); + const uniqueTypes = useMemo(() => { return Object.keys(typeCountsRecord).sort() as DocumentTypeEnum[]; }, [typeCountsRecord]); + const filteredTypes = useMemo(() => { + if (!typeSearchQuery.trim()) return uniqueTypes; + const query = typeSearchQuery.toLowerCase(); + return uniqueTypes.filter((type) => + getDocumentTypeLabel(type).toLowerCase().includes(query) + ); + }, [uniqueTypes, typeSearchQuery]); + const typeCounts = useMemo(() => { const map = new Map(); for (const [type, count] of Object.entries(typeCountsRecord)) { @@ -117,10 +129,13 @@ export function DocumentsFilters({ animate={{ opacity: 1, y: 0 }} transition={{ type: "spring", stiffness: 300, damping: 30 }} > +
+
onSearch(e.target.value)} placeholder="Filter by title" @@ -148,74 +163,94 @@ export function DocumentsFilters({ {/* Filter Buttons Group */}
- {/* Type Filter */} - - - - - -
-
- Filter by source + {/* Type Filter */} + + + + + +
+ {/* Search input */} +
+
+ + setTypeSearchQuery(e.target.value)} + className="h-6 pl-6 text-sm bg-transparent border-0 focus-visible:ring-0" + />
-
- {uniqueTypes.map((value: DocumentTypeEnum, i) => ( +
+ +
+ {filteredTypes.length === 0 ? ( +
+ No types found +
+ ) : ( + filteredTypes.map((value: DocumentTypeEnum, i) => ( - ))} -
- {activeTypes.length > 0 && ( -
- -
+ )) )}
- - + {activeTypes.length > 0 && ( +
+ +
+ )} +
+
+
{/* View/Columns Popover */} @@ -266,57 +301,69 @@ export function DocumentsFilters({
-
- {/* Bulk Delete Button */} - {selectedIds.size > 0 && ( - - - - - - - -
- - - Cancel - - Delete - - - - - )} + + Cancel + + Delete + + + + + )} +
); diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index faa7605a3..f23893fbe 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -1,16 +1,17 @@ "use client"; -import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react"; +import { formatDistanceToNow } from "date-fns"; +import { Calendar, ChevronDown, ChevronUp, FileText, FileX, Link2, Plus, User } from "lucide-react"; import { motion } from "motion/react"; import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import React, { useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { DocumentViewer } from "@/components/document-viewer"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { Spinner } from "@/components/ui/spinner"; +import { Skeleton } from "@/components/ui/skeleton"; import { Table, TableBody, @@ -37,35 +38,82 @@ function sortDocuments(docs: Document[], key: SortKey, desc: boolean): Document[ return desc ? sorted.reverse() : sorted; } -function formatDate(dateStr: string): string { +function formatRelativeDate(dateStr: string): string { + return formatDistanceToNow(new Date(dateStr), { addSuffix: true }); +} + +function formatAbsoluteDate(dateStr: string): string { const date = new Date(dateStr); - return date.toLocaleDateString("en-US", { + return date.toLocaleString("en-US", { year: "numeric", month: "long", day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, }); } +function TruncatedText({ text, className }: { text: string; className?: string }) { + const textRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + const checkTruncation = () => { + if (textRef.current) { + setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth); + } + }; + checkTruncation(); + window.addEventListener("resize", checkTruncation); + return () => window.removeEventListener("resize", checkTruncation); + }, []); + + if (isTruncated) { + return ( + + + + {text} + + + +

{text}

+
+
+ ); + } + + return ( + + {text} + + ); +} + function SortableHeader({ children, sortKey, currentSortKey, sortDesc, onSort, + icon, }: { children: React.ReactNode; sortKey: SortKey; currentSortKey: SortKey; sortDesc: boolean; onSort: (key: SortKey) => void; + icon?: React.ReactNode; }) { const isActive = currentSortKey === sortKey; return (