"use client"; import { ChevronDown, ChevronRight, File, FileSpreadsheet, FileText, FolderClosed, FolderOpen, HardDrive, Image, Presentation, } from "lucide-react"; import { useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Spinner } from "@/components/ui/spinner"; import { useComposioDriveFolders } from "@/hooks/use-composio-drive-folders"; import { connectorsApiService } from "@/lib/apis/connectors-api.service"; import { cn } from "@/lib/utils"; interface DriveItem { id: string; name: string; mimeType: string; isFolder: boolean; parents?: string[]; size?: number; iconLink?: string | null; } interface ItemTreeNode { item: DriveItem; children: DriveItem[] | null; // null = not loaded, [] = loaded but empty isExpanded: boolean; isLoading: boolean; } interface SelectedFolder { id: string; name: string; } interface ComposioDriveFolderTreeProps { connectorId: number; selectedFolders: SelectedFolder[]; onSelectFolders: (folders: SelectedFolder[]) => void; selectedFiles?: SelectedFolder[]; onSelectFiles?: (files: SelectedFolder[]) => 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 ; } export function ComposioDriveFolderTree({ connectorId, selectedFolders, onSelectFolders, selectedFiles = [], onSelectFiles = () => {}, }: ComposioDriveFolderTreeProps) { const [itemStates, setItemStates] = useState>(new Map()); const { data: rootData, isLoading: isLoadingRoot } = useComposioDriveFolders({ connectorId, }); const rootItems = rootData?.items || []; 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 }]); } }; /** * Find an item by ID across all loaded items (root and nested). */ const findItem = (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; }; /** * Load and display contents of a specific folder. */ const loadFolderContents = 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 connectorsApiService.listComposioDriveFolders({ connector_id: connectorId, parent_id: 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, }); } else { console.error(`Could not find item for folderId: ${folderId}`); } 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; }); } }; /** * Toggle folder expand/collapse state. */ 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; }); } }; /** * Render a single item (folder or file) with its children. */ 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; // Smaller indent for mobile 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", "My Drive")} 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 && rootItems.length === 0 && (
No files or folders found in your Google Drive
)}
); }