feat(filters): one vocabulary, server-rendered — the TS mirror retires (item 3.3)

"One filter system to maintain" now holds at the presentation layer
too: the field/op list the builder renders comes from
services.FilterFieldsPayload(scope) — the Go registry's wire face,
scope-filtered and group-ordered — rendered into the bookshelf mount
as a data attribute (templates.JSONArg), ingested once by the Alpine
component. The registry carries operator presentation (labels,
negation, arity) in FilterOpWire alongside the grammar; per-field
value-cell semantics (valueKind) derive from the existing Kind.

The hardcoded TS mirror (OPS, op(), F(), TEXT_OPS/NUM_OPS/DATE_OPS,
FIELD_GROUPS, its known drift surface) is deleted; filter_builder.ts
now ingests the wire (ingestFilterFields — malformed payloads leave
empty menus rather than client-invented rows), and the field menu's
episode fields are HIDDEN on the All-Books page for free (the books
scope payload excludes them — the plan's "hide podcasts from the
rules on the all books page" is now structural, not enforced by a
second hand-kept list).

Unit tests ingest a minimal wire fixture (the payload shape is the
contract, not the mirror); vitest 167 green, tsc + vp clean.
This commit is contained in:
John O'Keefe committed 2026-10-30 19:07:45 -04:00
1 parent 6b2125a45a
commit eb68672390
5 files changed
+345 -341

No files matched your search

+2
View File
@@ -3,6 +3,7 @@ package templates
import (
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/services"
"fmt"
)
@@ -176,6 +177,7 @@ templ BookShelf(
innerHTML templating). Rows are a flat projection of
the tree (depth = indent); groups nest via + group. -->
<div x-data="filterBuilderExtra" x-cloak
data-filter-fields={ templates.JSONArg(services.FilterFieldsPayload("books")) }
x-effect="filtersOpen === true && ensureReadyRule()"
>
<template x-for="row in builderRows" :key="row.key">