provinans / frontend /src /ProjectSwitcher.tsx
reversely's picture
Upload folder using huggingface_hub
69178d2 verified
Raw
History Blame Contribute Delete
4.37 kB
import { useEffect, useRef, useState } from "react";
import { createProject } from "./api";
import { useProject } from "./project";
import { getIdentity, subscribeIdentity } from "./session";
import { ChevronDown } from "./icons";
// The top-bar project ("repo") switcher. The pill shows the open project's name; clicking opens a menu
// of every project with its case count, and selecting one scopes the worklist. An admin (session role
// "admin") also gets a "New project" action that creates one, refreshes the list, and switches to it.
export default function ProjectSwitcher() {
const { projectId, setProjectId, projects, refresh } = useProject();
const [open, setOpen] = useState(false);
const [isAdmin, setIsAdmin] = useState(() => getIdentity().role === "admin");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const ref = useRef<HTMLDivElement>(null);
// The New project action follows the session identity: it appears on an admin login and disappears on
// sign-out without a reload.
useEffect(() => subscribeIdentity(() => setIsAdmin(getIdentity().role === "admin")), []);
// Dismiss the menu on a click outside it.
useEffect(() => {
if (!open) return;
function onPointerDown(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false);
}
document.addEventListener("mousedown", onPointerDown);
return () => document.removeEventListener("mousedown", onPointerDown);
}, [open]);
const current = projects.find((p) => p.project_id === projectId);
// Before the project list loads (or if the open project is not in it), fall back to the raw project id
// rather than a hardcoded product name that can disagree with the real project record (#247).
const currentName = current?.name ?? projectId;
function select(id: string) {
setProjectId(id);
setOpen(false);
}
async function handleNewProject() {
const name = window.prompt("New project name")?.trim();
if (!name) return;
setBusy(true);
setError(null);
try {
const created = await createProject(name);
await refresh();
setProjectId(created.project_id);
setOpen(false);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setBusy(false);
}
}
return (
<div className="project-switcher" ref={ref}>
<button
type="button"
className="topbar-project"
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
title="Switch the open project"
>
<span className="topbar-project-badge">Project</span>
<span className="topbar-project-name">{currentName}</span>
{current ? <span className="topbar-project-standards">{current.case_count} cases</span> : null}
<span className="project-switcher-caret" aria-hidden>
<ChevronDown />
</span>
</button>
{open ? (
<div className="project-menu" role="menu">
{projects.map((p) => (
<button
type="button"
key={p.project_id}
role="menuitemradio"
aria-checked={p.project_id === projectId}
className={p.project_id === projectId ? "project-menu-item active" : "project-menu-item"}
onClick={() => select(p.project_id)}
>
<span className="project-menu-name">{p.name}</span>
<span className="project-menu-count">{p.case_count}</span>
</button>
))}
{projects.length === 0 ? <span className="project-menu-empty">No projects yet.</span> : null}
{isAdmin ? (
<>
<div className="project-menu-sep" role="separator" />
<button
type="button"
role="menuitem"
className="project-menu-new"
onClick={handleNewProject}
disabled={busy}
>
{busy ? "Creating…" : "+ New project"}
</button>
</>
) : null}
{error ? (
<span className="project-menu-error" role="alert">
{error}
</span>
) : null}
</div>
) : null}
</div>
);
}