refactor: move view selection to titlebar menu
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Eye, LockKeyhole } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Check, LockKeyhole, View as ViewIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DismissibleAlert,
|
||||
dispatchPlatformViewChanged,
|
||||
usePlatformLanguage,
|
||||
useUnsavedChanges,
|
||||
@@ -15,10 +16,21 @@ export default function ViewSelector({
|
||||
}: ViewSelectorProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const { requestNavigation } = useUnsavedChanges();
|
||||
const options = projection?.availableViews ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
function onPointerDown(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
if (menuRef.current && !menuRef.current.contains(target)) setOpen(false);
|
||||
}
|
||||
window.addEventListener("mousedown", onPointerDown);
|
||||
return () => window.removeEventListener("mousedown", onPointerDown);
|
||||
}, []);
|
||||
|
||||
if (!projection?.activeViewId && options.length === 0) return null;
|
||||
|
||||
async function performSelect(viewId: string) {
|
||||
@@ -27,6 +39,7 @@ export default function ViewSelector({
|
||||
try {
|
||||
await selectEffectiveView(settings, viewId || null);
|
||||
dispatchPlatformViewChanged();
|
||||
setOpen(false);
|
||||
} catch (caught) {
|
||||
setError(
|
||||
caught instanceof Error
|
||||
@@ -45,6 +58,8 @@ export default function ViewSelector({
|
||||
}
|
||||
|
||||
const locked = Boolean(projection?.locked);
|
||||
const activeViewId = projection?.activeViewId ?? "";
|
||||
const specialized = projection?.projectionActive ?? Boolean(activeViewId);
|
||||
const diagnostic = projection?.diagnostics.find((item) => item.severity === "error")
|
||||
?? projection?.diagnostics[0];
|
||||
const title = translateText(error || diagnostic?.message || (
|
||||
@@ -54,27 +69,75 @@ export default function ViewSelector({
|
||||
));
|
||||
|
||||
return (
|
||||
<label className="titlebar-view-selector" title={title}>
|
||||
{locked
|
||||
? <LockKeyhole size={16} aria-hidden="true" />
|
||||
: <Eye size={16} aria-hidden="true" />}
|
||||
<span className="sr-only">i18n:govoplan-views.current_view</span>
|
||||
<select
|
||||
value={projection?.activeViewId ?? ""}
|
||||
disabled={busy || locked}
|
||||
<div className="context-menu-wrap titlebar-view-menu-wrap" ref={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`titlebar-icon-link${specialized ? " is-context-active" : ""}`}
|
||||
data-help-context-id="views.selector"
|
||||
data-help-module-id="views"
|
||||
data-help-scope="action"
|
||||
title={title}
|
||||
aria-label={translateText("i18n:govoplan-views.current_view")}
|
||||
aria-invalid={Boolean(error) || undefined}
|
||||
onChange={(event) => select(event.target.value)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
{!locked && (
|
||||
<option value="">
|
||||
{translateText("i18n:govoplan-views.full_interface")}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option.id} value={option.id}>{option.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<ViewIcon size={18} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="dropdown-menu titlebar-view-menu" role="menu">
|
||||
<div className="titlebar-view-menu-heading">
|
||||
<strong>i18n:govoplan-views.view_selector</strong>
|
||||
{locked && <LockKeyhole size={15} aria-hidden="true" />}
|
||||
</div>
|
||||
|
||||
{!locked && (
|
||||
<button
|
||||
type="button"
|
||||
className={`dropdown-item${specialized ? "" : " active"}`}
|
||||
disabled={busy}
|
||||
onClick={() => select("")}
|
||||
role="menuitemradio"
|
||||
aria-checked={!specialized}
|
||||
>
|
||||
<span>i18n:govoplan-views.full_interface</span>
|
||||
{!specialized && <Check size={16} aria-hidden="true" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{options.map((option) => {
|
||||
const active = option.id === activeViewId;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={option.id}
|
||||
className={`dropdown-item${active ? " active" : ""}`}
|
||||
disabled={busy || locked}
|
||||
onClick={() => select(option.id)}
|
||||
role="menuitemradio"
|
||||
aria-checked={active}
|
||||
>
|
||||
<span>{option.name}</span>
|
||||
{active && <Check size={16} aria-hidden="true" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{(locked || diagnostic) && (
|
||||
<p className="titlebar-view-menu-note">
|
||||
{locked
|
||||
? "i18n:govoplan-views.required_by_administrator"
|
||||
: diagnostic?.message}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<DismissibleAlert tone="danger" compact resetKey={error}>
|
||||
{error}
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+29
-26
@@ -1,32 +1,39 @@
|
||||
.titlebar-view-selector {
|
||||
display: inline-flex;
|
||||
.titlebar-view-menu {
|
||||
width: min(300px, calc(100vw - 24px));
|
||||
}
|
||||
|
||||
.titlebar-view-menu-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 34px;
|
||||
padding: 4px 9px 8px;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.titlebar-view-menu .dropdown-item > span {
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
color: var(--muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.titlebar-view-selector select {
|
||||
width: min(190px, 24vw);
|
||||
height: 32px;
|
||||
padding: 0 28px 0 8px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
.titlebar-view-menu .dropdown-item > svg {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.titlebar-view-selector select:hover,
|
||||
.titlebar-view-selector select:focus-visible {
|
||||
background-color: var(--titlebar-hover-bg);
|
||||
}
|
||||
|
||||
.titlebar-view-selector select:disabled {
|
||||
.titlebar-view-menu .dropdown-item:disabled {
|
||||
cursor: default;
|
||||
opacity: .78;
|
||||
opacity: .72;
|
||||
}
|
||||
|
||||
.titlebar-view-menu-note {
|
||||
margin: 8px 9px 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.views-management-layout {
|
||||
@@ -314,10 +321,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.titlebar-view-selector select {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.views-admin-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user