Keep search, facets, and sort in the page URL.
CONTENT-WP-0002-T04 writes q, facet values, and sort into history so back and forward restore the table. Assistant: grok Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
This commit is contained in:
parent
d92eb25f92
commit
0cbc7baf49
8 changed files with 503 additions and 9 deletions
113
ui/lib/table-view.ts
Normal file
113
ui/lib/table-view.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* URL shape adapted from OnCo viewParams: `q` for search, repeated facet keys,
|
||||
* and `sort` with a leading "-" for descending.
|
||||
* https://github.com/judegomila/OnCo @ b594d7433bf4e408b44215e25e5304696d6f840e
|
||||
* Copyright (c) 2026 OnCo. MIT License. See ui/NOTICE.
|
||||
*
|
||||
* The compact `v` parameter, table scopes, and domain value normalisers are not included.
|
||||
*/
|
||||
|
||||
import { cellSortValue, type BrowserRow } from "./browser-row";
|
||||
import { sortRows } from "./sort-rows";
|
||||
import type { SortState } from "./types";
|
||||
|
||||
export type TableView = {
|
||||
q: string;
|
||||
facets: Record<string, string[]>;
|
||||
sort?: SortState;
|
||||
};
|
||||
|
||||
export type ViewHistory = {
|
||||
read: () => string;
|
||||
push: (search: string) => void;
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
};
|
||||
|
||||
export function browserViewHistory(): ViewHistory {
|
||||
return {
|
||||
read: () => (typeof window === "undefined" ? "" : window.location.search),
|
||||
push: (search) => {
|
||||
const path = window.location.pathname;
|
||||
window.history.pushState(null, "", search ? `${path}?${search}` : path);
|
||||
},
|
||||
subscribe: (listener) => {
|
||||
window.addEventListener("popstate", listener);
|
||||
return () => window.removeEventListener("popstate", listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function facetKeysOf(rows: readonly BrowserRow[]): string[] {
|
||||
const keys = new Set<string>();
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row.facets)) keys.add(key);
|
||||
}
|
||||
return [...keys].sort();
|
||||
}
|
||||
|
||||
export function facetValues(rows: readonly BrowserRow[], key: string): string[] {
|
||||
const values = new Set<string>();
|
||||
for (const row of rows) {
|
||||
for (const value of row.facets[key] ?? []) values.add(value);
|
||||
}
|
||||
return [...values].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function splitValues(params: URLSearchParams, key: string): string[] {
|
||||
const values: string[] = [];
|
||||
for (const raw of params.getAll(key)) {
|
||||
for (const part of raw.split(",")) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed && !values.includes(trimmed)) values.push(trimmed);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function parseTableView(
|
||||
search: string,
|
||||
facetKeys: readonly string[],
|
||||
sortable: (key: string) => boolean,
|
||||
): TableView {
|
||||
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
||||
const facets: Record<string, string[]> = {};
|
||||
for (const key of facetKeys) {
|
||||
const values = splitValues(params, key);
|
||||
if (values.length > 0) facets[key] = values;
|
||||
}
|
||||
const rawSort = params.get("sort");
|
||||
let sort: SortState | undefined;
|
||||
if (rawSort) {
|
||||
const key = rawSort.replace(/^-/, "");
|
||||
if (sortable(key)) sort = { key, dir: rawSort.startsWith("-") ? -1 : 1 };
|
||||
}
|
||||
return { q: params.get("q") ?? "", facets, sort };
|
||||
}
|
||||
|
||||
export function serializeTableView(view: TableView, baseSort: SortState): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const key of Object.keys(view.facets).sort()) {
|
||||
for (const value of view.facets[key] ?? []) params.append(key, value);
|
||||
}
|
||||
const q = view.q.trim();
|
||||
if (q) params.set("q", q);
|
||||
if (view.sort && (view.sort.key !== baseSort.key || view.sort.dir !== baseSort.dir)) {
|
||||
params.set("sort", `${view.sort.dir === -1 ? "-" : ""}${view.sort.key}`);
|
||||
}
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export function applyTableView(rows: readonly BrowserRow[], view: TableView): BrowserRow[] {
|
||||
const q = view.q.trim().toLowerCase();
|
||||
const filtered = rows.filter((row) => {
|
||||
for (const [key, selected] of Object.entries(view.facets)) {
|
||||
if (selected.length === 0) continue;
|
||||
const have = new Set(row.facets[key] ?? []);
|
||||
if (!selected.some((value) => have.has(value))) return false;
|
||||
}
|
||||
if (!q) return true;
|
||||
const haystack = [row.name, row.tldr, ...Object.values(row.facets).flat()].join("\n").toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
return sortRows(filtered, view.sort, cellSortValue);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue