mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
refactor: enhance DocumentsFilters and DocumentsTableShell components by replacing icons for improved clarity and optimizing loading state management in useDocuments hook
This commit is contained in:
parent
dfe483efcb
commit
b7ca656823
3 changed files with 164 additions and 187 deletions
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
FileType,
|
|
||||||
ListFilter,
|
ListFilter,
|
||||||
Search,
|
Search,
|
||||||
Trash,
|
Trash,
|
||||||
|
|
@ -90,7 +89,7 @@ export function DocumentsFilters({
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-9 w-9 shrink-0 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
|
className="h-9 w-9 shrink-0 border-dashed border-border/60 text-muted-foreground hover:text-foreground hover:border-border"
|
||||||
>
|
>
|
||||||
<FileType size={14} />
|
<ListFilter size={14} />
|
||||||
{activeTypes.length > 0 && (
|
{activeTypes.length > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[9px] font-medium text-primary-foreground">
|
<span className="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[9px] font-medium text-primary-foreground">
|
||||||
{activeTypes.length}
|
{activeTypes.length}
|
||||||
|
|
@ -174,7 +173,7 @@ export function DocumentsFilters({
|
||||||
{/* Search Input */}
|
{/* Search Input */}
|
||||||
<div className="relative flex-1 min-w-0">
|
<div className="relative flex-1 min-w-0">
|
||||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground">
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground">
|
||||||
<ListFilter size={14} aria-hidden="true" />
|
<Search size={14} aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id={`${id}-input`}
|
id={`${id}-input`}
|
||||||
|
|
|
||||||
|
|
@ -204,9 +204,9 @@ function SortableHeader({
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSort(sortKey)}
|
onClick={() => onSort(sortKey)}
|
||||||
className="flex items-center gap-1.5 text-left text-sm font-medium text-muted-foreground/70 hover:text-muted-foreground transition-colors group"
|
className="flex items-center gap-1.5 text-left text-sm font-medium text-muted-foreground hover:text-muted-foreground transition-colors group"
|
||||||
>
|
>
|
||||||
{icon && <span className="opacity-60">{icon}</span>}
|
{icon && <span>{icon}</span>}
|
||||||
{children}
|
{children}
|
||||||
<span
|
<span
|
||||||
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}
|
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}
|
||||||
|
|
@ -447,76 +447,73 @@ export function DocumentsTableShell({
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.2 }}
|
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 ? (
|
{loading ? (
|
||||||
<>
|
<div className="flex-1 overflow-auto">
|
||||||
{/* Desktop Skeleton */}
|
<Table className="table-fixed w-full">
|
||||||
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
|
<TableBody>
|
||||||
<Table className="table-fixed w-full">
|
{[65, 80, 45, 72, 55, 88, 40, 60, 50, 75].map((widthPercent) => (
|
||||||
<TableHeader>
|
<TableRow
|
||||||
<TableRow className="hover:bg-transparent border-b border-border/50">
|
key={`skeleton-${widthPercent}`}
|
||||||
<TableHead className="w-10 pl-3 pr-0 text-center h-8">
|
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">
|
<div className="flex items-center justify-center h-full">
|
||||||
<Skeleton className="h-4 w-4 rounded" />
|
<Skeleton className="h-4 w-4 rounded" />
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableCell>
|
||||||
<TableHead className="h-8 px-2">
|
<TableCell className="px-2 py-1.5 max-w-0">
|
||||||
<Skeleton className="h-3 w-20" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-10 text-center h-8 px-0">
|
|
||||||
<Skeleton className="h-3 w-4 mx-auto" />
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
|
|
||||||
<Skeleton className="h-3 w-8 mx-auto" />
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
</Table>
|
|
||||||
<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>
|
|
||||||
{/* Mobile Skeleton */}
|
|
||||||
<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}%` }} />
|
<Skeleton className="h-4" style={{ width: `${widthPercent}%` }} />
|
||||||
</div>
|
</TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<TableCell className="w-10 px-0 py-1.5 text-center">
|
||||||
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
<Skeleton className="h-4 w-4 mx-auto rounded" />
|
||||||
<Skeleton className="h-5 w-5 rounded-full shrink-0" />
|
</TableCell>
|
||||||
</div>
|
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
|
||||||
</div>
|
<Skeleton className="h-5 w-5 mx-auto rounded-full" />
|
||||||
</div>
|
</TableCell>
|
||||||
))}
|
</TableRow>
|
||||||
</div>
|
))}
|
||||||
</>
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="flex flex-1 w-full items-center justify-center">
|
<div className="flex flex-1 w-full items-center justify-center">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
|
@ -548,49 +545,9 @@ export function DocumentsTableShell({
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div ref={desktopScrollRef} className="flex-1 overflow-auto">
|
||||||
{/* Desktop Table View */}
|
<Table className="table-fixed w-full">
|
||||||
<div className="hidden md:flex md:flex-col flex-1 min-h-0">
|
<TableBody>
|
||||||
<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/70" />
|
|
||||||
</span>
|
|
||||||
</TableHead>
|
|
||||||
<TableHead className="w-12 text-center h-8 pl-0 pr-3">
|
|
||||||
<span className="text-xs font-medium text-muted-foreground/70">
|
|
||||||
Status
|
|
||||||
</span>
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
</Table>
|
|
||||||
<div ref={desktopScrollRef} className="flex-1 overflow-auto">
|
|
||||||
<Table className="table-fixed w-full">
|
|
||||||
<TableBody>
|
|
||||||
{sorted.map((doc, index) => {
|
{sorted.map((doc, index) => {
|
||||||
const isSelected = selectedIds.has(doc.id);
|
const isSelected = selectedIds.has(doc.id);
|
||||||
const canSelect = isSelectable(doc);
|
const canSelect = isSelectable(doc);
|
||||||
|
|
@ -659,80 +616,98 @@ export function DocumentsTableShell({
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
{hasMore && (
|
|
||||||
<div ref={desktopSentinelRef} className="py-3" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Card View */}
|
|
||||||
<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 && (
|
{hasMore && (
|
||||||
<div ref={mobileSentinelRef} className="py-3" />
|
<div ref={desktopSentinelRef} className="py-3" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Document Content Viewer */}
|
||||||
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||||
|
|
|
||||||
|
|
@ -156,10 +156,13 @@ export function useDocuments(
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
setLoading(true);
|
const isRefresh = initialLoadDoneRef.current;
|
||||||
setDocuments([]);
|
if (!isRefresh) {
|
||||||
setTotal(0);
|
setLoading(true);
|
||||||
setHasMore(false);
|
setDocuments([]);
|
||||||
|
setTotal(0);
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
apiLoadedCountRef.current = 0;
|
apiLoadedCountRef.current = 0;
|
||||||
initialLoadDoneRef.current = false;
|
initialLoadDoneRef.current = false;
|
||||||
electricBaselineIdsRef.current = null;
|
electricBaselineIdsRef.current = null;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue