refactor: simplify DocumentsTableShell and DocumentsSidebar components by removing unused column visibility state and optimizing document loading logic in useDocuments hook

This commit is contained in:
Anish Sarkar 2026-03-06 12:12:03 +05:30
parent b7ca656823
commit 889af57d3f
3 changed files with 248 additions and 227 deletions

View file

@ -54,7 +54,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getDocumentTypeIcon, getDocumentTypeLabel } from "./DocumentTypeIcon";
import type { ColumnVisibility, Document, DocumentStatus } from "./types";
import type { Document, DocumentStatus } from "./types";
const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const;
const NON_DELETABLE_DOCUMENT_TYPES = ["SURFSENSE_DOCS"] as const;
@ -288,7 +288,6 @@ export function DocumentsTableShell({
error,
selectedIds,
setSelectedIds,
columnVisibility: _columnVisibility,
sortKey,
sortDesc,
onSortChange,
@ -304,7 +303,6 @@ export function DocumentsTableShell({
error: boolean;
selectedIds: Set<number>;
setSelectedIds: (update: Set<number>) => void;
columnVisibility: ColumnVisibility;
sortKey: SortKey;
sortDesc: boolean;
onSortChange: (key: SortKey) => void;
@ -447,122 +445,122 @@ export function DocumentsTableShell({
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.2 }}
>
{/* Desktop Table View */}
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
<Table className="table-fixed w-full">
<TableHeader>
<TableRow className="hover:bg-transparent border-b border-border/50">
<TableHead className="w-10 pl-3 pr-0 text-center h-8">
<div className="flex items-center justify-center h-full">
<Checkbox
checked={allSelectedOnPage || (someSelectedOnPage && "indeterminate")}
onCheckedChange={(v) => toggleAll(!!v)}
aria-label="Select all"
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
</div>
</TableHead>
<TableHead className="h-8 px-2">
<SortableHeader
sortKey="title"
currentSortKey={sortKey}
sortDesc={sortDesc}
onSort={onSortHeader}
icon={<FileText size={14} className="text-muted-foreground" />}
>
Document
</SortableHeader>
</TableHead>
<TableHead className="w-10 text-center h-8 px-0">
<span className="flex items-center justify-center">
<Network size={14} className="text-muted-foreground" />
</span>
</TableHead>
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
<span className="text-xs font-medium text-muted-foreground">
Status
</span>
</TableHead>
</TableRow>
</TableHeader>
</Table>
{loading ? (
<div className="flex-1 overflow-auto">
<Table className="table-fixed w-full">
<TableBody>
{[65, 80, 45, 72, 55, 88, 40, 60, 50, 75].map((widthPercent) => (
<TableRow
key={`skeleton-${widthPercent}`}
className="border-b border-border/50 hover:bg-transparent"
{/* Desktop Table View */}
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
<Table className="table-fixed w-full">
<TableHeader>
<TableRow className="hover:bg-transparent border-b border-border/50">
<TableHead className="w-10 pl-3 pr-0 text-center h-8">
<div className="flex items-center justify-center h-full">
<Checkbox
checked={allSelectedOnPage || (someSelectedOnPage && "indeterminate")}
onCheckedChange={(v) => toggleAll(!!v)}
aria-label="Select all"
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
/>
</div>
</TableHead>
<TableHead className="h-8 px-2">
<SortableHeader
sortKey="title"
currentSortKey={sortKey}
sortDesc={sortDesc}
onSort={onSortHeader}
icon={<FileText size={14} className="text-muted-foreground" />}
>
<TableCell className="w-10 pl-3 pr-0 py-1.5 text-center">
<div className="flex items-center justify-center h-full">
<Skeleton className="h-4 w-4 rounded" />
</div>
</TableCell>
<TableCell className="px-2 py-1.5 max-w-0">
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
</TableCell>
<TableCell className="w-10 px-0 py-1.5 text-center">
<Skeleton className="h-4 w-4 mx-auto rounded" />
</TableCell>
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
<Skeleton className="h-5 w-5 mx-auto rounded-full" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : error ? (
<div className="flex flex-1 w-full items-center justify-center">
<div className="flex flex-col items-center gap-3">
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="text-sm text-destructive">{t("error_loading")}</p>
Document
</SortableHeader>
</TableHead>
<TableHead className="w-10 text-center h-8 px-0">
<span className="flex items-center justify-center">
<Network size={14} className="text-muted-foreground" />
</span>
</TableHead>
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
<span className="text-xs font-medium text-muted-foreground">
Status
</span>
</TableHead>
</TableRow>
</TableHeader>
</Table>
{loading ? (
<div className="flex-1 overflow-auto">
<Table className="table-fixed w-full">
<TableBody>
{[65, 80, 45, 72, 55, 88, 40, 60, 50, 75].map((widthPercent) => (
<TableRow
key={`skeleton-${widthPercent}`}
className="border-b border-border/50 hover:bg-transparent"
>
<TableCell className="w-10 pl-3 pr-0 py-1.5 text-center">
<div className="flex items-center justify-center h-full">
<Skeleton className="h-4 w-4 rounded" />
</div>
</TableCell>
<TableCell className="px-2 py-1.5 max-w-0">
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
</TableCell>
<TableCell className="w-10 px-0 py-1.5 text-center">
<Skeleton className="h-4 w-4 mx-auto rounded" />
</TableCell>
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
<Skeleton className="h-5 w-5 mx-auto rounded-full" />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
) : sorted.length === 0 ? (
<div className="flex flex-1 w-full items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex flex-col items-center gap-4 max-w-md px-4 text-center"
>
<div className="rounded-full bg-muted/50 p-4">
<FileX className="h-8 w-8 text-muted-foreground" />
) : error ? (
<div className="flex flex-1 w-full items-center justify-center">
<div className="flex flex-col items-center gap-3">
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="text-sm text-destructive">{t("error_loading")}</p>
</div>
<div className="space-y-1.5">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</motion.div>
</div>
) : (
<div ref={desktopScrollRef} className="flex-1 overflow-auto">
<Table className="table-fixed w-full">
<TableBody>
{sorted.map((doc, index) => {
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<RowContextMenu
key={doc.id}
doc={doc}
onPreview={handleViewDocument}
onDelete={setDeleteDoc}
searchSpaceId={searchSpaceId}
>
<motion.tr
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.02 } : { duration: 0 }}
</div>
) : sorted.length === 0 ? (
<div className="flex flex-1 w-full items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex flex-col items-center gap-4 max-w-md px-4 text-center"
>
<div className="rounded-full bg-muted/50 p-4">
<FileX className="h-8 w-8 text-muted-foreground" />
</div>
<div className="space-y-1.5">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</motion.div>
</div>
) : (
<div ref={desktopScrollRef} className="flex-1 overflow-auto">
<Table className="table-fixed w-full">
<TableBody>
{sorted.map((doc, index) => {
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<RowContextMenu
key={doc.id}
doc={doc}
onPreview={handleViewDocument}
onDelete={setDeleteDoc}
searchSpaceId={searchSpaceId}
>
<motion.tr
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.02 } : { duration: 0 }}
className={`border-b border-border/50 transition-colors ${
isSelected
? "bg-primary/5 hover:bg-primary/8"
@ -610,104 +608,134 @@ export function DocumentsTableShell({
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
<StatusIndicator status={doc.status} />
</TableCell>
</motion.tr>
</RowContextMenu>
);
})}
</TableBody>
</Table>
</motion.tr>
</RowContextMenu>
);
})}
</TableBody>
</Table>
{hasMore && (
<div ref={desktopSentinelRef} className="py-3" />
)}
</div>
)}
</div>
{/* Mobile Card View */}
{loading ? (
<div className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
{[70, 85, 55, 78, 62, 90].map((widthPercent) => (
<div key={`skeleton-mobile-${widthPercent}`} className="px-3 py-2">
<div className="flex items-center gap-3">
<Skeleton className="h-4 w-4 rounded shrink-0" />
<div className="flex-1 min-w-0">
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 rounded shrink-0" />
<Skeleton className="h-5 w-5 rounded-full shrink-0" />
</div>
</div>
</div>
))}
</div>
) : error ? (
<div className="md:hidden flex flex-1 w-full items-center justify-center">
<div className="flex flex-col items-center gap-3">
<AlertCircle className="h-8 w-8 text-destructive" />
<p className="text-sm text-destructive">{t("error_loading")}</p>
</div>
</div>
) : sorted.length === 0 ? (
<div className="md:hidden flex flex-1 w-full items-center justify-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="flex flex-col items-center gap-4 max-w-md px-4 text-center"
>
<div className="rounded-full bg-muted/50 p-4">
<FileX className="h-8 w-8 text-muted-foreground" />
</div>
<div className="space-y-1.5">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</motion.div>
</div>
) : (
<div ref={mobileScrollRef} className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
{sorted.map((doc, index) => {
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<RowContextMenu
key={doc.id}
doc={doc}
onPreview={handleViewDocument}
onDelete={setDeleteDoc}
searchSpaceId={searchSpaceId}
>
<motion.div
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.03 } : { duration: 0 }}
className={`px-3 py-2 transition-colors ${
isSelected ? "bg-primary/5" : "hover:bg-muted/20"
}`}
>
<div className="flex items-center gap-3">
<Checkbox
checked={isSelected}
onCheckedChange={(v) =>
canSelect && toggleOne(doc.id, !!v)
}
disabled={!canSelect}
aria-label={
canSelect
? "Select row"
: "Cannot select while processing"
}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
/>
<div className="flex-1 min-w-0">
<DocumentNameTooltip
doc={doc}
className="truncate block text-sm text-foreground cursor-default"
/>
</div>
<div className="flex items-center gap-2 shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center justify-center">
{getDocumentTypeIcon(
doc.document_type,
"h-4 w-4"
)}
</span>
</TooltipTrigger>
<TooltipContent side="top">
{getDocumentTypeLabel(doc.document_type)}
</TooltipContent>
</Tooltip>
<StatusIndicator status={doc.status} />
</div>
</div>
</motion.div>
</RowContextMenu>
);
})}
{hasMore && (
<div ref={desktopSentinelRef} className="py-3" />
<div ref={mobileSentinelRef} className="py-3" />
)}
</div>
)}
</div>
{/* Mobile Card View */}
{loading ? (
<div className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
{[70, 85, 55, 78, 62, 90].map((widthPercent) => (
<div key={`skeleton-mobile-${widthPercent}`} className="px-3 py-2">
<div className="flex items-center gap-3">
<Skeleton className="h-4 w-4 rounded shrink-0" />
<div className="flex-1 min-w-0">
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 rounded shrink-0" />
<Skeleton className="h-5 w-5 rounded-full shrink-0" />
</div>
</div>
</div>
))}
</div>
) : !error && sorted.length > 0 && (
<div ref={mobileScrollRef} className="md:hidden divide-y divide-border/50 flex-1 overflow-auto">
{sorted.map((doc, index) => {
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<RowContextMenu
key={doc.id}
doc={doc}
onPreview={handleViewDocument}
onDelete={setDeleteDoc}
searchSpaceId={searchSpaceId}
>
<motion.div
initial={!isSearchMode && index < 20 ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={!isSearchMode && index < 20 ? { duration: 0.15, delay: index * 0.03 } : { duration: 0 }}
className={`px-3 py-2 transition-colors ${
isSelected ? "bg-primary/5" : "hover:bg-muted/20"
}`}
>
<div className="flex items-center gap-3">
<Checkbox
checked={isSelected}
onCheckedChange={(v) =>
canSelect && toggleOne(doc.id, !!v)
}
disabled={!canSelect}
aria-label={
canSelect
? "Select row"
: "Cannot select while processing"
}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
/>
<div className="flex-1 min-w-0">
<DocumentNameTooltip
doc={doc}
className="truncate block text-sm text-foreground cursor-default"
/>
</div>
<div className="flex items-center gap-2 shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center justify-center">
{getDocumentTypeIcon(
doc.document_type,
"h-4 w-4"
)}
</span>
</TooltipTrigger>
<TooltipContent side="top">
{getDocumentTypeLabel(doc.document_type)}
</TooltipContent>
</Tooltip>
<StatusIndicator status={doc.status} />
</div>
</div>
</motion.div>
</RowContextMenu>
);
})}
{hasMore && (
<div ref={mobileSentinelRef} className="py-3" />
)}
</div>
)}
{/* Document Content Viewer */}
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>

View file

@ -19,7 +19,6 @@ import {
DocumentsTableShell,
type SortKey,
} from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell";
import type { ColumnVisibility } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/types";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
const SEARCH_INITIAL_SIZE = 20;
@ -49,12 +48,6 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
const [search, setSearch] = useState("");
const debouncedSearch = useDebounced(search, 250);
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibility>({
document_type: true,
created_by: false,
created_at: true,
status: true,
});
const [sortKey, setSortKey] = useState<SortKey>("created_at");
const [sortDesc, setSortDesc] = useState(true);
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
@ -288,13 +281,6 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
});
}, []);
useEffect(() => {
if (!open) return;
const panelWidth = isMobile ? window.innerWidth : 720;
const isNarrow = panelWidth < 600;
setColumnVisibility((prev) => ({ ...prev, created_by: !isNarrow, created_at: !isNarrow }));
}, [open, isMobile]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
@ -345,7 +331,6 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
error={!!error}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
columnVisibility={columnVisibility}
sortKey={sortKey}
sortDesc={sortDesc}
onSortChange={handleSortChange}

View file

@ -77,6 +77,7 @@ export function useDocuments(
const apiLoadedCountRef = useRef(0);
const initialLoadDoneRef = useRef(false);
const prevParamsRef = useRef<{ sortBy: string; sortOrder: string; typeFilterKey: string } | null>(null);
// Snapshot of all doc IDs from Electric's first callback after initial load.
// Anything appearing in subsequent callbacks NOT in this set is genuinely new.
const electricBaselineIdsRef = useRef<Set<number> | null>(null);
@ -156,8 +157,15 @@ export function useDocuments(
let cancelled = false;
const isRefresh = initialLoadDoneRef.current;
if (!isRefresh) {
const prev = prevParamsRef.current;
const isSortOnlyChange =
initialLoadDoneRef.current &&
prev !== null &&
prev.typeFilterKey === typeFilterKey &&
(prev.sortBy !== sortBy || prev.sortOrder !== sortOrder);
prevParamsRef.current = { sortBy, sortOrder, typeFilterKey };
if (!isSortOnlyChange) {
setLoading(true);
setDocuments([]);
setTotal(0);