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
|
|
@ -1,40 +1,126 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { renderCell, type CellRenderer } from "@/components/cell-renderers";
|
||||
import { ResultsTable } from "@/components/ResultsTable";
|
||||
import { cellSortValue, type BrowserRow, type ColumnDef } from "@/lib/browser-row";
|
||||
import { nextSort, sortRows } from "@/lib/sort-rows";
|
||||
import { nextSort } from "@/lib/sort-rows";
|
||||
import {
|
||||
applyTableView,
|
||||
browserViewHistory,
|
||||
facetKeysOf,
|
||||
facetValues,
|
||||
parseTableView,
|
||||
serializeTableView,
|
||||
type TableView,
|
||||
type ViewHistory,
|
||||
} from "@/lib/table-view";
|
||||
import type { SortState } from "@/lib/types";
|
||||
|
||||
const FACET_LABELS: Record<string, string> = { tags: "Tags" };
|
||||
|
||||
export function RecordBrowser({
|
||||
rows,
|
||||
columns,
|
||||
heading = "Records",
|
||||
renderers,
|
||||
history,
|
||||
}: {
|
||||
rows: BrowserRow[];
|
||||
columns: ColumnDef[];
|
||||
heading?: string;
|
||||
renderers?: Record<string, CellRenderer>;
|
||||
history?: ViewHistory;
|
||||
}) {
|
||||
const [sort, setSort] = useState<SortState | undefined>({ key: columns[0]?.key ?? "name", dir: 1 });
|
||||
const ordered = sortRows(rows, sort, cellSortValue);
|
||||
const viewHistory = useMemo(() => history ?? browserViewHistory(), [history]);
|
||||
const facetKeys = useMemo(() => facetKeysOf(rows), [rows]);
|
||||
const sortable = (key: string) => columns.some((column) => column.key === key);
|
||||
const baseSort: SortState = { key: columns[0]?.key ?? "name", dir: 1 };
|
||||
const [search, setSearch] = useState("");
|
||||
const [draft, setDraft] = useState("");
|
||||
const view = parseTableView(search, facetKeys, sortable);
|
||||
const sort = view.sort ?? baseSort;
|
||||
const visible = applyTableView(rows, { ...view, sort });
|
||||
|
||||
useEffect(() => {
|
||||
const read = () => {
|
||||
const next = viewHistory.read();
|
||||
setSearch(next);
|
||||
setDraft(parseTableView(next, facetKeys, sortable).q);
|
||||
};
|
||||
read();
|
||||
return viewHistory.subscribe(read);
|
||||
}, [viewHistory, facetKeys, columns]);
|
||||
|
||||
function commit(next: TableView) {
|
||||
const query = serializeTableView(next, baseSort);
|
||||
viewHistory.push(query);
|
||||
setSearch(query ? `?${query}` : "");
|
||||
setDraft(next.q);
|
||||
}
|
||||
|
||||
function toggleFacet(key: string, value: string) {
|
||||
const current = view.facets[key] ?? [];
|
||||
const values = current.includes(value) ? current.filter((item) => item !== value) : [...current, value];
|
||||
commit({ q: view.q, sort, facets: { ...view.facets, [key]: values } });
|
||||
}
|
||||
|
||||
const tableColumns = columns.map((column) => ({
|
||||
key: column.key,
|
||||
label: column.label,
|
||||
sortable: true,
|
||||
render: (row: BrowserRow) => renderCell(column.key, row, renderers),
|
||||
}));
|
||||
|
||||
return (
|
||||
<section id="records" aria-labelledby="records-heading" className="mx-auto max-w-7xl px-4 py-14 sm:px-6">
|
||||
<h2 id="records-heading" className="mb-6 text-2xl">{heading}</h2>
|
||||
<form
|
||||
className="mb-4 flex flex-wrap items-center gap-2"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
commit({ q: draft, sort, facets: view.facets });
|
||||
}}
|
||||
>
|
||||
<label className="flex min-w-0 flex-1 items-center gap-2 text-sm">
|
||||
<span className="sr-only">Search records</span>
|
||||
<input
|
||||
aria-label="Search records"
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
className="h-10 w-full max-w-md rounded-[0.625rem] border border-border bg-card px-3 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="btn">Search</button>
|
||||
</form>
|
||||
{facetKeys.map((key) => {
|
||||
const values = facetValues(rows, key);
|
||||
if (values.length === 0) return null;
|
||||
const selected = view.facets[key] ?? [];
|
||||
return (
|
||||
<div key={key} className="mb-4 flex flex-wrap items-center gap-2" role="group" aria-label={FACET_LABELS[key] ?? key}>
|
||||
<span className="kicker">{FACET_LABELS[key] ?? key}</span>
|
||||
{values.map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
className={selected.includes(value) ? "btn btn-primary" : "btn"}
|
||||
aria-pressed={selected.includes(value)}
|
||||
onClick={() => toggleFacet(key, value)}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<ResultsTable
|
||||
columns={tableColumns}
|
||||
rows={ordered}
|
||||
rows={visible}
|
||||
rowKey={(row) => row.id}
|
||||
sort={sort}
|
||||
onSort={(key) => setSort((current) => nextSort(current, key))}
|
||||
onSort={(key) => commit({ q: view.q, facets: view.facets, sort: nextSort(sort, key) })}
|
||||
empty="No records match this view."
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue