diff --git a/internal/sections/resolver.go b/internal/sections/resolver.go index 9d7ed5cf46..2273677949 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) } @@ -102,14 +107,13 @@ func ResolveForSettings(admin []*PageSection, overrides []ProfileSectionOverride continue } rs := ResolvedSection{ - ID: s.ID, - SectionType: s.SectionType, - Title: s.Title, - DefaultTitle: s.Title, - Featured: s.Featured, - ItemLimit: s.ItemLimit, - Config: s.Config, - Position: s.Position, + ID: s.ID, + SectionType: s.SectionType, + Title: s.Title, + Featured: s.Featured, + ItemLimit: s.ItemLimit, + Config: s.Config, + Position: s.Position, } if hasOverride { @@ -132,6 +136,11 @@ func ResolveForSettings(admin []*PageSection, overrides []ProfileSectionOverride } } + // Both titles get the same readable fallback from the same config, so + // an admin title saved as the raw key still equals Title when the + // profile has no title override. + rs.DefaultTitle = readableTitle(rs.SectionType, s.Title, rs.Config) + rs.Title = readableTitle(rs.SectionType, rs.Title, rs.Config) result = append(result, rs) } @@ -187,7 +196,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, @@ -196,3 +205,42 @@ 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 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 + } + 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) + best, bestScore := presets[0].DisplayName, -1 + 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 && len(params) > bestScore { + best, bestScore = p.DisplayName, len(params) + } + } + return best +} diff --git a/internal/sections/resolver_test.go b/internal/sections/resolver_test.go index 1966ac71c9..ae9bc549aa 100644 --- a/internal/sections/resolver_test.go +++ b/internal/sections/resolver_test.go @@ -301,3 +301,48 @@ 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"}`)}, + {ID: "3", Position: 2, SectionType: "format_showcase", Title: "format_showcase", Config: json.RawMessage(`{"format":"4k","sort":"recent"}`)}, + } + + 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) + } + if result[2].Title != "New in 4K" { + t.Errorf("most specific preset = %q, want %q", result[2].Title, "New in 4K") + } +} + +// TestResolveForSettings_RawKeyAdminTitleIsNotARename: an admin row saved with +// the raw section_type key gets the preset name in both Title and DefaultTitle, +// so settings does not report a rename the profile never made. +func TestResolveForSettings_RawKeyAdminTitleIsNotARename(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: "trending_on_server", Config: json.RawMessage(`{"window":"7d"}`)}, + } + overrides := []ProfileSectionOverride{{SectionID: "2", Title: "My trending"}} + + result := ResolveForSettings(admin, overrides) + got := map[string][2]string{} + for _, r := range result { + got[r.ID] = [2]string{r.Title, r.DefaultTitle} + } + want := map[string][2]string{ + "1": {"Trending This Week", "Trending This Week"}, + "2": {"My trending", "Trending This Week"}, + } + for id, w := range want { + if got[id] != w { + t.Errorf("section %s (title, default title) = %q, want %q", id, got[id], w) + } + } +}