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
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
/*
|
|
* 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);
|
|
}
|