0c2f49876d
- 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>
79 lines
2.7 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|