Files
AgentHub/client/src/components/PathPickButton.tsx
T
nntrivi2001 0c2f49876d feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP
- Users page: one Edit user form (display name, email, role, active, a new or
  generated password, workspaces given or taken back, sign out everywhere),
  validated as a whole before anything is written (PATCH /api/users/:id,
  GET /api/users/:id); users.display_name/email added migration-safe.
- Settings → Account for every signed-in user: profile, own password (the
  current one required, this login kept), sign out other devices
  (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie
  only, never a pane token).
- Display names wherever a person is named (sidebar, workspace owner groups,
  Users page and audit log); owner_display_name / reader_display additive.
- Users page: admins add and remove Claude/Codex pool accounts.
- Folder browser: an admin with their own Linux account opens in their home;
  the "Server folder" tick opens the host's (never remembered).
- AI Proxy: "Config 9router" beside Add profile when 9router is installed,
  opened on the dashboard's own host (GET /api/ai-proxies/9router).
- Terminal: a tmux selection reaches the clipboard over plain HTTP too
  (writeClipboard's textarea fallback, focus handed back).
- Chat: a file the session touched outside the workspace previews
  (GET /api/sessions/:id/file).
- install-system: `!requiretty` only when visudo accepts it (sudo-rs on
  Ubuntu 25.10+ rejects the setting).
- openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:10:29 +07:00

79 lines
2.7 KiB
TypeScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
import { useState } 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,
}: {
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 {
// 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 (
<>
<button
type="button"
onClick={pick}
disabled={busy}
title={title}
aria-label={title}
className="flex shrink-0 items-center gap-1 self-stretch rounded-md border border-border-light px-2 text-xs text-fg-secondary hover:bg-surface-2 disabled:opacity-50"
>
<FolderOpen className="h-3.5 w-3.5" />
{showLabel && t("browse.button")}
</button>
{/* Its clicks stay here: a backdrop click must close only this modal,
not bubble on to a dialog this button sits in. */}
<div className="contents" onClick={(e) => e.stopPropagation()}>
<FolderBrowseModal
open={fallback}
kind={kind}
initialPath={start?.trim() || undefined}
onSelect={onPick}
onClose={() => setFallback(false)}
/>
</div>
</>
);
}