From 3bcb84c1f8d334925893dc9a89d12e5088336c3c Mon Sep 17 00:00:00 2001 From: Johan Marais Date: Thu, 8 Oct 2026 13:42:45 +0200 Subject: [PATCH 1/2] fix(sections): show recipe names instead of raw section type keys A Home section saved without a typed title stored its section_type (for example "trending_on_server") as the title, and clients showed that key as the row heading. The resolver now replaces a blank title, or one equal to the section type, with the recipe's display name: the preset whose default params match the section config, else the recipe's first preset. Existing stored sections recover without a migration. The section editor now saves the same preset name when the title is left blank. Fixes #2096 Co-Authored-By: Claude Opus 5.5 --- internal/sections/resolver.go | 45 ++++++++++++++++++- internal/sections/resolver_test.go | 15 +++++++ .../sections/SectionEditorDrawer.tsx | 10 +++-- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/internal/sections/resolver.go b/internal/sections/resolver.go index 4d9bdf4a42..5f4ba1b1fc 100644 --- a/internal/sections/resolver.go +++ b/internal/sections/resolver.go @@ -2,7 +2,11 @@ package sections import ( "encoding/json" + "reflect" "sort" + "strings" + + "github.com/Silo-Server/silo-server/internal/sections/recipes" ) const jsonNullLiteral = "null" @@ -61,6 +65,7 @@ func Resolve(admin []*PageSection, overrides []ProfileSectionOverride) []Resolve } } + rs.Title = readableTitle(rs.SectionType, rs.Title, rs.Config) result = append(result, rs) } @@ -130,6 +135,7 @@ func ResolveForSettings(admin []*PageSection, overrides []ProfileSectionOverride } } + rs.Title = readableTitle(rs.SectionType, rs.Title, rs.Config) result = append(result, rs) } @@ -185,7 +191,7 @@ func resolveUserAdded(o ProfileSectionOverride) ResolvedSection { return ResolvedSection{ ID: o.ID, SectionType: sectionType, - Title: title, + Title: readableTitle(sectionType, title, cfg), Featured: featured, ItemLimit: limit, Config: cfg, @@ -194,3 +200,40 @@ func resolveUserAdded(o ProfileSectionOverride) ResolvedSection { Hidden: o.Hidden, } } + +// readableTitle replaces a blank title, or one that is just the raw +// section_type key (older editors saved that when no title was typed), with +// the recipe's display name: the preset whose default params all match the +// section config, else the recipe's first preset. Other titles pass through. +func readableTitle(sectionType SectionType, title string, config json.RawMessage) string { + if t := strings.TrimSpace(title); t != "" && t != string(sectionType) { + return title + } + rec, ok := recipes.Get(string(sectionType)) + if !ok { + return title + } + presets := rec.Definition().Presets + if len(presets) == 0 { + return title + } + var cfg map[string]any + _ = json.Unmarshal(config, &cfg) + for _, p := range presets { + var params map[string]any + if json.Unmarshal(p.DefaultParams, ¶ms) != nil { + continue + } + matched := true + for k, v := range params { + if !reflect.DeepEqual(cfg[k], v) { + matched = false + break + } + } + if matched { + return p.DisplayName + } + } + return presets[0].DisplayName +} diff --git a/internal/sections/resolver_test.go b/internal/sections/resolver_test.go index 1cf3a7a13d..685a120ee5 100644 --- a/internal/sections/resolver_test.go +++ b/internal/sections/resolver_test.go @@ -257,3 +257,18 @@ func TestResolveBackwardCompatLegacyUserAdded(t *testing.T) { t.Fatalf("legacy user-added not resolved correctly: %+v", resolved) } } + +func TestResolve_ReplacesRawSectionTypeTitle(t *testing.T) { + admin := []*PageSection{ + {ID: "1", Position: 0, SectionType: "trending_on_server", Title: "trending_on_server", Config: json.RawMessage(`{"window":"7d"}`)}, + {ID: "2", Position: 1, SectionType: "trending_on_server", Title: "Hot Right Now", Config: json.RawMessage(`{"window":"7d"}`)}, + } + + result := Resolve(admin, nil) + if result[0].Title != "Trending This Week" { + t.Errorf("raw-key title = %q, want %q", result[0].Title, "Trending This Week") + } + if result[1].Title != "Hot Right Now" { + t.Errorf("custom title = %q, want unchanged", result[1].Title) + } +} diff --git a/web/src/components/sections/SectionEditorDrawer.tsx b/web/src/components/sections/SectionEditorDrawer.tsx index a38233a439..01c5758901 100644 --- a/web/src/components/sections/SectionEditorDrawer.tsx +++ b/web/src/components/sections/SectionEditorDrawer.tsx @@ -326,6 +326,10 @@ export default function SectionEditorDrawer(props: SectionEditorDrawerProps) { ? lookupRecipe(props.recipeCatalog, sectionType) : undefined; const isKnownRecipe = Boolean(recipeDef); + // A blank title saves the recipe's preset name, never the raw section_type key. + const defaultTitle = + matchRecipePresetFor(props.recipeCatalog, sectionType, recipeParams)?.display_name ?? + sectionTypeLabel(sectionType); const showCollectionPicker = sectionType === "collection"; const showLegacyFilter = isLegacyFilterType(sectionType); const showRecipeParams = !showCollectionPicker && !showLegacyFilter && isKnownRecipe; @@ -381,7 +385,7 @@ export default function SectionEditorDrawer(props: SectionEditorDrawerProps) { buildProfileSectionSaveEntry({ section: props.section, sectionType, - title, + title: title.trim() || defaultTitle, itemLimit, featured, queryDefinition, @@ -403,7 +407,7 @@ export default function SectionEditorDrawer(props: SectionEditorDrawerProps) { scope: props.scope, currentLibraryId: props.currentLibraryId, sectionType, - title, + title: title.trim() || defaultTitle, itemLimit, featured, enabled, @@ -506,7 +510,7 @@ export default function SectionEditorDrawer(props: SectionEditorDrawerProps) { setTitle(event.target.value)} - placeholder={sectionTypeLabel(sectionType)} + placeholder={defaultTitle} /> From c1d33f51effaeff605887bbab85b0f62b25fe441 Mon Sep 17 00:00:00 2001 From: Johan Marais Date: Thu, 8 Oct 2026 14:48:31 +0200 Subject: [PATCH 2/2] fix(sections): pick the most specific preset for a raw-key title Match the web editor's matchRecipePreset: when several presets fit the section config, the one matching the most keys names the section, so {"format":"4k","sort":"recent"} resolves to "New in 4K". Co-Authored-By: Claude Opus 5.5 --- internal/sections/resolver.go | 12 +++++++----- internal/sections/resolver_test.go | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/sections/resolver.go b/internal/sections/resolver.go index 5f4ba1b1fc..727015a4e4 100644 --- a/internal/sections/resolver.go +++ b/internal/sections/resolver.go @@ -203,8 +203,9 @@ func resolveUserAdded(o ProfileSectionOverride) ResolvedSection { // readableTitle replaces a blank title, or one that is just the raw // section_type key (older editors saved that when no title was typed), with -// the recipe's display name: the preset whose default params all match the -// section config, else the recipe's first preset. Other titles pass through. +// the recipe's display name: the preset whose default params match the +// section config on the most keys (as the web editor's matchRecipePreset +// picks), else the recipe's first preset. Other titles pass through. func readableTitle(sectionType SectionType, title string, config json.RawMessage) string { if t := strings.TrimSpace(title); t != "" && t != string(sectionType) { return title @@ -219,6 +220,7 @@ func readableTitle(sectionType SectionType, title string, config json.RawMessage } var cfg map[string]any _ = json.Unmarshal(config, &cfg) + best, bestScore := presets[0].DisplayName, -1 for _, p := range presets { var params map[string]any if json.Unmarshal(p.DefaultParams, ¶ms) != nil { @@ -231,9 +233,9 @@ func readableTitle(sectionType SectionType, title string, config json.RawMessage break } } - if matched { - return p.DisplayName + if matched && len(params) > bestScore { + best, bestScore = p.DisplayName, len(params) } } - return presets[0].DisplayName + return best } diff --git a/internal/sections/resolver_test.go b/internal/sections/resolver_test.go index 685a120ee5..d66cb0d718 100644 --- a/internal/sections/resolver_test.go +++ b/internal/sections/resolver_test.go @@ -262,6 +262,7 @@ func TestResolve_ReplacesRawSectionTypeTitle(t *testing.T) { admin := []*PageSection{ {ID: "1", Position: 0, SectionType: "trending_on_server", Title: "trending_on_server", Config: json.RawMessage(`{"window":"7d"}`)}, {ID: "2", Position: 1, SectionType: "trending_on_server", Title: "Hot Right Now", Config: json.RawMessage(`{"window":"7d"}`)}, + {ID: "3", Position: 2, SectionType: "format_showcase", Title: "format_showcase", Config: json.RawMessage(`{"format":"4k","sort":"recent"}`)}, } result := Resolve(admin, nil) @@ -271,4 +272,7 @@ func TestResolve_ReplacesRawSectionTypeTitle(t *testing.T) { if result[1].Title != "Hot Right Now" { t.Errorf("custom title = %q, want unchanged", result[1].Title) } + if result[2].Title != "New in 4K" { + t.Errorf("most specific preset = %q, want %q", result[2].Title, "New in 4K") + } }