feat(podcasts): the rules page retires — the collection builder IS the filter builder (item 3.4)

The smart-collection editor at /collections/:id/rules now hosts the
SAME builder island as All-Books (FilterBuilderRows — the rows markup
extracted into one shared templ both mounts render; the shared
filterBuilderExtra component). One vocabulary, one engine, one row UI;
the collection differences are exactly three: the payload is the
collection's scope (episode trees get the episode fields — books
trees hide them), Apply writes the tree to the collection (PUT
filter_tree through the validated write path), and a legacy rules
array preloads as its CONVERTED tree (RulesToFilterTree rendered
server-side) so the upgrade lands at the first Apply — read-compat
never mutates data silently.

Retired with the page: the per-rule CRUD endpoints and test-rules
(their handler bodies, routes, api-docs rows, and the dead collections.ts
island functions — the file's LIVE halves, the collection modal/navigate/
grid-refresh, stay; the dead rules half went). match-mode endpoint and
preview survive (legacy channel + the dashboard custom-section-builder,
flagged for its own migration). Contract-pin changelog documents the
deletions per the asymmetry rule.

The builder component gains collection mounts: scopeKey (the row
universe for counts/apply), data-tree prefill, treeJSON (the apply
wire with scope riding the top level), and an apply hook (the page
owns the write; All-Books never sets one). ensureReadyRule defaults to
the scope's first field instead of hardcoding author.

Tests migrated onto trees: TestSmartCollections' live-membership /
reject-mutations / include-auto / system-collection subtests now drive
TREE collections; the deleted-endpoint subtests die with them.
This commit is contained in:
John O'Keefe committed 2026-10-30 19:22:17 -04:00
1 parent eb68672390
commit 4f71eda6be
16 files changed
+518 -1422

No files matched your search

+25 -2
View File
@@ -701,6 +701,29 @@ func registerFrontendRoutes(cfg *Config) {
return renderErrorPage(c, "System collections cannot have rules", "not_allowed")
}
// The editor's vocabulary + prefill (item 3.4): the unified
// builder in the collection's scope. A stored tree renders as-is;
// a legacy rules array preloads as its CONVERTED tree
// (RulesToFilterTree) — the upgrade lands at the first Apply.
scope := "books"
treeJSON := "{}"
if len(collection.FilterTree) > 0 {
if t, perr := services.ParseFilterTree(string(collection.FilterTree)); perr == nil {
scope = t.FilterScope()
if b, merr := json.Marshal(t); merr == nil {
treeJSON = string(b)
}
}
} else if rules := cfg.CollectionHandler.CollectionService().ParseRules(collection.AutoAssignRules); len(rules) > 0 {
if t, cerr := services.RulesToFilterTree(rules, collection.RuleMatchMode); cerr == nil {
scope = t.FilterScope()
if b, merr := json.Marshal(t); merr == nil {
treeJSON = string(b)
}
}
}
fieldsJSON := templates.JSONArg(services.FilterFieldsPayload(scope))
colData := templates.CollectionData{
ID: collectionID,
Name: collection.Name,
@@ -708,11 +731,11 @@ func registerFrontendRoutes(cfg *Config) {
Color: utils.PgTextString(collection.Color),
Icon: utils.PgTextString(collection.Icon),
IsSystem: collection.IsSystemCollection.Bool,
IsSmart: cfg.CollectionHandler.CollectionService().IsSmartCollection(collection.AutoAssignRules),
IsSmart: services.IsCollectionSmart(collection),
}
var buf bytes.Buffer
err = templates.CollectionRules(user, colData).Render(c.Request().Context(), &buf)
err = templates.CollectionRules(user, colData, fieldsJSON, treeJSON, scope).Render(c.Request().Context(), &buf)
if err != nil {
return err
}