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.
48 lines
1.6 KiB
Go
48 lines
1.6 KiB
Go
package utils
|
|
|
|
// Genre display rendering (faceted plan Batch B / Phase 7): the
|
|
// canonically-lowercase genre value is title-cased at DISPLAY time
|
|
// only — never in the DB. Canonical home: the filter panel (services),
|
|
// templ (templates wraps this), and the TS port in bookshelf.ts all
|
|
// share these exact rules.
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// genreSmallWords stay lowercase in title-case rendering (except the
|
|
// first word of the value).
|
|
var genreSmallWords = map[string]bool{
|
|
"a": true, "an": true, "and": true, "of": true, "the": true,
|
|
"in": true, "on": true, "for": true, "to": true, "or": true,
|
|
}
|
|
|
|
// PrettifyGenre renders the canonically-lowercase genre value in title
|
|
// case at display time (Batch B transform — never in the DB). Only
|
|
// values that are ENTIRELY lowercase get transformed; anything else is
|
|
// returned verbatim, protecting user-entered mixed case. Small words
|
|
// stay down except in the first position; "&" has no case and passes
|
|
// through. "science fiction" → "Science Fiction", "sword & sorcery" →
|
|
// "Sword & Sorcery".
|
|
func PrettifyGenre(value string) string {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return ""
|
|
}
|
|
if trimmed != strings.ToLower(trimmed) {
|
|
return trimmed // mixed case = user-authored; display as written
|
|
}
|
|
words := strings.Fields(trimmed)
|
|
out := make([]string, len(words))
|
|
for i, w := range words {
|
|
if i > 0 && genreSmallWords[w] {
|
|
out[i] = w
|
|
continue
|
|
}
|
|
runes := []rune(w)
|
|
out[i] = string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
|
|
}
|
|
return strings.Join(out, " ")
|
|
}
|