The 9 Batch A facets could not be SET from the web UI, the facets
endpoint had no in-app consumer, and the registry's libraryTypes hints
rendered nowhere. This phase closes all three.
- ONE UNION-ALL sqlc query (FacetCountsUnderFilters) computes all 14
listable facets in a single round trip: each arm omits its OWN
facet's filter block and carries every other registered facet's
block (copied mechanically from SearchMediaItemsUnified) — classic
faceted semantics: with filters active each facet counts under the
others; with none, blocks are neutral and the counts are the v1
global-per-library ones. One query serves both cases. The 14
per-column Batch C queries are deleted.
- Endpoint: no facet param → the new all-facets shape
({facets: {param: [{value, display?, count}]}}), filters bound via
the shared ParseMediaSearchParams, library_id optional (the
All-Libraries view, family lens still enforced); facet=X keeps the
pinned single-facet shape (now routed through the same query with
neutral filters).
- SSR partial GET /media-items/facet-panel: templ renders the drawer's
value lists — registry order, Batch D library-type scoping
(FacetsForLibraryType: manga_type on manga, reading_direction on
manga+comics, narrator on audiobooks; All view = union), prettified
genre labels + language display names, active values marked. Family
lens: out-of-set explicit library_id is 403.
- Drawer wiring: the panel fetches on drawer open and after every
apply with the CURRENT filter query string; a value click toggles
that facet's comma-joined filter (hidden input where the drawer has
no field — same as deep links) and re-applies the form; chips and
URL stay in sync. Values are server-rendered (rendering doctrine);
bookshelf.ts owns only the click/fetch orchestration.
- prettifyGenre moves to internal/utils as the canonical
implementation (services' display enrichment shares it; templ wraps
it; the TS port in bookshelf.ts is unchanged).
- Drift guard extended: FacetCountsUnderFiltersParams must carry every
registry facet's param, the bools as pgtype.Bool.
Tests: under-filter exactness (own-block omission, OR-within,
AND-across), family lens (hidden values absent; 403 on both shapes),
all-shape + pinned-shape coexistence, partial rendering with Batch D
scoping + active marking + 403. Full container suite green except the
documented pre-existing TestAdminCreateUser red.
86 lines
3.2 KiB
Templ
86 lines
3.2 KiB
Templ
package templates
|
|
|
|
// Facet panel partial (faceted plan Phase 7): the filter drawer's
|
|
// value-list sections. The values are SERVER DATA — registry order,
|
|
// library-type scoped (Batch D), counts computed under the active
|
|
// filters — so templ renders them; the click behavior is the drawer's
|
|
// existing client-state flow (bookshelf.ts toggles the facet's filter
|
|
// value and re-applies the form). No JSON innerHTML anywhere.
|
|
|
|
import "strconv"
|
|
|
|
// FacetPanelValue is one browsable facet value.
|
|
type FacetPanelValue struct {
|
|
Value string // raw value (rides the filter param)
|
|
Display string // display form (prettified/display name); falls back to Value
|
|
Count int64
|
|
Active bool // currently selected in this facet's filter
|
|
}
|
|
|
|
// FacetPanelSection is one facet's collapsible value list.
|
|
type FacetPanelSection struct {
|
|
WireParam string // the facet's query param (genre_filter, ...)
|
|
Title string // human label
|
|
Values []FacetPanelValue
|
|
}
|
|
|
|
// facetPanelTitles maps a registry param to its section label (mirrors
|
|
// bookshelf.ts's facetChipLabel names).
|
|
var facetPanelTitles = map[string]string{
|
|
"author": "Author",
|
|
"publisher": "Publisher",
|
|
"narrator": "Narrator",
|
|
"tags": "Tags",
|
|
"series": "Series",
|
|
"genre": "Genre",
|
|
"language": "Language",
|
|
"story_arc": "Story arc",
|
|
"imprint": "Imprint",
|
|
"edition": "Edition",
|
|
"age_rating": "Age rating",
|
|
"format_group": "Format",
|
|
"manga_type": "Manga type",
|
|
"reading_direction": "Reading direction",
|
|
}
|
|
|
|
// FacetPanelTitle is the section label for a registry param (the param
|
|
// itself when unmapped — same fallback the chips use).
|
|
func FacetPanelTitle(param string) string {
|
|
if t, ok := facetPanelTitles[param]; ok {
|
|
return t
|
|
}
|
|
return param
|
|
}
|
|
|
|
templ FacetPanel(sections []FacetPanelSection) {
|
|
<div id="facet-panel-content" class="space-y-2">
|
|
for _, sec := range sections {
|
|
if len(sec.Values) > 0 {
|
|
<details
|
|
class="facet-section rounded-lg border overflow-hidden"
|
|
style="border-color: var(--border);"
|
|
data-param={ sec.WireParam }
|
|
>
|
|
<summary class="facet-section-summary flex items-center justify-between px-3 py-2 cursor-pointer select-none text-sm font-semibold" style="color: var(--text-primary); background-color: var(--bg-secondary);">
|
|
<span>{ sec.Title }</span>
|
|
<span class="text-xs font-normal" style="color: var(--text-secondary);">{ strconv.Itoa(len(sec.Values)) }</span>
|
|
</summary>
|
|
<div class="facet-values max-h-56 overflow-y-auto px-2 py-1.5 space-y-0.5">
|
|
for _, v := range sec.Values {
|
|
<button
|
|
type="button"
|
|
class={ "facet-value-btn w-full flex items-center justify-between gap-2 px-2.5 py-1.5 rounded text-left text-sm transition-colors", templ.KV("facet-value-active", v.Active) }
|
|
data-param={ sec.WireParam }
|
|
data-value={ v.Value }
|
|
>
|
|
<span class="truncate" style="color: var(--text-primary);">{ v.Display }</span>
|
|
<span class="text-xs shrink-0 tabular-nums" style="color: var(--text-secondary);">{ strconv.FormatInt(v.Count, 10) }</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</details>
|
|
}
|
|
}
|
|
</div>
|
|
}
|