/** * SharepointBrowseTree – Lazy-loading tree for SharePoint browse. * Same look & feel as FolderTree (chevron, FaFolder/FaFolderOpen, styling). * Loads children on expand via onLoadChildren(path). */ import React, { useState, useCallback, useEffect } from 'react'; import { FaFolder, FaFolderOpen, FaChevronRight, FaGlobe } from 'react-icons/fa'; import styles from './FolderTree.module.css'; export interface BrowseEntry { name: string; path: string; isFolder: boolean; size?: number; mimeType?: string; metadata?: Record; } export interface SharepointBrowseTreeProps { /** Root path (usually "/") - children loaded via onLoadChildren */ rootPath?: string; /** Load children for a given path. Returns folders and files. */ onLoadChildren: (path: string) => Promise; /** Called when user selects a file path */ onSelectFile: (path: string) => void; /** Called when user selects a folder path (e.g. for destination). If provided, folder rows are selectable. */ onSelectFolder?: (path: string) => void; /** Currently selected path (for highlight) */ selectedPath?: string | null; /** Optional: pre-seed root children (e.g. from initial load) */ initialChildren?: BrowseEntry[]; } function _fileIcon(mime?: string): string { if (!mime) return '\uD83D\uDCC4'; if (mime.startsWith('image/')) return '\uD83D\uDDBC\uFE0F'; if (mime.includes('pdf')) return '\uD83D\uDCD5'; if (mime.includes('word') || mime.includes('docx')) return '\uD83D\uDCD8'; if (mime.includes('sheet') || mime.includes('xlsx') || mime.includes('csv')) return '\uD83D\uDCCA'; if (mime.includes('presentation') || mime.includes('pptx')) return '\uD83D\uDCD9'; if (mime.includes('zip') || mime.includes('tar') || mime.includes('gz')) return '\uD83D\uDCE6'; if (mime.startsWith('text/') || mime.includes('json') || mime.includes('xml')) return '\uD83D\uDCDD'; return '\uD83D\uDCC4'; } /* ── File row ──────────────────────────────────────────────────────────── */ function _FileRow({ entry, selectedPath, onSelect, }: { entry: BrowseEntry; selectedPath: string | null | undefined; onSelect: (path: string) => void; }) { const isSelected = selectedPath === entry.path; return (
onSelect(entry.path)} title={entry.path} > {_fileIcon(entry.mimeType)} {entry.name} {entry.size != null && ( {(entry.size / 1024).toFixed(0)}K )}
); } /* ── Folder row (expandable, lazy-loads children) ───────────────────────── */ function _FolderRow({ entry, selectedPath, expandedPaths, loadedChildren, loadingPaths, onToggle, onSelectFile, onSelectFolder, }: { entry: BrowseEntry; selectedPath: string | null | undefined; expandedPaths: Set; loadedChildren: Record; loadingPaths: Set; onToggle: (path: string) => void; onSelectFile: (path: string) => void; onSelectFolder?: (path: string) => void; }) { const isExpanded = expandedPaths.has(entry.path); const isSelected = selectedPath === entry.path; const children = loadedChildren[entry.path] ?? []; const folders = children.filter((c) => c.isFolder).sort((a, b) => a.name.localeCompare(b.name)); const files = children.filter((c) => !c.isFolder).sort((a, b) => a.name.localeCompare(b.name)); const isLoading = isExpanded && loadingPaths.has(entry.path); const handleRowClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement; if (target.closest(`.${styles.chevron}`)) return; if (onSelectFolder) { onSelectFolder(entry.path); return; } onToggle(entry.path); }; const handleChevronClick = (e: React.MouseEvent) => { e.stopPropagation(); onToggle(entry.path); }; return (
{isExpanded ? : } {entry.name} {isLoading && ( )}
{isExpanded && (
{isLoading ? (
Wird geladen…
) : ( <> {folders.map((child) => ( <_FolderRow key={child.path} entry={child} selectedPath={selectedPath} expandedPaths={expandedPaths} loadedChildren={loadedChildren} loadingPaths={loadingPaths} onToggle={onToggle} onSelectFile={onSelectFile} onSelectFolder={onSelectFolder} /> ))} {files.map((child) => ( <_FileRow key={child.path} entry={child} selectedPath={selectedPath} onSelect={onSelectFile} /> ))} {children.length === 0 && (
Leer
)} )}
)}
); } /* ── Root component ─────────────────────────────────────────────────────── */ export function SharepointBrowseTree({ rootPath = '/', onLoadChildren, onSelectFile, onSelectFolder, selectedPath, initialChildren = [], }: SharepointBrowseTreeProps) { const [expandedPaths, setExpandedPaths] = useState>(new Set([rootPath])); const [loadedChildren, setLoadedChildren] = useState>(() => initialChildren.length > 0 ? { [rootPath]: initialChildren } : {} ); const [loadingPaths, setLoadingPaths] = useState>(new Set()); const loadPath = useCallback( async (path: string) => { setLoadingPaths((p) => new Set(p).add(path)); try { const items = await onLoadChildren(path); setLoadedChildren((prev) => ({ ...prev, [path]: items })); } catch { setLoadedChildren((prev) => ({ ...prev, [path]: [] })); } finally { setLoadingPaths((p) => { const next = new Set(p); next.delete(path); return next; }); } }, [onLoadChildren] ); const handleToggle = useCallback( (path: string) => { setExpandedPaths((prev) => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); loadPath(path); } return next; }); }, [loadPath] ); useEffect(() => { if (rootPath in loadedChildren) return; if (initialChildren.length > 0) return; loadPath(rootPath); }, [rootPath, initialChildren.length, loadPath]); const rootItems = loadedChildren[rootPath] ?? []; const rootLoading = loadingPaths.has(rootPath); const rootFolders = rootItems.filter((e) => e.isFolder).sort((a, b) => a.name.localeCompare(b.name)); const rootFiles = rootItems.filter((e) => !e.isFolder).sort((a, b) => a.name.localeCompare(b.name)); const isRootExpanded = expandedPaths.has(rootPath); return (
handleToggle(rootPath)} > SharePoint {rootLoading && ( )}
{isRootExpanded && (
{rootLoading ? (
Sites werden geladen…
) : ( <> {rootFolders.map((entry) => ( <_FolderRow key={entry.path} entry={entry} selectedPath={selectedPath} expandedPaths={expandedPaths} loadedChildren={loadedChildren} loadingPaths={loadingPaths} onToggle={handleToggle} onSelectFile={onSelectFile} onSelectFolder={onSelectFolder} /> ))} {rootFiles.map((entry) => ( <_FileRow key={entry.path} entry={entry} selectedPath={selectedPath} onSelect={onSelectFile} /> ))} {rootItems.length === 0 && !rootLoading && (
Keine Einträge
)} )}
)}
); }