mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +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 {
|
||||
CircleAlert,
|
||||
FileType,
|
||||
ListFilter,
|
||||
Search,
|
||||
Trash,
|
||||
|
|
@ -90,7 +89,7 @@ export function DocumentsFilters({
|
|||
size="icon"
|
||||
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 && (
|
||||
<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}
|
||||
|
|
@ -174,7 +173,7 @@ export function DocumentsFilters({
|
|||
{/* Search Input */}
|
||||
<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">
|
||||
<ListFilter size={14} aria-hidden="true" />
|
||||
<Search size={14} aria-hidden="true" />
|
||||
</div>
|
||||
<Input
|
||||
id={`${id}-input`}
|
||||
|
|
|
|||
|
|
@ -204,9 +204,9 @@ function SortableHeader({
|
|||
<button
|
||||
type="button"
|
||||
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}
|
||||
<span
|
||||
className={`transition-opacity ${isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50"}`}
|
||||
|
|
@ -447,76 +447,73 @@ 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 ? (
|
||||
<>
|
||||
{/* Desktop Skeleton */}
|
||||
<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-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>
|
||||
</TableHead>
|
||||
<TableHead className="h-8 px-2">
|
||||
<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">
|
||||
</TableCell>
|
||||
<TableCell className="px-2 py-1.5 max-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>
|
||||
</>
|
||||
</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">
|
||||
|
|
@ -548,49 +545,9 @@ export function DocumentsTableShell({
|
|||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 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/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>
|
||||
<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);
|
||||
|
|
@ -659,80 +616,98 @@ export function DocumentsTableShell({
|
|||
})}
|
||||
</TableBody>
|
||||
</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 && (
|
||||
<div ref={mobileSentinelRef} className="py-3" />
|
||||
<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 && 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()}>
|
||||
|
|
|
|||
|
|
@ -156,10 +156,13 @@ export function useDocuments(
|
|||
|
||||
let cancelled = false;
|
||||
|
||||
setLoading(true);
|
||||
setDocuments([]);
|
||||
setTotal(0);
|
||||
setHasMore(false);
|
||||
const isRefresh = initialLoadDoneRef.current;
|
||||
if (!isRefresh) {
|
||||
setLoading(true);
|
||||
setDocuments([]);
|
||||
setTotal(0);
|
||||
setHasMore(false);
|
||||
}
|
||||
apiLoadedCountRef.current = 0;
|
||||
initialLoadDoneRef.current = false;
|
||||
electricBaselineIdsRef.current = null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue