"use client"; import { ChevronDown, ChevronRight, File, FileText, Folder, FolderOpen, HardDrive, Image, Loader2, Sheet, Presentation, } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { authenticatedFetch } from "@/lib/auth-utils"; interface DriveItem { id: string; name: string; mimeType: string; isFolder: boolean; parents?: string[]; size?: number; iconLink?: string; } interface ItemTreeNode { item: DriveItem; children: DriveItem[] | null; // null = not loaded, [] = loaded but empty isExpanded: boolean; isLoading: boolean; } interface GoogleDriveFolderTreeProps { connectorId: number; selectedFolderId: string | null; onSelectFolder: (folderId: string, folderName: string) => void; } // Helper to get appropriate icon for file type function getFileIcon(mimeType: string, className: string = "h-4 w-4") { if (mimeType.includes("spreadsheet") || mimeType.includes("excel")) { return ; } if (mimeType.includes("presentation") || mimeType.includes("powerpoint")) { return ; } if (mimeType.includes("document") || mimeType.includes("word") || mimeType.includes("text")) { return ; } if (mimeType.includes("image")) { return ; } return ; } // Helper to format file size function formatFileSize(bytes: number | undefined): string { if (!bytes) return ""; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; } export function GoogleDriveFolderTree({ connectorId, selectedFolderId, onSelectFolder, }: GoogleDriveFolderTreeProps) { const [rootItems, setRootItems] = useState([]); const [itemStates, setItemStates] = useState>(new Map()); const [isLoadingRoot, setIsLoadingRoot] = useState(false); const [isInitialized, setIsInitialized] = useState(false); // Load root items (folders and files) on mount const loadRootItems = async () => { if (isInitialized) return; // Already loaded setIsLoadingRoot(true); try { const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/connectors/${connectorId}/google-drive/folders` ); if (!response.ok) throw new Error("Failed to load items"); const data = await response.json(); setRootItems(data.items || []); setIsInitialized(true); } catch (error) { console.error("Error loading root items:", error); } finally { setIsLoadingRoot(false); } }; // Helper function to find an item recursively through all loaded items const findItem = (itemId: string): DriveItem | undefined => { // First check if we have it in itemStates const state = itemStates.get(itemId); if (state?.item) return state.item; // Check root items const rootItem = rootItems.find((item) => item.id === itemId); if (rootItem) return rootItem; // Recursively search through all loaded children for (const [, nodeState] of itemStates) { if (nodeState.children) { const found = nodeState.children.find((child) => child.id === itemId); if (found) return found; } } return undefined; }; // Load children (folders and files) for a specific folder const loadFolderContents = async (folderId: string) => { try { // Set loading state setItemStates((prev) => { const newMap = new Map(prev); const existing = newMap.get(folderId); if (existing) { newMap.set(folderId, { ...existing, isLoading: true }); } else { // First time loading this folder - create initial state const item = findItem(folderId); if (item) { newMap.set(folderId, { item, children: null, isExpanded: false, isLoading: true, }); } } return newMap; }); const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/connectors/${connectorId}/google-drive/folders?parent_id=${folderId}` ); if (!response.ok) throw new Error("Failed to load folder contents"); const data = await response.json(); const items = data.items || []; // Check if folder only contains files (no subfolders) const hasSubfolders = items.some((item: DriveItem) => item.isFolder); // Update item state with loaded children 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, // Always expand after loading isLoading: false, }); } else { console.error(`Could not find item for folderId: ${folderId}`); } return newMap; }); } catch (error) { console.error("Error loading folder contents:", error); // Clear loading state on error setItemStates((prev) => { const newMap = new Map(prev); const existing = newMap.get(folderId); if (existing) { newMap.set(folderId, { ...existing, isLoading: false }); } return newMap; }); } }; // Toggle folder expansion const toggleFolder = async (item: DriveItem) => { if (!item.isFolder) return; // Only folders can be expanded const state = itemStates.get(item.id); if (!state || state.children === null) { // First time expanding - load children await loadFolderContents(item.id); } else { // Toggle expansion state setItemStates((prev) => { const newMap = new Map(prev); newMap.set(item.id, { ...state, isExpanded: !state.isExpanded, }); return newMap; }); } }; // Recursive render function for item tree 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 isSelected = selectedFolderId === item.id; const isFolder = item.isFolder; // Separate folders and files for children const childFolders = children?.filter((c) => c.isFolder) || []; const childFiles = children?.filter((c) => !c.isFolder) || []; return (
{/* Render children if expanded (folders first, then files) */} {isExpanded && isFolder && children && (
{/* Render folders first */} {childFolders.map((child) => renderItem(child, level + 1))} {/* Render files */} {childFiles.map((child) => renderItem(child, level + 1))} {/* Empty state */} {children.length === 0 && (
Empty folder
)}
)}
); }; // Initialize on first render if (!isInitialized && !isLoadingRoot) { loadRootItems(); } return (
{/* My Drive Header (always visible, selectable) */}
{/* Loading indicator */} {isLoadingRoot && (
)} {/* Root items (folders and files) - same level as Google Drive shows */}
{!isLoadingRoot && rootItems.map((item) => renderItem(item, 0))}
{/* Empty state */} {!isLoadingRoot && rootItems.length === 0 && (
No files or folders found in your Google Drive
)}
); }