Selaa lähdekoodia

report page: deep-link view state (filter, grouping, sort, columns, paging) via url query

Alexander Korolev 1 kuukausi sitten
vanhempi
sitoutus
62e13c270e
2 muutettua tiedostoa jossa 42 lisäystä ja 12 poistoa
  1. 35 7
      src/report/page.html
  2. 7 5
      src/report/tests.rs

+ 35 - 7
src/report/page.html

@@ -69,13 +69,40 @@ function cell(r, k) {
   return r[k];
 }
 
-const S = { q: "", group: "mq", sort: "name", dir: 1, hidden: new Set(["title", "image", "port"]),
-            page: 0, size: 50, closed: new Set(), openRow: null };
-// the browser restores form controls across page reloads — follow them
-// (otherwise the select shows the old choice while the table stays grouped
-// by the hardcoded default)
-S.q = document.getElementById("q").value;
-S.group = document.getElementById("group").value;
+// deep-link state: the full view (filter, grouping, sort, columns, paging,
+// collapsed groups) lives in the URL — survives any reload and is shareable
+const P = new URLSearchParams(location.search);
+const size0 = +(P.get("size") ?? 50);
+const S = {
+  q: P.get("q") ?? "",
+  group: P.get("group") ?? "mq",
+  sort: P.get("sort") ?? "name",
+  dir: P.get("dir") ? +P.get("dir") : 1,
+  size: [25, 50, 100, 0].includes(size0) ? size0 : 50,
+  page: Math.max(0, +(P.get("page") ?? 0)),
+  hidden: new Set((P.get("hidden") ?? "title,image,port").split(",").filter(Boolean)),
+  closed: new Set((P.get("closed") ?? "").split("|").map(decodeURIComponent).filter(Boolean)),
+  openRow: P.get("open") ?? null,
+};
+document.getElementById("q").value = S.q;
+document.getElementById("group").value = S.group;
+document.getElementById("size").value = String(S.size);
+function syncUrl() {
+  const p = new URLSearchParams();
+  if (S.q) p.set("q", S.q);
+  if (S.group !== "mq") p.set("group", S.group);
+  if (S.sort !== "name") p.set("sort", S.sort);
+  if (S.dir !== 1) p.set("dir", String(S.dir));
+  if (S.size !== 50) p.set("size", String(S.size));
+  if (S.page) p.set("page", String(S.page));
+  const hidden = [...S.hidden].sort();
+  if (hidden.join(",") !== ["title", "image", "port"].sort().join(",")) p.set("hidden", hidden.join(","));
+  const closed = [...S.closed].map(encodeURIComponent);
+  if (closed.length) p.set("closed", closed.join("|"));
+  if (S.openRow) p.set("open", S.openRow);
+  const qs = p.toString();
+  history.replaceState(null, "", qs ? "?" + qs : location.pathname);
+}
 
 const esc = s => String(s).replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
 
@@ -108,6 +135,7 @@ function gkey(r, level) {
 }
 
 function render() {
+  syncUrl();
   // visible columns
   const vis = COLS.filter(c => !S.hidden.has(c[0]));
   document.querySelector("thead").innerHTML = "<tr>" + vis.map(c => {

+ 7 - 5
src/report/tests.rs

@@ -272,12 +272,14 @@ fn html_page_embeds_data_escaped() {
 }
 
 #[test]
-fn html_page_syncs_state_from_restored_form_controls() {
-    // after a browser page reload the select/input are restored but the JS
-    // state defaults are stale — the state must follow the DOM
+fn html_page_keeps_view_state_in_the_url() {
+    // the full view (filter, grouping, sort, columns, paging) must be
+    // deep-link-backed: parse from the query string on load, and
+    // replaceState it on every render
     let page = html_page(&[]);
-    assert!(page.contains("S.q = document.getElementById(\"q\").value;"));
-    assert!(page.contains("S.group = document.getElementById(\"group\").value;"));
+    assert!(page.contains("new URLSearchParams(location.search)"));
+    assert!(page.contains("history.replaceState"));
+    assert!(page.contains("syncUrl();"));
 }
 
 #[test]