/** * @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) it falls back to the in-app * `FolderBrowseModal` (as it does on any other failure) — for a file field * too, which then lists files and picks one on click. * @author Nguyễn Ngọc Trí Vĩ */ import { useState, type Ref } from "react"; import { useTranslation } from "react-i18next"; import { FolderOpen } from "lucide-react"; import { api } from "../lib/api"; import { FolderBrowseModal } from "./lanes/FolderBrowseModal"; export function PathPickButton({ kind = "dir", start, onPick, showLabel = false, buttonRef, }: { kind?: "dir" | "file"; /** The field's current value — the picker opens there. */ start?: string; onPick: (path: string) => void; showLabel?: boolean; /** Lets a field beside it open the same picker (a click on an empty path). */ buttonRef?: Ref; }) { 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 { // The in-app browser is the fallback whatever went wrong with the // native one: a LAN browser (EREMOTE), no desktop, a crashed dialog. setFallback(true); } 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)} />
); }