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 - +