From 9e831085ef33d18f21ad42159a35f017fb363412 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 18 Oct 2026 16:38:03 -0400 Subject: [PATCH] =?UTF-8?q?fix(ui):=20derive=20Library=20Type=20dropdowns?= =?UTF-8?q?=20from=20library=5Ftypes=20=E2=80=94=20audiobooks=20was=20miss?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audiobook library type shipped backend-complete (LibraryTypeAudiobooks constant, allowed extensions, MIME map, schema seed row, GET /api/libraries/types) but neither create-library form offered it: the admin Create Library modal and the setup wizard's add-library step both hardcoded their - - + @LibraryTypeOptions(libraryTypes)
diff --git a/templates/setup_templ.go b/templates/setup_templ.go index 07511c3c..30d1f86b 100644 --- a/templates/setup_templ.go +++ b/templates/setup_templ.go @@ -8,7 +8,9 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func Setup() templ.Component { +import "bookhoard/internal/database" + +func Setup(libraryTypes []database.LibraryTypes) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -85,7 +87,15 @@ func Setup() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Configure Library Folders

Add the filesystem folders where your media files are stored.

No libraries created. You can add folders later from the admin panel.

Scan & Finish

Review your configuration and start the initial scan.

Server URL

Administrator

Libraries ()

Scan Progress

Initial scan complete! Your libraries are ready to use.

Scan & Finish

Review your configuration and start the initial scan.

Server URL

Administrator

Libraries ()

Scan Progress

Initial scan complete! Your libraries are ready to use.

Browse Folders

Browse Folders

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/utils.go b/templates/utils.go index 5dff1b0a..f32e7649 100644 --- a/templates/utils.go +++ b/templates/utils.go @@ -409,3 +409,61 @@ func badgeLabel(badge string) string { } return "" } + +// Library type dropdown helpers. The option list comes from the DB-backed +// library_types table (the same source the create API resolves types +// against), so a new type appears in the UI without template edits. + +// libraryTypeDisplayOrder pins the long-standing library types to their +// historical dropdown order. +var libraryTypeDisplayOrder = map[string]int{ + "ebooks": 0, + "comics": 1, + "manga": 2, + "audiobooks": 3, +} + +const libraryTypeRankUnknown = 1 << 30 + +func libraryTypeRank(name string) int { + if rank, ok := libraryTypeDisplayOrder[name]; ok { + return rank + } + return libraryTypeRankUnknown +} + +// OrderedLibraryTypes sorts library types for a create-library dropdown: +// the original types keep their UI order, any newer types follow by name. +func OrderedLibraryTypes(types []database.LibraryTypes) []database.LibraryTypes { + sorted := make([]database.LibraryTypes, len(types)) + copy(sorted, types) + sort.SliceStable(sorted, func(i, j int) bool { + ri, rj := libraryTypeRank(sorted[i].Name), libraryTypeRank(sorted[j].Name) + if ri != rj { + return ri < rj + } + return sorted[i].Name < sorted[j].Name + }) + return sorted +} + +// LibraryTypeDisplayName renders a library_types.name for the UI +// ("audiobooks" → "Audiobooks"); unknown names just get their first letter +// capitalized so a future type still reads sensibly. +func LibraryTypeDisplayName(name string) string { + switch name { + case "ebooks": + return "Ebooks" + case "comics": + return "Comics" + case "manga": + return "Manga" + case "audiobooks": + return "Audiobooks" + } + if name == "" { + return name + } + r := []rune(name) + return strings.ToUpper(string(r[0])) + string(r[1:]) +} diff --git a/templates/utils_test.go b/templates/utils_test.go index 474b3bbc..0bb2e74f 100644 --- a/templates/utils_test.go +++ b/templates/utils_test.go @@ -1,6 +1,10 @@ package templates -import "testing" +import ( + "testing" + + "bookhoard/internal/database" +) func TestGroupTunableSettings(t *testing.T) { entries := []SettingEntry{ @@ -54,3 +58,40 @@ func TestThemeCheckClass(t *testing.T) { t.Errorf("inactive theme should be hidden, got %q", got) } } + +// The library-type dropdown renders from DB rows; ordering and labels are +// presentation policy (OrderedLibraryTypes / LibraryTypeDisplayName). +func TestOrderedLibraryTypes(t *testing.T) { + // The DB query returns ORDER BY name; the dropdown must not reshuffle + // the historical UI order, and unknown (future) types append by name. + types := []database.LibraryTypes{ + {Name: "audiobooks"}, {Name: "comics"}, {Name: "ebooks"}, + {Name: "manga"}, {Name: "videos"}, + } + got := OrderedLibraryTypes(types) + want := []string{"ebooks", "comics", "manga", "audiobooks", "videos"} + if len(got) != len(want) { + t.Fatalf("got %d types, want %d", len(got), len(want)) + } + for i, name := range want { + if got[i].Name != name { + t.Errorf("position %d = %q, want %q", i, got[i].Name, name) + } + } +} + +func TestLibraryTypeDisplayName(t *testing.T) { + cases := map[string]string{ + "ebooks": "Ebooks", + "comics": "Comics", + "manga": "Manga", + "audiobooks": "Audiobooks", + "videos": "Videos", // unknown type: capitalized fallback + "": "", + } + for name, want := range cases { + if got := LibraryTypeDisplayName(name); got != want { + t.Errorf("LibraryTypeDisplayName(%q) = %q, want %q", name, got, want) + } + } +}