/** * @file A server-backed folder browser for the Add Lane modal's path inputs. * Browsers refuse to expose an absolute filesystem path from a native folder * picker, so path selection here is done by browsing `GET /api/lanes/browse` * (immediate subdirectories of a path) instead — breadcrumb-free, just an * up-one-level button and a click-to-descend list, since this tool is * local-first and the server already trusts arbitrary local paths. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { FolderOpen, FolderGit2, ArrowUp } from "lucide-react"; import { api } from "../../lib/api"; export function FolderBrowseModal({ open, initialPath, onSelect, onClose, }: { open: boolean; /** Path to start browsing from; omitted defaults server-side to the home dir. */ initialPath?: string; onSelect: (path: string) => void; onClose: () => void; }) { const { t } = useTranslation(["lanes"]); const [listing, setListing] = useState> | null>(null); const [error, setError] = useState(null); const load = async (path?: string) => { setError(null); try { setListing(await api.lanes.browse(path)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; useEffect(() => { if (open) void load(initialPath); // Only re-run when the modal actually opens - not on every initialPath // keystroke in the field behind it. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()} role="dialog" aria-modal="true" aria-label={t("browse.title")} >
{listing?.path || "…"}
{error && (

{error}

)} {listing?.parent && ( )} {listing?.entries.map((entry) => ( ))} {listing && listing.entries.length === 0 && !listing.parent && (

{t("browse.empty")}

)}
); }