2026-03-27 01:39:15 -07:00
"use client" ;
2026-03-28 16:39:46 -07:00
import {
AlertCircle ,
Clock ,
Download ,
Eye ,
MoreHorizontal ,
Move ,
PenLine ,
Trash2 ,
} from "lucide-react" ;
2026-03-28 02:32:03 +05:30
import React , { useCallback , useRef , useState } from "react" ;
2026-03-27 01:39:15 -07:00
import { useDrag } from "react-dnd" ;
2026-03-27 03:17:05 -07:00
import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon" ;
2026-03-28 02:58:38 +05:30
import { ExportContextItems , ExportDropdownItems } from "@/components/shared/ExportMenuItems" ;
2026-03-27 01:39:15 -07:00
import { Button } from "@/components/ui/button" ;
import { Checkbox } from "@/components/ui/checkbox" ;
import {
ContextMenu ,
ContextMenuContent ,
ContextMenuItem ,
2026-03-28 02:58:38 +05:30
ContextMenuSub ,
ContextMenuSubContent ,
ContextMenuSubTrigger ,
2026-03-27 01:39:15 -07:00
ContextMenuTrigger ,
} from "@/components/ui/context-menu" ;
import {
DropdownMenu ,
DropdownMenuContent ,
DropdownMenuItem ,
2026-03-28 02:58:38 +05:30
DropdownMenuSub ,
DropdownMenuSubContent ,
DropdownMenuSubTrigger ,
2026-03-27 01:39:15 -07:00
DropdownMenuTrigger ,
} from "@/components/ui/dropdown-menu" ;
2026-03-28 00:11:32 +05:30
import { Spinner } from "@/components/ui/spinner" ;
import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
2026-03-27 01:39:15 -07:00
import type { DocumentTypeEnum } from "@/contracts/types/document.types" ;
import { cn } from "@/lib/utils" ;
import { DND_TYPES } from "./FolderNode" ;
2026-03-29 22:29:40 +05:30
const EDITABLE_DOCUMENT_TYPES = new Set ( [ "FILE" , "NOTE" ] ) ;
2026-03-27 01:39:15 -07:00
export interface DocumentNodeDoc {
id : number ;
title : string ;
document_type : string ;
folderId : number | null ;
status ? : { state : string ; reason? : string | null } ;
}
interface DocumentNodeProps {
doc : DocumentNodeDoc ;
depth : number ;
isMentioned : boolean ;
onToggleChatMention : ( doc : DocumentNodeDoc , isMentioned : boolean ) = > void ;
onPreview : ( doc : DocumentNodeDoc ) = > void ;
onEdit : ( doc : DocumentNodeDoc ) = > void ;
onDelete : ( doc : DocumentNodeDoc ) = > void ;
onMove : ( doc : DocumentNodeDoc ) = > void ;
2026-03-28 02:58:38 +05:30
onExport ? : ( doc : DocumentNodeDoc , format : string ) = > void ;
2026-03-27 23:14:10 +05:30
contextMenuOpen? : boolean ;
onContextMenuOpenChange ? : ( open : boolean ) = > void ;
2026-03-27 01:39:15 -07:00
}
export const DocumentNode = React . memo ( function DocumentNode ( {
doc ,
depth ,
isMentioned ,
onToggleChatMention ,
onPreview ,
onEdit ,
onDelete ,
onMove ,
2026-03-28 02:58:38 +05:30
onExport ,
2026-03-27 23:14:10 +05:30
contextMenuOpen ,
onContextMenuOpenChange ,
2026-03-27 01:39:15 -07:00
} : DocumentNodeProps ) {
const statusState = doc . status ? . state ? ? "ready" ;
const isSelectable = statusState !== "pending" && statusState !== "processing" ;
const isEditable =
2026-03-29 22:29:40 +05:30
EDITABLE_DOCUMENT_TYPES . has ( doc . document_type ) &&
statusState !== "pending" &&
statusState !== "processing" ;
2026-03-27 01:39:15 -07:00
const handleCheckChange = useCallback ( ( ) = > {
if ( isSelectable ) {
onToggleChatMention ( doc , isMentioned ) ;
}
} , [ doc , isMentioned , isSelectable , onToggleChatMention ] ) ;
const [ { isDragging } , drag ] = useDrag (
( ) = > ( {
type : DND_TYPES . DOCUMENT ,
item : { id : doc.id } ,
collect : ( monitor ) = > ( { isDragging : monitor.isDragging ( ) } ) ,
} ) ,
2026-03-27 03:17:05 -07:00
[ doc . id ]
2026-03-27 01:39:15 -07:00
) ;
const isProcessing = statusState === "pending" || statusState === "processing" ;
2026-03-28 02:32:03 +05:30
const [ dropdownOpen , setDropdownOpen ] = useState ( false ) ;
2026-03-28 02:58:38 +05:30
const [ exporting , setExporting ] = useState < string | null > ( null ) ;
2026-03-28 12:18:57 +05:30
const rowRef = useRef < HTMLDivElement > ( null ) ;
2026-03-28 00:11:32 +05:30
2026-03-28 02:58:38 +05:30
const handleExport = useCallback (
( format : string ) = > {
if ( ! onExport ) return ;
setExporting ( format ) ;
onExport ( doc , format ) ;
setTimeout ( ( ) = > setExporting ( null ) , 2000 ) ;
} ,
[ doc , onExport ]
) ;
2026-03-28 00:11:32 +05:30
const attachRef = useCallback (
2026-03-28 12:18:57 +05:30
( node : HTMLDivElement | null ) = > {
( rowRef as React . MutableRefObject < HTMLDivElement | null > ) . current = node ;
2026-03-28 00:11:32 +05:30
drag ( node ) ;
} ,
[ drag ]
) ;
2026-03-27 01:39:15 -07:00
return (
2026-03-27 23:14:10 +05:30
< ContextMenu onOpenChange = { onContextMenuOpenChange } >
2026-03-27 01:39:15 -07:00
< ContextMenuTrigger asChild >
2026-03-28 16:39:46 -07:00
{ /* biome-ignore lint/a11y/useSemanticElements: contains nested interactive children (Checkbox) that render as <button>, making a semantic <button> wrapper invalid */ }
2026-03-28 12:18:57 +05:30
< div
role = "button"
tabIndex = { 0 }
2026-03-28 00:11:32 +05:30
ref = { attachRef }
2026-03-27 01:39:15 -07:00
className = { cn (
2026-03-28 16:39:46 -07:00
"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" ,
isDragging && "opacity-40"
2026-03-27 01:39:15 -07:00
) }
style = { { paddingLeft : ` ${ depth * 16 + 4 } px ` } }
onClick = { handleCheckChange }
2026-03-28 12:18:57 +05:30
onKeyDown = { ( e ) = > {
if ( e . key === "Enter" || e . key === " " ) {
e . preventDefault ( ) ;
handleCheckChange ( ) ;
}
} }
2026-03-27 01:39:15 -07:00
>
2026-03-28 16:39:46 -07:00
{ ( ( ) = > {
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 >
2026-03-31 21:29:46 -07:00
< TooltipContent side = "top" > Pending — waiting to be synced < / TooltipContent >
2026-03-28 16:39:46 -07:00
< / 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 >
) ;
}
2026-03-28 00:11:32 +05:30
return (
2026-03-28 16:39:46 -07:00
< Checkbox
checked = { isMentioned }
onCheckedChange = { handleCheckChange }
onClick = { ( e ) = > e . stopPropagation ( ) }
className = "h-3.5 w-3.5 shrink-0"
/ >
2026-03-28 00:11:32 +05:30
) ;
2026-03-28 16:39:46 -07:00
} ) ( ) }
2026-03-27 01:39:15 -07:00
< span className = "flex-1 min-w-0 truncate" > { doc . title } < / span >
< span className = "shrink-0" >
2026-03-27 03:17:05 -07:00
{ getDocumentTypeIcon (
doc . document_type as DocumentTypeEnum ,
"h-3.5 w-3.5 text-muted-foreground"
) }
2026-03-27 01:39:15 -07:00
< / span >
2026-03-28 16:39:46 -07:00
< DropdownMenu open = { dropdownOpen } onOpenChange = { setDropdownOpen } >
< DropdownMenuTrigger asChild >
< Button
variant = "ghost"
size = "icon"
className = { cn (
"hidden sm:inline-flex h-6 w-6 shrink-0 hover:bg-transparent" ,
dropdownOpen
? "opacity-100 bg-accent hover:bg-accent"
: "opacity-0 group-hover:opacity-100"
) }
onClick = { ( e ) = > e . stopPropagation ( ) }
>
2026-03-27 01:39:15 -07:00
< MoreHorizontal className = "h-3.5 w-3.5" / >
< / Button >
< / DropdownMenuTrigger >
2026-03-29 04:20:22 +05:30
< DropdownMenuContent align = "end" className = "w-40" onClick = { ( e ) = > e . stopPropagation ( ) } >
2026-04-01 20:31:45 +05:30
< DropdownMenuItem onClick = { ( ) = > onPreview ( doc ) } disabled = { isProcessing } >
2026-03-27 03:17:05 -07:00
< Eye className = "mr-2 h-4 w-4" / >
Open
< / DropdownMenuItem >
2026-03-27 01:39:15 -07:00
{ isEditable && (
< DropdownMenuItem onClick = { ( ) = > onEdit ( doc ) } >
2026-03-27 23:26:12 +05:30
< PenLine className = "mr-2 h-4 w-4" / >
2026-03-27 01:39:15 -07:00
Edit
< / DropdownMenuItem >
) }
< DropdownMenuItem onClick = { ( ) = > onMove ( doc ) } >
< Move className = "mr-2 h-4 w-4" / >
Move to . . .
< / DropdownMenuItem >
2026-03-28 02:58:38 +05:30
{ onExport && (
< DropdownMenuSub >
< DropdownMenuSubTrigger >
< Download className = "mr-2 h-4 w-4" / >
Export
< / DropdownMenuSubTrigger >
< DropdownMenuSubContent className = "min-w-[180px]" >
< ExportDropdownItems onExport = { handleExport } exporting = { exporting } / >
< / DropdownMenuSubContent >
< / DropdownMenuSub >
) }
2026-03-27 01:39:15 -07:00
< DropdownMenuItem
className = "text-destructive focus:text-destructive"
disabled = { isProcessing }
onClick = { ( ) = > onDelete ( doc ) }
>
< Trash2 className = "mr-2 h-4 w-4" / >
Delete
< / DropdownMenuItem >
< / DropdownMenuContent >
< / DropdownMenu >
2026-03-28 12:18:57 +05:30
< / div >
2026-03-27 01:39:15 -07:00
< / ContextMenuTrigger >
2026-03-27 23:14:10 +05:30
{ contextMenuOpen && (
2026-03-29 04:20:22 +05:30
< ContextMenuContent className = "w-40" onClick = { ( e ) = > e . stopPropagation ( ) } >
2026-04-01 20:31:45 +05:30
< ContextMenuItem onClick = { ( ) = > onPreview ( doc ) } disabled = { isProcessing } >
2026-03-27 23:14:10 +05:30
< Eye className = "mr-2 h-4 w-4" / >
Open
2026-03-27 01:39:15 -07:00
< / ContextMenuItem >
2026-03-27 23:14:10 +05:30
{ isEditable && (
< ContextMenuItem onClick = { ( ) = > onEdit ( doc ) } >
2026-03-27 23:26:12 +05:30
< PenLine className = "mr-2 h-4 w-4" / >
2026-03-27 23:14:10 +05:30
Edit
< / ContextMenuItem >
) }
< ContextMenuItem onClick = { ( ) = > onMove ( doc ) } >
< Move className = "mr-2 h-4 w-4" / >
Move to . . .
< / ContextMenuItem >
2026-03-28 02:58:38 +05:30
{ onExport && (
< ContextMenuSub >
< ContextMenuSubTrigger >
< Download className = "mr-2 h-4 w-4" / >
Export
< / ContextMenuSubTrigger >
< ContextMenuSubContent className = "min-w-[180px]" >
< ExportContextItems onExport = { handleExport } exporting = { exporting } / >
< / ContextMenuSubContent >
< / ContextMenuSub >
) }
2026-03-27 23:14:10 +05:30
< ContextMenuItem
className = "text-destructive focus:text-destructive"
disabled = { isProcessing }
onClick = { ( ) = > onDelete ( doc ) }
>
< Trash2 className = "mr-2 h-4 w-4" / >
Delete
< / ContextMenuItem >
< / ContextMenuContent >
) }
2026-03-27 01:39:15 -07:00
< / ContextMenu >
) ;
} ) ;