Startseite heisst /home, Bearbeiten steht in der URL
Die Adresszeile sagte nicht, wo man ist. Die Startseite lag auf "/", waehrend jede andere Seite einen sprechenden Pfad hat, und der Bearbeitungsmodus des Dashboards stand nirgends: Neuladen warf einen heraus, der Stand liess sich nicht verschicken, die Zurueck-Taste tat nichts. Jetzt /home und /home/edit, fuer weitere Dashboards /d/:id und /d/:id/edit. "/" und jeder unbekannte Pfad leiten sichtbar auf /home um. Der Kern der Arbeit steckte in Dashboard.jsx: Der Modus war nicht als eigenes Flag gefuehrt, sondern aus `entwurf !== null` abgeleitet - eine Variable, die zugleich Modus und ungespeicherte Aenderung war. Der Modus kommt jetzt als Prop aus der Route, `entwurf` ist nur noch das, was sein Name sagt. Weil der Entwurf allein im Speicher lebt, baut ihn ein Effekt aus dem gespeicherten Layout auf, sobald dieses geladen ist - sonst koennte man /home/edit nicht direkt aufrufen oder neu laden. Dabei eine Falle vermieden: `aktives` ist nach jedem setDashboards ein neues Objekt. Ohne Merker, zu welchem Dashboard der Entwurf gehoert, haette jedes Neuladen der Liste waehrend des Bearbeitens die ungespeicherte Anordnung verworfen. "Anordnen" navigiert mit push, damit die Zurueck-Taste den Modus verlaesst; "Speichern" und "Abbrechen" mit replace, damit sie nicht hineinfuehrt. /home/edit bei verbindlicher Admin-Vorgabe faellt auf die Ansicht zurueck, statt eine Leiste ohne Speichern-Knopf zu zeigen. Dazu zwei Dinge, die sonst stillschweigend gebrochen waeren: - Der ErrorBoundary-Schluessel hing am ganzen Pfad. /home/edit haette als andere Seite gegolten, den Baum neu eingehaengt und beim Umschalten Dashboards, Artikel und Lagerorte erneut geladen. Jetzt nur das erste Pfadsegment - im Mitschnitt steht beim Umschalten kein einziger Aufruf. - Die Seitenleisten-Links trugen `end` (exakter Match), weil "/" sonst ueberall als Treffer galt. Bei /home ist das nicht noetig, und ohne `end` bleibt der Eintrag auch unter /home/edit markiert. Von Hand durchgeklickt: "/" und "/gibtsnicht" landen auf /home; Anordnen -> /home/edit, Neuladen bleibt im Modus mit sieben Karten als Entwurf, Zurueck verlaesst ihn, nach Abbrechen fuehrt Zurueck nicht wieder hinein; zweites Dashboard -> /d/1/edit; bei erzwungener Vorgabe faellt /home/edit auf /home zurueck. Backend-Suite unveraendert bei 517. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
98db3339a2
commit
a1b10d9ec3
+25
-8
@@ -108,11 +108,13 @@ function Sidebar() {
|
|||||||
<BrandMark />
|
<BrandMark />
|
||||||
</div>
|
</div>
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
<NavItem to="/" end icon="dashboard" label="Übersicht" />
|
{/* Ohne `end`: Der Eintrag soll auch bei /home/edit markiert bleiben.
|
||||||
|
Bei "/" war `end` noetig, weil sonst JEDER Pfad als Treffer galt. */}
|
||||||
|
<NavItem to="/home" icon="dashboard" label="Übersicht" />
|
||||||
{/* Ab dem zweiten Dashboard lohnt die Aufzaehlung; bei einem einzigen
|
{/* Ab dem zweiten Dashboard lohnt die Aufzaehlung; bei einem einzigen
|
||||||
waere sie nur eine Verdopplung von "Übersicht". */}
|
waere sie nur eine Verdopplung von "Übersicht". */}
|
||||||
{dashboards.length > 1 && dashboards.map((d, index) => (
|
{dashboards.length > 1 && dashboards.map((d, index) => (
|
||||||
<NavLink key={d.id} to={index === 0 ? "/" : `/d/${d.id}`} end
|
<NavLink key={d.id} to={index === 0 ? "/home" : `/d/${d.id}`}
|
||||||
className={({ isActive }) => (isActive ? "nav-link sub active" : "nav-link sub")}>
|
className={({ isActive }) => (isActive ? "nav-link sub active" : "nav-link sub")}>
|
||||||
<span className="label">{d.name}</span>
|
<span className="label">{d.name}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -206,7 +208,7 @@ function Protected({ children, adminOnly = false, wide = false }) {
|
|||||||
if (loading) return <div className="center muted" style={{ padding: 60 }}>Lädt…</div>;
|
if (loading) return <div className="center muted" style={{ padding: 60 }}>Lädt…</div>;
|
||||||
if (startFehler) return <StartFehler text={startFehler} erneut={erneutVersuchen} />;
|
if (startFehler) return <StartFehler text={startFehler} erneut={erneutVersuchen} />;
|
||||||
if (!user) return <Navigate to="/login" replace />;
|
if (!user) return <Navigate to="/login" replace />;
|
||||||
if (adminOnly && !isAdmin) return <Navigate to="/" replace />;
|
if (adminOnly && !isAdmin) return <Navigate to="/home" replace />;
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
@@ -215,8 +217,14 @@ function Protected({ children, adminOnly = false, wide = false }) {
|
|||||||
<div className={wide ? "content content--wide" : "content"}>
|
<div className={wide ? "content content--wide" : "content"}>
|
||||||
{/* Je Seite eine eigene Grenze: stuerzt eine ab, bleibt wenigstens
|
{/* Je Seite eine eigene Grenze: stuerzt eine ab, bleibt wenigstens
|
||||||
die Navigation bedienbar. `key` setzt sie beim Seitenwechsel
|
die Navigation bedienbar. `key` setzt sie beim Seitenwechsel
|
||||||
zurueck, sonst haenge man nach einem Fehler dauerhaft fest. */}
|
zurueck, sonst haenge man nach einem Fehler dauerhaft fest.
|
||||||
<ErrorBoundary key={pfad} bereich="Diese Seite">{children}</ErrorBoundary>
|
|
||||||
|
Nur das ERSTE Pfadsegment: Sonst gilt /home/edit als andere Seite
|
||||||
|
als /home, der ganze Baum wird neu eingehaengt und laedt beim
|
||||||
|
Umschalten in den Bearbeitungsmodus alles noch einmal. */}
|
||||||
|
<ErrorBoundary key={pfad.split("/")[1] || "home"} bereich="Diese Seite">
|
||||||
|
{children}
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,10 +238,19 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={loading ? null : user ? <Navigate to="/" replace /> : <Login />}
|
element={loading ? null : user ? <Navigate to="/home" replace /> : <Login />}
|
||||||
/>
|
/>
|
||||||
<Route path="/" element={<Protected wide><Dashboard /></Protected>} />
|
{/* Die Startseite hat einen Namen, damit in der Adresszeile steht, wo
|
||||||
|
man ist; "/" leitet sichtbar dorthin um. Der Bearbeitungsmodus ist
|
||||||
|
ein eigener Pfad statt eines Zustands im Speicher: So ueberlebt er
|
||||||
|
ein Neuladen, laesst sich verschicken, und die Zurueck-Taste
|
||||||
|
verlaesst ihn. Den Modus gibt die Prop vor, damit der Pfad nicht ein
|
||||||
|
zweites Mal in der Komponente steht. */}
|
||||||
|
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||||
|
<Route path="/home" element={<Protected wide><Dashboard /></Protected>} />
|
||||||
|
<Route path="/home/edit" element={<Protected wide><Dashboard bearbeiten /></Protected>} />
|
||||||
<Route path="/d/:id" element={<Protected wide><Dashboard /></Protected>} />
|
<Route path="/d/:id" element={<Protected wide><Dashboard /></Protected>} />
|
||||||
|
<Route path="/d/:id/edit" element={<Protected wide><Dashboard bearbeiten /></Protected>} />
|
||||||
<Route path="/checkin" element={<Protected><CheckIn /></Protected>} />
|
<Route path="/checkin" element={<Protected><CheckIn /></Protected>} />
|
||||||
<Route path="/checkout" element={<Protected><CheckOut /></Protected>} />
|
<Route path="/checkout" element={<Protected><CheckOut /></Protected>} />
|
||||||
<Route path="/lebensmittel" element={<Protected wide><Products fixedType="food" /></Protected>} />
|
<Route path="/lebensmittel" element={<Protected wide><Products fixedType="food" /></Protected>} />
|
||||||
@@ -270,7 +287,7 @@ export default function App() {
|
|||||||
<Route path="/auth/callback" element={<OidcCallback />} />
|
<Route path="/auth/callback" element={<OidcCallback />} />
|
||||||
{/* Ebenfalls oeffentlich: hier ist man noch nicht angemeldet. */}
|
{/* Ebenfalls oeffentlich: hier ist man noch nicht angemeldet. */}
|
||||||
<Route path="/sso-neu" element={<SsoNeu />} />
|
<Route path="/sso-neu" element={<SsoNeu />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/home" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-13
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import GridLayout, { WidthProvider } from "react-grid-layout";
|
import GridLayout, { WidthProvider } from "react-grid-layout";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
@@ -41,7 +41,14 @@ function neueKennung(art, vorhanden) {
|
|||||||
* Eine Karte ist eine *Instanz*: „i“ ist ihre Kennung, „type“ ihre Art. Erst
|
* Eine Karte ist eine *Instanz*: „i“ ist ihre Kennung, „type“ ihre Art. Erst
|
||||||
* dadurch lassen sich zwei Verlaufskarten für zwei Artikel nebeneinander legen.
|
* dadurch lassen sich zwei Verlaufskarten für zwei Artikel nebeneinander legen.
|
||||||
*/
|
*/
|
||||||
export default function Dashboard() {
|
/**
|
||||||
|
* `bearbeiten` kommt aus der Route (/home/edit bzw. /d/:id/edit), nicht aus
|
||||||
|
* dem Speicher. Vorher war der Modus aus `entwurf !== null` abgeleitet - ein
|
||||||
|
* Neuladen warf einen heraus, der Stand liess sich nicht verschicken, und die
|
||||||
|
* Zurueck-Taste tat nichts. `entwurf` bleibt, ist jetzt aber nur noch das,
|
||||||
|
* was sein Name sagt: die ungespeicherte Aenderung.
|
||||||
|
*/
|
||||||
|
export default function Dashboard({ bearbeiten = false }) {
|
||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -62,9 +69,14 @@ export default function Dashboard() {
|
|||||||
const aktivesId = routeId ? Number(routeId) : dashboards[0]?.id;
|
const aktivesId = routeId ? Number(routeId) : dashboards[0]?.id;
|
||||||
const aktives = dashboards.find((d) => d.id === aktivesId) ?? dashboards[0];
|
const aktives = dashboards.find((d) => d.id === aktivesId) ?? dashboards[0];
|
||||||
|
|
||||||
const bearbeiten = entwurf !== null;
|
|
||||||
const aktuell = entwurf ?? bereinige(aktives?.layout);
|
const aktuell = entwurf ?? bereinige(aktives?.layout);
|
||||||
|
|
||||||
|
// Pfad dieses Dashboards - Grundlage fuer das Hin und Her zwischen Ansehen
|
||||||
|
// und Bearbeiten.
|
||||||
|
const basisPfad = routeId ? `/d/${routeId}` : "/home";
|
||||||
|
// Zu welchem Dashboard der aktuelle Entwurf gehoert (siehe Effekt unten).
|
||||||
|
const entwurfFuer = useRef(null);
|
||||||
|
|
||||||
async function laden() {
|
async function laden() {
|
||||||
try {
|
try {
|
||||||
const antwort = await api.dashboards();
|
const antwort = await api.dashboards();
|
||||||
@@ -90,9 +102,48 @@ export default function Dashboard() {
|
|||||||
api.listLocations().then(setLocations).catch(() => {});
|
api.listLocations().then(setLocations).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Beim Wechsel des Dashboards einen offenen Entwurf verwerfen – sonst
|
/**
|
||||||
// landete er beim Speichern auf dem falschen.
|
* Entwurf und Route zur Deckung bringen.
|
||||||
useEffect(() => { setEntwurf(null); setMenuOffen(false); setKonfigKarte(null); }, [aktivesId]);
|
*
|
||||||
|
* Beim Verlassen des Bearbeitungsmodus wird verworfen; beim Betreten wird
|
||||||
|
* aus dem GESPEICHERTEN Layout ein Entwurf gemacht. Letzteres ist noetig,
|
||||||
|
* weil der Entwurf nur im Speicher lebt: Wer /home/edit direkt aufruft oder
|
||||||
|
* neu laedt, braucht einen frischen. Das Layout kommt asynchron, deshalb
|
||||||
|
* wartet der Effekt auf `aktives` statt einmalig zu laufen.
|
||||||
|
*
|
||||||
|
* Deckt den frueheren Reset beim Dashboard-Wechsel mit ab: `aktivesId` ist
|
||||||
|
* in der Abhaengigkeitsliste, ein Wechsel baut den Entwurf also neu auf dem
|
||||||
|
* richtigen Dashboard auf, statt ihn auf das falsche zu speichern.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bearbeiten) {
|
||||||
|
setEntwurf(null);
|
||||||
|
setMenuOffen(false);
|
||||||
|
setKonfigKarte(null);
|
||||||
|
entwurfFuer.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!aktives) return; // Layout noch nicht geladen
|
||||||
|
// Gehoert der Entwurf schon zu diesem Dashboard, bleibt er stehen. Ohne
|
||||||
|
// diese Sperre wuerde jedes Neuladen der Dashboard-Liste waehrend des
|
||||||
|
// Bearbeitens die ungespeicherte Anordnung verwerfen - `aktives` ist nach
|
||||||
|
// jedem setDashboards ein neues Objekt.
|
||||||
|
if (entwurfFuer.current === aktives.id) return;
|
||||||
|
entwurfFuer.current = aktives.id;
|
||||||
|
setEntwurf(bereinige(aktives.layout).map((k) => ({ ...k })));
|
||||||
|
setMenuOffen(false);
|
||||||
|
setKonfigKarte(null);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [bearbeiten, aktivesId, aktives]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wer /home/edit aufruft, obwohl gar nicht bearbeitet werden darf, landet
|
||||||
|
* wieder in der Ansicht - sonst saehe er eine Leiste ohne Speichern-Knopf.
|
||||||
|
* `replace`, damit die Zurueck-Taste nicht wieder hierher fuehrt.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (bearbeiten && erzwungen) navigate(basisPfad, { replace: true });
|
||||||
|
}, [bearbeiten, erzwungen, basisPfad, navigate]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unbekannte Karten aussortieren – z.B. nach einem Update, das eine entfernt.
|
* Unbekannte Karten aussortieren – z.B. nach einem Update, das eine entfernt.
|
||||||
@@ -122,8 +173,17 @@ export default function Dashboard() {
|
|||||||
[aktuell],
|
[aktuell],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Nur navigieren - den Entwurf baut der Effekt oben. Mit `push`, damit die
|
||||||
|
// Zurueck-Taste den Bearbeitungsmodus wieder verlaesst.
|
||||||
function starteBearbeitung() {
|
function starteBearbeitung() {
|
||||||
setEntwurf(aktuell.map((k) => ({ ...k })));
|
navigate(`${basisPfad}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zurueck in die Ansicht. `replace`, weil der Bearbeitungsmodus nach
|
||||||
|
// Speichern oder Abbrechen erledigt ist - die Zurueck-Taste soll nicht
|
||||||
|
// hineinfuehren.
|
||||||
|
function beendeBearbeitung() {
|
||||||
|
navigate(basisPfad, { replace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function hinzufuegen(art) {
|
function hinzufuegen(art) {
|
||||||
@@ -153,8 +213,7 @@ export default function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
const gespeichert = await api.updateDashboard(aktives.id, { layout: schlank(entwurf) });
|
const gespeichert = await api.updateDashboard(aktives.id, { layout: schlank(entwurf) });
|
||||||
setDashboards((alt) => alt.map((d) => (d.id === gespeichert.id ? gespeichert : d)));
|
setDashboards((alt) => alt.map((d) => (d.id === gespeichert.id ? gespeichert : d)));
|
||||||
setEntwurf(null);
|
beendeBearbeitung();
|
||||||
setKonfigKarte(null);
|
|
||||||
toast("Anordnung gespeichert.");
|
toast("Anordnung gespeichert.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
@@ -211,7 +270,7 @@ export default function Dashboard() {
|
|||||||
setEntwurf(null);
|
setEntwurf(null);
|
||||||
await laden();
|
await laden();
|
||||||
meldeAenderung();
|
meldeAenderung();
|
||||||
navigate("/");
|
navigate("/home");
|
||||||
toast("Dashboard gelöscht.");
|
toast("Dashboard gelöscht.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
@@ -258,7 +317,7 @@ export default function Dashboard() {
|
|||||||
setEntwurf(null);
|
setEntwurf(null);
|
||||||
await laden();
|
await laden();
|
||||||
meldeAenderung();
|
meldeAenderung();
|
||||||
navigate("/");
|
navigate("/home");
|
||||||
toast("Auf die Vorgabe zurückgesetzt.");
|
toast("Auf die Vorgabe zurückgesetzt.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
@@ -338,8 +397,7 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn primary" onClick={speichern}>Speichern</button>
|
<button className="btn primary" onClick={speichern}>Speichern</button>
|
||||||
<button className="btn ghost"
|
<button className="btn ghost" onClick={beendeBearbeitung}>
|
||||||
onClick={() => { setEntwurf(null); setMenuOffen(false); setKonfigKarte(null); }}>
|
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function Login() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await login(username, password);
|
await login(username, password);
|
||||||
navigate("/");
|
navigate("/home");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || "Anmeldung fehlgeschlagen");
|
setError(err.message || "Anmeldung fehlgeschlagen");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function OidcCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenUebernehmen(token)
|
tokenUebernehmen(token)
|
||||||
.then(() => navigate("/", { replace: true }))
|
.then(() => navigate("/home", { replace: true }))
|
||||||
.catch((err) => setFehler(err.message || "Anmeldung fehlgeschlagen."));
|
.catch((err) => setFehler(err.message || "Anmeldung fehlgeschlagen."));
|
||||||
}, [tokenUebernehmen, navigate]);
|
}, [tokenUebernehmen, navigate]);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function SsoNeu() {
|
|||||||
try {
|
try {
|
||||||
const token = await api.oidcPendingAnlegen(code);
|
const token = await api.oidcPendingAnlegen(code);
|
||||||
await tokenUebernehmen(token.access_token);
|
await tokenUebernehmen(token.access_token);
|
||||||
navigate("/", { replace: true });
|
navigate("/home", { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -72,7 +72,7 @@ export default function SsoNeu() {
|
|||||||
try {
|
try {
|
||||||
const token = await api.oidcPendingVerknuepfen(code, username, password);
|
const token = await api.oidcPendingVerknuepfen(code, username, password);
|
||||||
await tokenUebernehmen(token.access_token);
|
await tokenUebernehmen(token.access_token);
|
||||||
navigate("/", { replace: true });
|
navigate("/home", { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFehler(err.message);
|
setFehler(err.message);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user