"use client"; import { useAtom } from "jotai"; import { TreePine } from "lucide-react"; import { useCallback, useMemo } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { renamingFolderIdAtom } from "@/atoms/documents/folder.atoms"; import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import { DocumentNode, type DocumentNodeDoc } from "./DocumentNode"; import { FolderNode, type FolderDisplay } from "./FolderNode"; interface FolderTreeViewProps { folders: FolderDisplay[]; documents: DocumentNodeDoc[]; expandedIds: Set; onToggleExpand: (folderId: number) => void; mentionedDocIds: Set; onToggleChatMention: (doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => void; onRenameFolder: (folder: FolderDisplay, newName: string) => void; onDeleteFolder: (folder: FolderDisplay) => void; onMoveFolder: (folder: FolderDisplay) => void; onCreateFolder: (parentId: number | null) => void; onPreviewDocument: (doc: DocumentNodeDoc) => void; onEditDocument: (doc: DocumentNodeDoc) => void; onDeleteDocument: (doc: DocumentNodeDoc) => void; onMoveDocument: (doc: DocumentNodeDoc) => void; activeTypes: DocumentTypeEnum[]; onDropIntoFolder?: (itemType: "folder" | "document", itemId: number, targetFolderId: number | null) => void; onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void; } function groupBy(items: T[], keyFn: (item: T) => string | number): Record { const result: Record = {}; for (const item of items) { const key = keyFn(item); (result[key] ??= []).push(item); } return result; } export function FolderTreeView({ folders, documents, expandedIds, onToggleExpand, mentionedDocIds, onToggleChatMention, onRenameFolder, onDeleteFolder, onMoveFolder, onCreateFolder, onPreviewDocument, onEditDocument, onDeleteDocument, onMoveDocument, activeTypes, onDropIntoFolder, onReorderFolder, }: FolderTreeViewProps) { const foldersByParent = useMemo( () => groupBy(folders, (f) => f.parentId ?? "root"), [folders], ); const docsByFolder = useMemo( () => groupBy(documents, (d) => d.folderId ?? "root"), [documents], ); const folderChildCounts = useMemo(() => { const counts: Record = {}; for (const f of folders) { const children = foldersByParent[f.id] ?? []; const docs = docsByFolder[f.id] ?? []; counts[f.id] = children.length + docs.length; } return counts; }, [folders, foldersByParent, docsByFolder]); // Single subscription for rename state — derived boolean passed to each FolderNode const [renamingFolderId, setRenamingFolderId] = useAtom(renamingFolderIdAtom); const handleStartRename = useCallback( (folderId: number) => setRenamingFolderId(folderId), [setRenamingFolderId], ); const handleCancelRename = useCallback( () => setRenamingFolderId(null), [setRenamingFolderId], ); const hasDescendantMatch = useMemo(() => { if (activeTypes.length === 0) return null; const match: Record = {}; function check(folderId: number): boolean { if (match[folderId] !== undefined) return match[folderId]; const childDocs = (docsByFolder[folderId] ?? []).some((d) => activeTypes.includes(d.document_type as DocumentTypeEnum), ); if (childDocs) { match[folderId] = true; return true; } const childFolders = foldersByParent[folderId] ?? []; for (const cf of childFolders) { if (check(cf.id)) { match[folderId] = true; return true; } } match[folderId] = false; return false; } for (const f of folders) { check(f.id); } return match; }, [folders, docsByFolder, foldersByParent, activeTypes]); function renderLevel(parentId: number | null, depth: number): React.ReactNode[] { const key = parentId ?? "root"; const childFolders = (foldersByParent[key] ?? []) .slice() .sort((a, b) => a.position.localeCompare(b.position)); const visibleFolders = hasDescendantMatch ? childFolders.filter((f) => hasDescendantMatch[f.id]) : childFolders; const childDocs = (docsByFolder[key] ?? []) .filter( (d) => activeTypes.length === 0 || activeTypes.includes(d.document_type as DocumentTypeEnum), ); const nodes: React.ReactNode[] = []; for (let i = 0; i < visibleFolders.length; i++) { const f = visibleFolders[i]; const siblingPositions = { before: i > 0 ? visibleFolders[i - 1].position : null, after: i < visibleFolders.length - 1 ? visibleFolders[i + 1].position : null, }; nodes.push( , ); if (expandedIds.has(f.id)) { nodes.push(...renderLevel(f.id, depth + 1)); } } for (const d of childDocs) { nodes.push( , ); } return nodes; } const treeNodes = renderLevel(null, 0); if (treeNodes.length === 0 && folders.length === 0 && documents.length === 0) { return (

No documents yet

); } if (treeNodes.length === 0 && activeTypes.length > 0) { return (

No matching documents

); } return (
{treeNodes}
); }