diff --git a/web/src/components/DataTable.jsx b/web/src/components/DataTable.jsx
index 3803b42..b474d67 100644
--- a/web/src/components/DataTable.jsx
+++ b/web/src/components/DataTable.jsx
@@ -1,7 +1,9 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
+import { useSearchParams } from "react-router-dom";
import Icon from "./Icon";
import { Skeleton } from "./Skeleton";
+import { leseTabelle, schreibeTabelle } from "../urlState";
/**
* Excel-artige Tabelle in der bestehenden Designsprache: feste Filterzeile je
@@ -34,6 +36,16 @@ export default function DataTable({
}) {
const colByKey = useMemo(() => Object.fromEntries(columns.map((c) => [c.key, c])), [columns]);
+ const [params, setParams] = useSearchParams();
+ /**
+ * Was beim Aufruf in der Adresse stand - einmal beim Mounten festgehalten.
+ *
+ * Die Regel dahinter: Der Browserspeicher ist die persoenliche Vorgabe, die
+ * URL schlaegt sie. Ein verschickter Link zeigt damit beim Empfaenger
+ * dasselbe, auch wenn der sich etwas anderes eingestellt hat.
+ */
+ const [vonUrl] = useState(() => leseTabelle(params, (k) => !!colByKey[k]));
+
const [order, setOrder] = useState(() => {
const p = loadPersisted(id);
const saved = Array.isArray(p.order) ? p.order.filter((k) => colByKey[k]) : [];
@@ -61,6 +73,7 @@ export default function DataTable({
// „Filter löschen" drückt oder anders sortiert – auch nach Navigieren/Neuladen.
// Beim Speichern werden Sets zu Arrays; beim Laden zurück zu Sets.
const [filters, setFilters] = useState(() => {
+ if (vonUrl) return vonUrl.filters;
const saved = loadPersisted(id).filters || {};
const out = {};
for (const [k, v] of Object.entries(saved)) {
@@ -70,28 +83,84 @@ export default function DataTable({
return out;
}); // { key: { text, values:Set|null } }
const [sort, setSort] = useState(() => {
+ if (vonUrl) return vonUrl.sort;
const s = loadPersisted(id).sort;
return s && s.key && colByKey[s.key] ? s : null;
}); // { key, dir }
const [filterShown, setFilterShown] = useState(true);
const [openFilter, setOpenFilter] = useState(null);
const [collapsed, setCollapsed] = useState(() => new Set());
- const [hidden, setHidden] = useState(
- () => new Set((loadPersisted(id).hidden || []).filter((k) => colByKey[k])),
- );
+ const [hidden, setHidden] = useState(() => (vonUrl
+ ? vonUrl.hidden
+ : new Set((loadPersisted(id).hidden || []).filter((k) => colByKey[k]))));
const [chooserOpen, setChooserOpen] = useState(false);
+ /**
+ * Kam die letzte Aenderung an Filter, Sortierung oder Spaltenauswahl von
+ * einer Bedienhandlung?
+ *
+ * Nur dann wird die persoenliche Vorgabe im Browserspeicher fortgeschrieben.
+ * Wer einen fremden gefilterten Link oeffnet, soll seine eigenen
+ * Einstellungen nicht verlieren, bloss weil er geschaut hat.
+ *
+ * Bewusst ein Merker an den Settern und kein "ueberspring den ersten
+ * Durchlauf": React fuehrt Effekte im Entwicklungsmodus doppelt aus, ein
+ * Zaehler am Mounten wird dadurch wertlos.
+ */
+ const vomBenutzer = useRef(false);
+ const setzeFilters = (wert) => { vomBenutzer.current = true; setFilters(wert); };
+ const setzeSort = (wert) => { vomBenutzer.current = true; setSort(wert); };
+ const setzeHidden = (wert) => { vomBenutzer.current = true; setHidden(wert); };
+
+ /**
+ * Die persoenliche Vorgabe fortschreiben.
+ *
+ * Spaltenreihenfolge und -breiten sind reines Layout und entstehen immer
+ * durch Ziehen - die werden ohne Wenn und Aber gemerkt. Filter, Sortierung
+ * und ausgeblendete Spalten koennen dagegen aus einer fremden URL stammen;
+ * dann bleibt stehen, was vorher gespeichert war.
+ */
useEffect(() => {
- // Nur echte Einschränkungen sichern (leerer Text + values=null = kein Filter);
- // Sets werden zu Arrays serialisiert.
- const filtersSer = {};
- for (const [k, v] of Object.entries(filters)) {
- if (!v || (!v.text && v.values == null)) continue;
- filtersSer[k] = { text: v.text || "", values: v.values ? [...v.values] : null };
+ const bisher = loadPersisted(id);
+ const neu = { order, widths };
+ if (vomBenutzer.current) {
+ vomBenutzer.current = false;
+ // Nur echte Einschränkungen sichern (leerer Text + values=null = kein
+ // Filter); Sets werden zu Arrays serialisiert.
+ const filtersSer = {};
+ for (const [k, v] of Object.entries(filters)) {
+ if (!v || (!v.text && v.values == null)) continue;
+ filtersSer[k] = { text: v.text || "", values: v.values ? [...v.values] : null };
+ }
+ Object.assign(neu, { hidden: [...hidden], filters: filtersSer, sort });
+ } else {
+ Object.assign(neu, {
+ hidden: bisher.hidden || [],
+ filters: bisher.filters || {},
+ sort: bisher.sort ?? null,
+ });
}
- savePersisted(id, { order, widths, hidden: [...hidden], filters: filtersSer, sort });
+ savePersisted(id, neu);
}, [id, order, widths, hidden, filters, sort]);
+ /**
+ * Was man sieht, steht in der Adresse - auch wenn es aus der gemerkten
+ * Vorgabe stammt. Erst damit laesst sich ein gefilterter Stand verschicken.
+ *
+ * `setParams` bekommt eine Funktion, damit fremde Parameter derselben Seite
+ * (auf der Kategorienseite etwa `kat` und `panel`) aus dem AKTUELLEN Stand
+ * kommen und nicht aus einem veralteten Abschluss. Immer `replace`: Ein
+ * Filter ist kein Ort, zu dem man zurueckblaettern will, und jeder
+ * Tastendruck im Filterfeld wuerde sonst einen Verlaufseintrag anlegen.
+ */
+ useEffect(() => {
+ setParams((alt) => {
+ const neu = schreibeTabelle(alt, { filters, sort, hidden });
+ return neu.toString() === alt.toString() ? alt : neu;
+ }, { replace: true });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filters, sort, hidden]);
+
// Containerbreite messen, damit die grow-Spalte den Rest füllt.
const wrapRef = useRef(null);
const [containerWidth, setContainerWidth] = useState(0);
@@ -135,7 +204,7 @@ export default function DataTable({
}, [chooserOpen]);
function toggleHidden(key) {
- setHidden((s) => {
+ setzeHidden((s) => {
const n = new Set(s);
if (n.has(key)) { n.delete(key); return n; }
// Mindestens eine Spalte sichtbar lassen.
@@ -254,7 +323,7 @@ export default function DataTable({
function toggleSort(c) {
if (!c.sortValue && !c.filterText) return;
- setSort((s) => {
+ setzeSort((s) => {
if (!s || s.key !== c.key) return { key: c.key, dir: "asc" };
if (s.dir === "asc") return { key: c.key, dir: "desc" };
return null;
@@ -362,7 +431,7 @@ export default function DataTable({
// Sortierung bleiben; der Speicher-Effekt schreibt den neuen Stand.
setOrder(columns.map((c) => c.key));
setWidths({});
- setHidden(new Set());
+ setzeHidden(new Set());
}
return (
@@ -372,7 +441,7 @@ export default function DataTable({
onClick={() => setFilterShown((v) => !v)}>
Filter
-
+