Seitenleiste: Eintraege ausblenden, als Vorgabe und je Konto
In den Einstellungen legt der Administrator fest, was es im Haushalt ueberhaupt gibt; in "Mein Konto" blendet jeder zusaetzlich aus, was er selbst nicht braucht. Angehakt heisst sichtbar - auch wenn der Server das Gegenteil speichert. Was der Administrator ausgeblendet hat, steht beim Konto abgehakt und gesperrt da: Die eigene Auswahl addiert nur. Uebersicht und Einstellungen fehlen in der Liste mit Absicht - dort landen alle Anmeldewege bzw. es ist der einzige Weg zurueck. Wer beides verstecken koennte, sperrte sich aus. Ausgeblendet wird ueber einen Kontext statt per CSS-Klasse: Unter 768px blendet die CSS die Listen-Kinder wieder ein, eine Klasse bisse sich damit. Faellt dabei die ganze Listen-Gruppe weg, verschwindet auch ihr Aufklapp-Knopf, und die Ueberschrift "Stammdaten" steht nur noch, solange etwas darunter ist. Die Leiste zieht sofort nach, ueber dasselbe Ereignis-Muster wie bei den Dashboards - kein Neuladen noetig. Versteckt wird nur der Eintrag, nicht die Seite: /shopping bleibt per Lesezeichen und ueber die Dashboard-Karten erreichbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
be267e70a5
commit
757d06100a
+40
-4
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import { useAuth } from "./auth";
|
import { useAuth } from "./auth";
|
||||||
|
import { gruppenPfade } from "./nav";
|
||||||
import Icon from "./components/Icon";
|
import Icon from "./components/Icon";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import BrandMark from "./components/BrandMark";
|
import BrandMark from "./components/BrandMark";
|
||||||
@@ -36,6 +37,11 @@ import Konto from "./pages/Konto";
|
|||||||
|
|
||||||
const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link");
|
const navClass = ({ isActive }) => (isActive ? "nav-link active" : "nav-link");
|
||||||
|
|
||||||
|
// Ausgeblendete Eintraege stehen hier drin. Ueber einen Kontext und nicht als
|
||||||
|
// Eigenschaft an jedem NavItem: So bleiben die zwanzig Zeilen der Leiste
|
||||||
|
// unveraendert lesbar.
|
||||||
|
const VersteckteNav = createContext(new Set());
|
||||||
|
|
||||||
// Die vier Bestandslisten stehen unter einer zuklappbaren Gruppe. Sie sind der
|
// Die vier Bestandslisten stehen unter einer zuklappbaren Gruppe. Sie sind der
|
||||||
// laengste Block der Leiste und wird im Alltag selten gebraucht – wer einlagert,
|
// laengste Block der Leiste und wird im Alltag selten gebraucht – wer einlagert,
|
||||||
// nutzt die Knoepfe darueber.
|
// nutzt die Knoepfe darueber.
|
||||||
@@ -48,6 +54,9 @@ const LISTEN = [
|
|||||||
const LISTEN_OFFEN_KEY = "nav:listen-offen";
|
const LISTEN_OFFEN_KEY = "nav:listen-offen";
|
||||||
|
|
||||||
function NavItem({ to, icon, label, end }) {
|
function NavItem({ to, icon, label, end }) {
|
||||||
|
// Gar nicht erst rendern statt per CSS verbergen: Unter 768px blendet die
|
||||||
|
// CSS die Listen-Kinder wieder ein, eine Klasse bisse sich damit.
|
||||||
|
if (useContext(VersteckteNav).has(to)) return null;
|
||||||
return (
|
return (
|
||||||
<NavLink to={to} end={end} className={navClass}>
|
<NavLink to={to} end={end} className={navClass}>
|
||||||
<Icon name={icon} size={17} />
|
<Icon name={icon} size={17} />
|
||||||
@@ -64,6 +73,9 @@ function Sidebar() {
|
|||||||
// Der Rezept-Eintrag erscheint nur, wenn Mealie eingerichtet ist. Ein Fehler
|
// Der Rezept-Eintrag erscheint nur, wenn Mealie eingerichtet ist. Ein Fehler
|
||||||
// hier darf die Leiste nicht stoeren, deshalb still abfangen.
|
// hier darf die Leiste nicht stoeren, deshalb still abfangen.
|
||||||
const [mealie, setMealie] = useState(false);
|
const [mealie, setMealie] = useState(false);
|
||||||
|
// Vorgabe des Administrators plus eigene Auswahl, vom Server schon
|
||||||
|
// zusammengerechnet. Ein Fehler darf die Leiste nicht leeren.
|
||||||
|
const [versteckt, setVersteckt] = useState(() => new Set());
|
||||||
// Zugeklappt als Vorgabe; der Zustand bleibt im Browser, wie die
|
// Zugeklappt als Vorgabe; der Zustand bleibt im Browser, wie die
|
||||||
// Spalteneinstellungen der Tabellen.
|
// Spalteneinstellungen der Tabellen.
|
||||||
const [listenOffen, setListenOffen] = useState(
|
const [listenOffen, setListenOffen] = useState(
|
||||||
@@ -71,7 +83,11 @@ function Sidebar() {
|
|||||||
);
|
);
|
||||||
// Steht man auf einer dieser Seiten, ist die Gruppe offen – ohne den
|
// Steht man auf einer dieser Seiten, ist die Gruppe offen – ohne den
|
||||||
// gemerkten Zustand zu ueberschreiben. Sonst saehe man nicht, wo man ist.
|
// gemerkten Zustand zu ueberschreiben. Sonst saehe man nicht, wo man ist.
|
||||||
const aufEinerListe = LISTEN.some((e) => pathname.startsWith(e.to));
|
const sichtbareListen = LISTEN.filter((e) => !versteckt.has(e.to));
|
||||||
|
const aufEinerListe = sichtbareListen.some((e) => pathname.startsWith(e.to));
|
||||||
|
// Eine Gruppe ohne Kinder waere ein Knopf, der nichts aufklappt; eine
|
||||||
|
// Ueberschrift ohne Eintraege darunter genauso sinnlos.
|
||||||
|
const zeigtGruppe = (gruppe) => gruppenPfade(gruppe).some((t) => !versteckt.has(t));
|
||||||
|
|
||||||
function klappeListen() {
|
function klappeListen() {
|
||||||
setListenOffen((alt) => {
|
setListenOffen((alt) => {
|
||||||
@@ -93,6 +109,20 @@ function Sidebar() {
|
|||||||
return () => window.removeEventListener("dashboards-geaendert", laden);
|
return () => window.removeEventListener("dashboards-geaendert", laden);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Dasselbe Spiel fuer die ausgeblendeten Eintraege: Einstellungen und
|
||||||
|
// Kontoseite loesen beim Speichern ein Ereignis aus, statt die Seite neu zu
|
||||||
|
// laden.
|
||||||
|
useEffect(() => {
|
||||||
|
function laden() {
|
||||||
|
api.navigation()
|
||||||
|
.then((a) => setVersteckt(new Set(a.versteckt || [])))
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
laden();
|
||||||
|
window.addEventListener("navigation-geaendert", laden);
|
||||||
|
return () => window.removeEventListener("navigation-geaendert", laden);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.mealieVerfuegbar().then((a) => setMealie(!!a.available)).catch(() => {});
|
api.mealieVerfuegbar().then((a) => setMealie(!!a.available)).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -107,6 +137,7 @@ function Sidebar() {
|
|||||||
<div className="sidebar-brand">
|
<div className="sidebar-brand">
|
||||||
<BrandMark />
|
<BrandMark />
|
||||||
</div>
|
</div>
|
||||||
|
<VersteckteNav.Provider value={versteckt}>
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
{/* Ohne `end`: Der Eintrag soll auch bei /home/edit markiert bleiben.
|
{/* Ohne `end`: Der Eintrag soll auch bei /home/edit markiert bleiben.
|
||||||
Bei "/" war `end` noetig, weil sonst JEDER Pfad als Treffer galt. */}
|
Bei "/" war `end` noetig, weil sonst JEDER Pfad als Treffer galt. */}
|
||||||
@@ -124,13 +155,15 @@ function Sidebar() {
|
|||||||
{/* Auf schmalen Bildschirmen wird die Leiste waagerecht; dort gibt es
|
{/* Auf schmalen Bildschirmen wird die Leiste waagerecht; dort gibt es
|
||||||
keine Abschnitte (styles.css) und die Eintraege stehen flach
|
keine Abschnitte (styles.css) und die Eintraege stehen flach
|
||||||
nebeneinander – zuklappen loest dort kein Platzproblem. */}
|
nebeneinander – zuklappen loest dort kein Platzproblem. */}
|
||||||
|
{sichtbareListen.length > 0 && (
|
||||||
<button type="button" className="nav-link nav-group" onClick={klappeListen}
|
<button type="button" className="nav-link nav-group" onClick={klappeListen}
|
||||||
aria-expanded={listenOffen || aufEinerListe}>
|
aria-expanded={listenOffen || aufEinerListe}>
|
||||||
<Icon name="chevron" size={17}
|
<Icon name="chevron" size={17}
|
||||||
className={listenOffen || aufEinerListe ? "" : "rot-90"} />
|
className={listenOffen || aufEinerListe ? "" : "rot-90"} />
|
||||||
<span className="label">Listen</span>
|
<span className="label">Listen</span>
|
||||||
</button>
|
</button>
|
||||||
{LISTEN.map((e) => (
|
)}
|
||||||
|
{sichtbareListen.map((e) => (
|
||||||
<NavLink key={e.to} to={e.to}
|
<NavLink key={e.to} to={e.to}
|
||||||
className={({ isActive }) => [
|
className={({ isActive }) => [
|
||||||
"nav-link nav-child",
|
"nav-link nav-child",
|
||||||
@@ -152,7 +185,7 @@ function Sidebar() {
|
|||||||
<>
|
<>
|
||||||
{/* Stammdaten strukturieren den Katalog und werden nur von Admins
|
{/* Stammdaten strukturieren den Katalog und werden nur von Admins
|
||||||
gepflegt – deshalb alle zusammen hier, wie in der iOS-App. */}
|
gepflegt – deshalb alle zusammen hier, wie in der iOS-App. */}
|
||||||
<div className="nav-section">Stammdaten</div>
|
{zeigtGruppe("stammdaten") && <div className="nav-section">Stammdaten</div>}
|
||||||
<NavItem to="/categories" icon="package" label="Kategorien" />
|
<NavItem to="/categories" icon="package" label="Kategorien" />
|
||||||
<NavItem to="/groups" icon="tag" label="Gruppen" />
|
<NavItem to="/groups" icon="tag" label="Gruppen" />
|
||||||
<NavItem to="/locations" icon="location" label="Lagerorte" />
|
<NavItem to="/locations" icon="location" label="Lagerorte" />
|
||||||
@@ -160,6 +193,8 @@ function Sidebar() {
|
|||||||
<NavItem to="/units" icon="box" label="Einheiten" />
|
<NavItem to="/units" icon="box" label="Einheiten" />
|
||||||
<NavItem to="/package-types" icon="package" label="Gebinde" />
|
<NavItem to="/package-types" icon="package" label="Gebinde" />
|
||||||
<NavItem to="/masse" icon="split" label="Maße" />
|
<NavItem to="/masse" icon="split" label="Maße" />
|
||||||
|
{/* "System" steht immer, weil die Einstellungen darunter nicht
|
||||||
|
ausblendbar sind – sonst käme man nicht mehr zurück. */}
|
||||||
<div className="nav-section">System</div>
|
<div className="nav-section">System</div>
|
||||||
<NavItem to="/users" icon="users" label="Benutzer" />
|
<NavItem to="/users" icon="users" label="Benutzer" />
|
||||||
<NavItem to="/transfer" icon="history" label="Import / Export" />
|
<NavItem to="/transfer" icon="history" label="Import / Export" />
|
||||||
@@ -167,6 +202,7 @@ function Sidebar() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
</VersteckteNav.Provider>
|
||||||
<div className="sidebar-foot">
|
<div className="sidebar-foot">
|
||||||
{/* Der eigene Name ist der naheliegendste Weg zum eigenen Konto. */}
|
{/* Der eigene Name ist der naheliegendste Weg zum eigenen Konto. */}
|
||||||
<NavLink to="/konto" className="sidebar-user" title="Mein Konto">
|
<NavLink to="/konto" className="sidebar-user" title="Mein Konto">
|
||||||
|
|||||||
@@ -437,6 +437,14 @@ export const api = {
|
|||||||
request(`/etiketten/vorlagen/${id}`, { method: "DELETE" }),
|
request(`/etiketten/vorlagen/${id}`, { method: "DELETE" }),
|
||||||
// Die CSV baut der Server. basis_url braucht nur der Lagerort-QR (…/l/<id>);
|
// Die CSV baut der Server. basis_url braucht nur der Lagerort-QR (…/l/<id>);
|
||||||
// nur der Browser weiss, unter welcher Adresse er gerade erreichbar ist.
|
// nur der Browser weiss, unter welcher Adresse er gerade erreichbar ist.
|
||||||
|
// Seitenleiste: Vorgabe des Admins, eigene Auswahl und die fertige
|
||||||
|
// Vereinigung – alles in einem Zug, das Zusammenrechnen macht der Server.
|
||||||
|
navigation: () => request("/navigation"),
|
||||||
|
setzeEigeneNavigation: (versteckt) =>
|
||||||
|
request("/navigation", { method: "PUT", body: { versteckt } }),
|
||||||
|
setzeNavigationsVorgabe: (versteckt) =>
|
||||||
|
request("/navigation/vorgabe", { method: "PUT", body: { versteckt } }),
|
||||||
|
|
||||||
etikettenCsv: (id, name) =>
|
etikettenCsv: (id, name) =>
|
||||||
downloadFile(
|
downloadFile(
|
||||||
`/etiketten/vorlagen/${id}/csv?basis_url=${encodeURIComponent(window.location.origin)}`,
|
`/etiketten/vorlagen/${id}/csv?basis_url=${encodeURIComponent(window.location.origin)}`,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { NAV_AUSBLENDBAR } from "../nav";
|
||||||
|
import { useAuth } from "../auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Häkchenliste für die Seitenleiste – an zwei Stellen benutzt: in den
|
||||||
|
* Einstellungen für die Vorgabe des Administrators, auf der Kontoseite für die
|
||||||
|
* eigene Auswahl.
|
||||||
|
*
|
||||||
|
* Angehakt heisst **sichtbar**. Das ist herum, wie man es erwartet, auch wenn
|
||||||
|
* der Server das Gegenteil speichert (die Liste der versteckten Pfade).
|
||||||
|
*
|
||||||
|
* `gesperrt` sind Einträge, die der Administrator schon ausgeblendet hat: Sie
|
||||||
|
* stehen abgehakt und unveränderlich da, denn die eigene Auswahl addiert nur –
|
||||||
|
* sie nimmt nichts weg.
|
||||||
|
*/
|
||||||
|
export default function NavigationAuswahl({ versteckt, gesperrt = [], onChange }) {
|
||||||
|
const { isAdmin } = useAuth();
|
||||||
|
const aus = new Set(versteckt);
|
||||||
|
const fest = new Set(gesperrt);
|
||||||
|
// Stammdaten und System sieht ohnehin nur ein Administrator.
|
||||||
|
const eintraege = NAV_AUSBLENDBAR.filter(
|
||||||
|
(e) => isAdmin || !["stammdaten", "system"].includes(e.gruppe),
|
||||||
|
);
|
||||||
|
|
||||||
|
function umschalten(to, sichtbar) {
|
||||||
|
const neu = new Set(aus);
|
||||||
|
if (sichtbar) neu.delete(to);
|
||||||
|
else neu.add(to);
|
||||||
|
onChange([...neu]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="nav-auswahl">
|
||||||
|
{eintraege.map((e) => {
|
||||||
|
const blockiert = fest.has(e.to);
|
||||||
|
return (
|
||||||
|
<label key={e.to} className="check-inline"
|
||||||
|
title={blockiert ? "Vom Administrator ausgeblendet" : undefined}>
|
||||||
|
<input type="checkbox" disabled={blockiert}
|
||||||
|
checked={!blockiert && !aus.has(e.to)}
|
||||||
|
onChange={(ev) => umschalten(e.to, ev.target.checked)} />
|
||||||
|
<span className={blockiert ? "muted" : ""}>{e.label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Die Einträge der Seitenleiste, die sich ausblenden lassen.
|
||||||
|
*
|
||||||
|
* Als Schlüssel dient der Routenpfad – der ist ohnehin eindeutig, eine zweite
|
||||||
|
* Benennung wäre nur eine weitere Stelle, die auseinanderlaufen kann.
|
||||||
|
*
|
||||||
|
* **Übersicht und Einstellungen fehlen hier mit Absicht:** Auf „/home" landen
|
||||||
|
* alle Anmeldewege und das Auffangmuster unbekannter Adressen, und über
|
||||||
|
* „/settings" schaltet man Ausgeblendetes wieder ein. Wer beides verstecken
|
||||||
|
* könnte, sperrte sich aus. „Mein Konto" steht in der Fusszeile und ist gar
|
||||||
|
* kein Eintrag der Leiste.
|
||||||
|
*
|
||||||
|
* Versteckt wird nur der Eintrag, nicht die Seite: Lesezeichen,
|
||||||
|
* Dashboard-Karten und Lagerort-QR verweisen weiter auf diese Routen.
|
||||||
|
*/
|
||||||
|
export const NAV_AUSBLENDBAR = [
|
||||||
|
{ to: "/checkin", label: "Einlagern" },
|
||||||
|
{ to: "/checkout", label: "Auslagern" },
|
||||||
|
{ to: "/lebensmittel", label: "Lebensmittel", gruppe: "listen" },
|
||||||
|
{ to: "/gegenstaende", label: "Gegenstände", gruppe: "listen" },
|
||||||
|
{ to: "/items", label: "Einzelstücke", gruppe: "listen" },
|
||||||
|
{ to: "/chargen", label: "Chargen", gruppe: "listen" },
|
||||||
|
{ to: "/rezepte", label: "Rezepte" },
|
||||||
|
{ to: "/shopping", label: "Einkaufsliste" },
|
||||||
|
{ to: "/mindestbestaende", label: "Mindestbestände" },
|
||||||
|
{ to: "/verliehen", label: "Verliehen" },
|
||||||
|
{ to: "/history", label: "Verlauf" },
|
||||||
|
{ to: "/categories", label: "Kategorien", gruppe: "stammdaten" },
|
||||||
|
{ to: "/groups", label: "Gruppen", gruppe: "stammdaten" },
|
||||||
|
{ to: "/locations", label: "Lagerorte", gruppe: "stammdaten" },
|
||||||
|
{ to: "/shops", label: "Shops", gruppe: "stammdaten" },
|
||||||
|
{ to: "/units", label: "Einheiten", gruppe: "stammdaten" },
|
||||||
|
{ to: "/package-types", label: "Gebinde", gruppe: "stammdaten" },
|
||||||
|
{ to: "/masse", label: "Maße", gruppe: "stammdaten" },
|
||||||
|
{ to: "/users", label: "Benutzer", gruppe: "system" },
|
||||||
|
{ to: "/transfer", label: "Import / Export", gruppe: "system" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Pfade einer Gruppe – für die Abschnittsüberschriften der Leiste. */
|
||||||
|
export function gruppenPfade(gruppe) {
|
||||||
|
return NAV_AUSBLENDBAR.filter((e) => e.gruppe === gruppe).map((e) => e.to);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useAuth } from "../auth";
|
|||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
|
import NavigationAuswahl from "../components/NavigationAuswahl";
|
||||||
|
|
||||||
// Kennungen aus dem Rueckweg des Anmeldedienstes (`/konto#sso_error=…`).
|
// Kennungen aus dem Rueckweg des Anmeldedienstes (`/konto#sso_error=…`).
|
||||||
const SSO_FEHLERTEXTE = {
|
const SSO_FEHLERTEXTE = {
|
||||||
@@ -209,6 +210,60 @@ export default function Konto() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SeitenleisteKarte />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die eigene Auswahl: was ICH in der Leiste nicht brauche.
|
||||||
|
*
|
||||||
|
* Was der Administrator ausgeblendet hat, steht gesperrt da – die eigene
|
||||||
|
* Auswahl addiert nur, sie nimmt nichts weg.
|
||||||
|
*/
|
||||||
|
function SeitenleisteKarte() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [daten, setDaten] = useState(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.navigation().then(setDaten).catch((e) => setError(e.message));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function speichern(neu) {
|
||||||
|
setDaten((alt) => ({ ...alt, eigen: neu }));
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await api.setzeEigeneNavigation(neu);
|
||||||
|
window.dispatchEvent(new Event("navigation-geaendert"));
|
||||||
|
toast("Seitenleiste gespeichert.");
|
||||||
|
} catch (err) { setError(err.message); } finally { setBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-head"><Icon name="dashboard" /><h2>Meine Seitenleiste</h2></div>
|
||||||
|
<p className="muted small">
|
||||||
|
Abgewählte Einträge verschwinden nur bei dir. Die Seiten bleiben
|
||||||
|
erreichbar – Lesezeichen und Links funktionieren weiter.
|
||||||
|
</p>
|
||||||
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
{daten == null
|
||||||
|
? <p className="muted small">Wird geladen…</p>
|
||||||
|
: <>
|
||||||
|
<fieldset disabled={busy} style={{ border: 0, padding: 0, margin: 0 }}>
|
||||||
|
<NavigationAuswahl versteckt={daten.eigen || []}
|
||||||
|
gesperrt={daten.vorgabe || []} onChange={speichern} />
|
||||||
|
</fieldset>
|
||||||
|
{(daten.vorgabe || []).length > 0 && (
|
||||||
|
<p className="muted small">
|
||||||
|
Ausgegraute Einträge hat der Administrator für alle ausgeblendet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
|
import NavigationAuswahl from "../components/NavigationAuswahl";
|
||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import BrandingUpload from "../components/BrandingUpload";
|
import BrandingUpload from "../components/BrandingUpload";
|
||||||
import DangerAction from "../components/DangerAction";
|
import DangerAction from "../components/DangerAction";
|
||||||
@@ -192,6 +193,7 @@ export default function Settings() {
|
|||||||
<button className="btn primary" disabled={busy}>{busy ? "Speichern…" : "Speichern"}</button>
|
<button className="btn primary" disabled={busy}>{busy ? "Speichern…" : "Speichern"}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<SeitenleisteCard />
|
||||||
<MealieCard />
|
<MealieCard />
|
||||||
<ApiTokensCard />
|
<ApiTokensCard />
|
||||||
<DangerZoneCard />
|
<DangerZoneCard />
|
||||||
@@ -199,6 +201,53 @@ export default function Settings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Welche Einträge der Seitenleiste es im Haushalt überhaupt gibt.
|
||||||
|
*
|
||||||
|
* Was hier abgewählt ist, sieht niemand – auch kein Administrator. Jeder kann
|
||||||
|
* in seinem Konto zusätzlich eigene Einträge ausblenden, aber nichts davon
|
||||||
|
* wieder einblenden.
|
||||||
|
*/
|
||||||
|
function SeitenleisteCard() {
|
||||||
|
const toast = useToast();
|
||||||
|
const [versteckt, setVersteckt] = useState(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.navigation().then((a) => setVersteckt(a.vorgabe || [])).catch((e) => setError(e.message));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function speichern(neu) {
|
||||||
|
setVersteckt(neu);
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await api.setzeNavigationsVorgabe(neu);
|
||||||
|
// Die Leiste horcht darauf, statt dass man neu laden muss.
|
||||||
|
window.dispatchEvent(new Event("navigation-geaendert"));
|
||||||
|
toast("Seitenleiste gespeichert.");
|
||||||
|
} catch (err) { setError(err.message); } finally { setBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-head"><Icon name="dashboard" /><h2>Seitenleiste</h2></div>
|
||||||
|
<p className="muted small">
|
||||||
|
Was hier abgewählt ist, verschwindet für alle aus der Leiste. Die Seiten
|
||||||
|
selbst bleiben erreichbar – Lesezeichen und Links funktionieren weiter.
|
||||||
|
Übersicht und Einstellungen lassen sich nicht ausblenden.
|
||||||
|
</p>
|
||||||
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
{versteckt == null
|
||||||
|
? <p className="muted small">Wird geladen…</p>
|
||||||
|
: <fieldset disabled={busy} style={{ border: 0, padding: 0, margin: 0 }}>
|
||||||
|
<NavigationAuswahl versteckt={versteckt} onChange={speichern} />
|
||||||
|
</fieldset>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Langlebige Tokens für externe Zugriffe (Home Assistant, Skripte). */
|
/** Langlebige Tokens für externe Zugriffe (Home Assistant, Skripte). */
|
||||||
/**
|
/**
|
||||||
* Verbindung zu einer selbst gehosteten Mealie-Instanz.
|
* Verbindung zu einer selbst gehosteten Mealie-Instanz.
|
||||||
|
|||||||
@@ -1264,3 +1264,11 @@ tr.row-active { background: var(--surface-2); }
|
|||||||
/* "Foto machen"/"Galerie" sind <label> und erben sonst das margin-bottom des
|
/* "Foto machen"/"Galerie" sind <label> und erben sonst das margin-bottom des
|
||||||
globalen label-Stils – dann saessen sie hoeher als der <button> "Entfernen". */
|
globalen label-Stils – dann saessen sie hoeher als der <button> "Entfernen". */
|
||||||
.foto-knoepfe .btn { cursor: pointer; margin: 0; }
|
.foto-knoepfe .btn { cursor: pointer; margin: 0; }
|
||||||
|
|
||||||
|
/* Die Haekchenliste fuer die Seitenleiste: mehrspaltig, damit die zwanzig
|
||||||
|
Eintraege nicht als lange Saeule dastehen. */
|
||||||
|
.nav-auswahl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
|
||||||
|
gap: var(--sp-2) var(--sp-4);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user