feat: enhance DocumentNode component with loading and error indicators

This commit is contained in:
Anish Sarkar 2026-03-28 00:11:32 +05:30
parent 0204ed5363
commit 96549791e6

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { Eye, MoreHorizontal, Move, PenLine, Trash2 } from "lucide-react"; import { AlertCircle, Clock, Eye, MoreHorizontal, Move, PenLine, Trash2 } from "lucide-react";
import React, { useCallback } from "react"; import React, { useCallback, useRef } from "react";
import { useDrag } from "react-dnd"; import { useDrag } from "react-dnd";
import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -18,6 +18,8 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DND_TYPES } from "./FolderNode"; import { DND_TYPES } from "./FolderNode";
@ -76,48 +78,78 @@ export const DocumentNode = React.memo(function DocumentNode({
); );
const isProcessing = statusState === "pending" || statusState === "processing"; const isProcessing = statusState === "pending" || statusState === "processing";
const rowRef = useRef<HTMLButtonElement>(null);
const attachRef = useCallback(
(node: HTMLButtonElement | null) => {
(rowRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;
drag(node);
},
[drag]
);
return ( return (
<ContextMenu onOpenChange={onContextMenuOpenChange}> <ContextMenu onOpenChange={onContextMenuOpenChange}>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag ref */} <button
<div type="button"
ref={drag} ref={attachRef}
role="button"
tabIndex={0}
className={cn( className={cn(
"group flex h-8 items-center gap-2.5 rounded-md px-1 text-sm hover:bg-accent/50 cursor-pointer select-none", "group flex h-8 w-full items-center gap-2.5 rounded-md px-1 text-sm hover:bg-accent/50 cursor-pointer select-none text-left",
isMentioned && "bg-accent/30", isMentioned && "bg-accent/30",
isDragging && "opacity-40" isDragging && "opacity-40"
)} )}
style={{ paddingLeft: `${depth * 16 + 4}px` }} style={{ paddingLeft: `${depth * 16 + 4}px` }}
onClick={handleCheckChange} onClick={handleCheckChange}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleCheckChange();
}
}}
> >
{isSelectable ? ( {(() => {
if (statusState === "pending") {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<Clock className="h-3.5 w-3.5 text-muted-foreground/60" />
</span>
</TooltipTrigger>
<TooltipContent side="top">Pending - waiting to be synced</TooltipContent>
</Tooltip>
);
}
if (statusState === "processing") {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<Spinner size="xs" className="text-primary" />
</span>
</TooltipTrigger>
<TooltipContent side="top">Syncing</TooltipContent>
</Tooltip>
);
}
if (statusState === "failed") {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center">
<AlertCircle className="h-3.5 w-3.5 text-destructive" />
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
{doc.status?.reason || "Processing failed"}
</TooltipContent>
</Tooltip>
);
}
return (
<Checkbox <Checkbox
checked={isMentioned} checked={isMentioned}
onCheckedChange={handleCheckChange} onCheckedChange={handleCheckChange}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="h-3.5 w-3.5 shrink-0" className="h-3.5 w-3.5 shrink-0"
/> />
) : ( );
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center"> })()}
<span
className={cn(
"h-2 w-2 rounded-full",
statusState === "processing" && "animate-pulse bg-amber-500",
statusState === "pending" && "bg-muted-foreground/40",
statusState === "failed" && "bg-destructive"
)}
/>
</span>
)}
<span className="flex-1 min-w-0 truncate">{doc.title}</span> <span className="flex-1 min-w-0 truncate">{doc.title}</span>
@ -164,7 +196,7 @@ export const DocumentNode = React.memo(function DocumentNode({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </button>
</ContextMenuTrigger> </ContextMenuTrigger>
{contextMenuOpen && ( {contextMenuOpen && (