/** * @file PathPickButton.tsx * @description The one "Browse" button beside every path field: it asks the * server to open the operating system's own folder/file picker * (`POST /api/lanes/pick-path`) and fills the field with the absolute path the * user chose. A browser's picker cannot give a page that path, so the dialog * opens on the server's desktop — which is only this user's screen when the * browser runs on the same machine. Anywhere else (a LAN browser, a server * with no desktop or no zenity/kdialog) a folder falls back to the in-app * `FolderBrowseModal` (as it does on any other failure), and a file to typing * the path. * @author Nguyễn Ngọc Trí Vĩ */ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { FolderOpen } from "lucide-react"; import { api } from "../lib/api"; import { toast } from "../lib/toast"; import { FolderBrowseModal } from "./lanes/FolderBrowseModal"; /** Server answers that mean "no native picker for this browser" — not a fault. */ const FALLBACK = new Set(["EREMOTE", "EBADORIGIN", "ENODIALOG"]); export function PathPickButton({ kind = "dir", start, onPick, showLabel = false, }: { kind?: "dir" | "file"; /** The field's current value — the picker opens there. */ start?: string; onPick: (path: string) => void; showLabel?: boolean; }) { const { t } = useTranslation(["lanes"]); const [busy, setBusy] = useState(false); const [fallback, setFallback] = useState(false); const title = kind === "dir" ? t("browse.title") : t("browse.fileTitle"); const pick = async () => { setBusy(true); try { const r = await api.lanes.pickPath(kind, start?.trim() || undefined); if (r.path) onPick(r.path); } catch (err) { const code = (err as { code?: string }).code; // A folder always has the in-app browser to fall back on, whatever went // wrong with the native one (a crashed dialog, one already open). if (kind === "dir") setFallback(true); else if (code && FALLBACK.has(code)) toast.info(t("browse.typePath")); else { toast.error(err instanceof Error ? err.message : String(err)); } } finally { setBusy(false); } }; return ( <> {/* Its clicks stay here: a backdrop click must close only this modal, not bubble on to a dialog this button sits in. */}
e.stopPropagation()}> setFallback(false)} />
); }