import { useState, useCallback, useEffect } from "react"; import { Folder, FolderOpen, ChevronRight, ChevronUp, Loader2, Eye, EyeOff, AlertCircle } from "lucide-react"; import { browseDirectory, type BrowseDirectoryResult } from "../api"; import "./DirectoryPicker.css"; export interface DirectoryPickerProps { value: string; onChange: (path: string) => void; placeholder?: string; /** Optional keydown handler forwarded to the text input (e.g. Enter-to-submit). */ onInputKeyDown?: (e: React.KeyboardEvent) => void; /** Node ID of the target node for browsing. */ nodeId?: string; /** Node ID of the local node (used to determine when to route through proxy). */ localNodeId?: string; } interface BrowserState { isOpen: boolean; loading: boolean; error: string | null; currentPath: string; parentPath: string | null; entries: BrowseDirectoryResult["entries"]; showHidden: boolean; } export function DirectoryPicker({ value, onChange, placeholder, onInputKeyDown, nodeId, localNodeId }: DirectoryPickerProps) { const [browser, setBrowser] = useState({ isOpen: false, loading: false, error: null, currentPath: "", parentPath: null, entries: [], showHidden: false, }); const fetchEntries = useCallback(async (path?: string, showHidden = false) => { setBrowser((prev) => ({ ...prev, loading: true, error: null })); try { const result = await browseDirectory(path, showHidden, nodeId, localNodeId); setBrowser((prev) => ({ ...prev, loading: false, currentPath: result.currentPath, parentPath: result.parentPath, entries: result.entries, })); } catch (err) { setBrowser((prev) => ({ ...prev, loading: false, error: err instanceof Error ? err.message : "Failed to browse directory", })); } }, [nodeId, localNodeId]); const handleToggleBrowser = useCallback(() => { setBrowser((prev) => { if (!prev.isOpen) { // Opening — fetch entries return { ...prev, isOpen: true }; } return { ...prev, isOpen: false }; }); }, []); // Fetch when browser opens useEffect(() => { if (browser.isOpen && !browser.loading && browser.entries.length === 0 && !browser.error) { fetchEntries(value || undefined, browser.showHidden); } }, [browser.isOpen, browser.loading, browser.entries.length, browser.error, value, browser.showHidden, fetchEntries, nodeId, localNodeId]); const handleNavigate = useCallback( (path: string) => { fetchEntries(path, browser.showHidden); }, [fetchEntries, browser.showHidden] ); const handleSelect = useCallback(() => { onChange(browser.currentPath); setBrowser((prev) => ({ ...prev, isOpen: false })); }, [browser.currentPath, onChange]); const handleToggleHidden = useCallback(() => { setBrowser((prev) => { const next = !prev.showHidden; return { ...prev, showHidden: next }; }); }, []); // Refetch when showHidden changes while browser is open useEffect(() => { if (browser.isOpen && browser.currentPath) { fetchEntries(browser.currentPath, browser.showHidden); } }, [browser.showHidden]); const breadcrumbs = browser.currentPath ? browser.currentPath.split("/").filter(Boolean) : []; return (
onChange(e.target.value)} onKeyDown={onInputKeyDown} placeholder={placeholder || "/path/to/your/project"} />
{browser.isOpen && (
{/* Breadcrumbs */}
{breadcrumbs.map((segment, i) => { const segPath = "/" + breadcrumbs.slice(0, i + 1).join("/"); return ( ); })}
{/* Toolbar */}
{browser.parentPath && ( )}
{/* Content */} {browser.loading ? (
Loading…
) : browser.error ? (
{browser.error}
) : (
{browser.entries.length === 0 ? (
No subdirectories
) : ( browser.entries.map((entry) => ( )) )}
)} {/* Actions */}
{browser.currentPath}
)}
); }