fix(ui): derive Library Type dropdowns from library_types — audiobooks was missing

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 <option> lists to the original three types
(ebooks/comics/manga), so an audiobook library could not be created from
the UI.

Both dropdowns now render from the same DB-backed library_types table
the create API resolves types against — a future type appears without
template edits (the exact drift the rendering-ownership rule warns
about):

- templates/utils.go: OrderedLibraryTypes pins the historical dropdown
  order (ebooks, comics, manga, audiobooks) and appends unknown future
  types by name; LibraryTypeDisplayName renders labels with a
  capitalize-the-name fallback for types added without a label
- templates/admin_library.templ: new LibraryTypeOptions fragment used
  by the modal's select; AdminLibrary takes the types list
- templates/setup.templ: Setup takes the types list, same fragment
- internal/router/helpers.go: libraryTypeOptionsFor fetches via
  LibraryService.GetLibraryTypes, falling back to the built-in type
  names on DB failure so the forms stay usable (CreateLibrary still
  resolves the name against the DB, so the fallback is never
  authoritative)
- internal/router/frontend.go, internal/router/setup.go: pass the list
  into the two pages

Tests:
- templates/utils_test.go: pure unit tests for ordering + labels
- cmd/server/tests/admin_library_page_test.go: SSR regression —
  /admin/library must offer all four library types, audiobooks exactly
  once (options come from the DB loop, not a hardcode)
This commit is contained in:
John O'Keefe committed 2026-10-18 16:38:03 -04:00
1 parent 327fc2c0f9
commit 9e831085ef
10 files changed
+593 -346

No files matched your search

+2 -1
View File
@@ -1143,7 +1143,8 @@ func registerFrontendRoutes(cfg *Config) {
var buf bytes.Buffer
archivedCount, _ := cfg.Queries.CountArchivedMediaItems(c.Request().Context())
err = templates.AdminLibrary(user, libData, userData, archivedCount).Render(c.Request().Context(), &buf)
libraryTypes := libraryTypeOptionsFor(c.Request().Context(), cfg)
err = templates.AdminLibrary(user, libData, userData, archivedCount, libraryTypes).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
+21
View File
@@ -405,3 +405,24 @@ func (cfg *Config) loadCollectionBooksLite(c *echo.Context, userUUID uuid.UUID,
return cfg.collectionBooksForScope(c, userUUID, pgtype.UUID{Bytes: collUUID, Valid: true}, collection, collectionID,
libUUID, libUUID, libraryID != "", visibleIDs)
}
// libraryTypeOptionsFor loads the library_types rows for the create-library
// dropdowns (admin page + setup wizard), ordered for display. On DB failure
// it falls back to the built-in type names so the form stays usable —
// CreateLibrary resolves the name against the DB, so this is never
// authoritative.
func libraryTypeOptionsFor(ctx context.Context, cfg *Config) []database.LibraryTypes {
types, err := cfg.LibraryService.GetLibraryTypes(ctx)
if err != nil {
log.Printf("libraryTypeOptionsFor: GetLibraryTypes failed, using built-in type names: %v", err)
for _, name := range []string{
services.LibraryTypeEbooks,
services.LibraryTypeComics,
services.LibraryTypeManga,
services.LibraryTypeAudiobooks,
} {
types = append(types, database.LibraryTypes{Name: name})
}
}
return templates.OrderedLibraryTypes(types)
}
+2 -1
View File
@@ -81,7 +81,8 @@ func registerSetupRoutes(cfg *Config) {
return c.Redirect(http.StatusFound, "/")
}
var buf bytes.Buffer
if err := templates.Setup().Render(c.Request().Context(), &buf); err != nil {
libraryTypes := libraryTypeOptionsFor(c.Request().Context(), cfg)
if err := templates.Setup(libraryTypes).Render(c.Request().Context(), &buf); err != nil {
log.Printf("Failed to render setup template: %v", err)
return c.HTML(http.StatusInternalServerError, "Failed to render setup page")
}