File size: 4,366 Bytes
c9a1ce7
 
69178d2
c9a1ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69178d2
 
 
c9a1ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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>
  );
}