"use client"; import { ChevronDown, ChevronRight, File, FileSpreadsheet, FileText, FolderClosed, FolderOpen, HardDrive, Image, Presentation, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; export interface DriveItem { id: string; name: string; mimeType: string; isFolder: boolean; parents?: string[]; size?: number; iconLink?: string | null; } interface ItemTreeNode { item: DriveItem; children: DriveItem[] | null; isExpanded: boolean; isLoading: boolean; } export interface SelectedFolder { id: string; name: string; } interface DriveFolderTreeProps { fetchItems: (parentId?: string) => Promise<{ items: DriveItem[] }>; selectedFolders: SelectedFolder[]; onSelectFolders: (folders: SelectedFolder[]) => void; selectedFiles?: SelectedFolder[]; onSelectFiles?: (files: SelectedFolder[]) => void; onAuthError?: (message: string) => void; rootLabel?: string; providerName?: string; } function getFileIcon(mimeType?: string, className: string = "h-4 w-4") { const type = mimeType ?? ""; if (type.includes("spreadsheet") || type.includes("excel")) { return ; } if (type.includes("presentation") || type.includes("powerpoint")) { return ; } if (type.includes("document") || type.includes("word") || type.includes("text")) { return ; } if (type.includes("image")) { return ; } return ; } export function DriveFolderTree({ fetchItems, selectedFolders, onSelectFolders, selectedFiles = [], onSelectFiles = () => {}, onAuthError, rootLabel = "My Drive", providerName = "Drive", }: DriveFolderTreeProps) { const [itemStates, setItemStates] = useState>(new Map()); const [rootItems, setRootItems] = useState([]); const [isLoadingRoot, setIsLoadingRoot] = useState(true); const [rootError, setRootError] = useState(null); useEffect(() => { let cancelled = false; setIsLoadingRoot(true); setRootError(null); fetchItems() .then((data) => { if (!cancelled) { setRootItems(data.items || []); setIsLoadingRoot(false); } }) .catch((err) => { if (!cancelled) { const error = err instanceof Error ? err : new Error(String(err)); setRootError(error); setIsLoadingRoot(false); if (onAuthError) { const msg = error.message; if ( msg.toLowerCase().includes("authentication expired") || msg.toLowerCase().includes("re-authenticate") ) { onAuthError(msg); } } } }); return () => { cancelled = true; }; }, [fetchItems, onAuthError]); const isFolderSelected = (folderId: string): boolean => { return selectedFolders.some((f) => f.id === folderId); }; const isFileSelected = (fileId: string): boolean => { return selectedFiles.some((f) => f.id === fileId); }; const toggleFolderSelection = (folderId: string, folderName: string) => { if (isFolderSelected(folderId)) { onSelectFolders(selectedFolders.filter((f) => f.id !== folderId)); } else { onSelectFolders([...selectedFolders, { id: folderId, name: folderName }]); } }; const toggleFileSelection = (fileId: string, fileName: string) => { if (isFileSelected(fileId)) { onSelectFiles(selectedFiles.filter((f) => f.id !== fileId)); } else { onSelectFiles([...selectedFiles, { id: fileId, name: fileName }]); } }; const findItem = useCallback( (itemId: string): DriveItem | undefined => { const state = itemStates.get(itemId); if (state?.item) return state.item; const rootItem = rootItems.find((item) => item.id === itemId); if (rootItem) return rootItem; for (const [, nodeState] of itemStates) { if (nodeState.children) { const found = nodeState.children.find((child) => child.id === itemId); if (found) return found; } } return undefined; }, [itemStates, rootItems] ); const loadFolderContents = useCallback( async (folderId: string) => { try { setItemStates((prev) => { const newMap = new Map(prev); const existing = newMap.get(folderId); if (existing) { newMap.set(folderId, { ...existing, isLoading: true }); } else { const item = findItem(folderId); if (item) { newMap.set(folderId, { item, children: null, isExpanded: false, isLoading: true, }); } } return newMap; }); const data = await fetchItems(folderId); const items = data.items || []; setItemStates((prev) => { const newMap = new Map(prev); const existing = newMap.get(folderId); const item = existing?.item || findItem(folderId); if (item) { newMap.set(folderId, { item, children: items, isExpanded: true, isLoading: false, }); } return newMap; }); } catch (error) { console.error("Error loading folder contents:", error); setItemStates((prev) => { const newMap = new Map(prev); const existing = newMap.get(folderId); if (existing) { newMap.set(folderId, { ...existing, isLoading: false }); } return newMap; }); } }, [fetchItems, findItem] ); const toggleFolder = async (item: DriveItem) => { if (!item.isFolder) return; const state = itemStates.get(item.id); if (!state || state.children === null) { await loadFolderContents(item.id); } else { setItemStates((prev) => { const newMap = new Map(prev); newMap.set(item.id, { ...state, isExpanded: !state.isExpanded, }); return newMap; }); } }; const renderItem = (item: DriveItem, level: number = 0) => { const state = itemStates.get(item.id); const isExpanded = state?.isExpanded || false; const isLoading = state?.isLoading || false; const children = state?.children; const isFolder = item.isFolder; const isSelected = isFolder ? isFolderSelected(item.id) : isFileSelected(item.id); const childFolders = children?.filter((c) => c.isFolder) || []; const childFiles = children?.filter((c) => !c.isFolder) || []; const indentSize = 0.75; return (
{isFolder ? ( ) : ( )} { if (isFolder) { toggleFolderSelection(item.id, item.name); } else { toggleFileSelection(item.id, item.name); } }} className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20" onClick={(e) => e.stopPropagation()} />
{isFolder ? ( isExpanded ? ( ) : ( ) ) : ( getFileIcon(item.mimeType, "h-3 w-3 sm:h-4 sm:w-4") )}
{isFolder ? ( ) : ( {item.name} )}
{isExpanded && isFolder && children && (
{childFolders.map((child) => renderItem(child, level + 1))} {childFiles.map((child) => renderItem(child, level + 1))} {children.length === 0 && (
Empty folder
)}
)}
); }; return (
toggleFolderSelection("root", rootLabel)} className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20" />
{isLoadingRoot && (
)}
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
{!isLoadingRoot && rootError && (
{rootError.message.includes("authentication expired") ? `${providerName} authentication has expired. Please re-authenticate above.` : `Failed to load ${providerName} contents.`}
)} {!isLoadingRoot && !rootError && rootItems.length === 0 && (
No files or folders found in your {providerName}
)}
); }