V2.4 UI improvements, gears and collectibles, books api improvements#467
Open
devpro wants to merge 28 commits into
Open
V2.4 UI improvements, gears and collectibles, books api improvements#467devpro wants to merge 28 commits into
devpro wants to merge 28 commits into
Conversation
[PersistentState] is fully reverted in TvShowDetail.razor — Show/Reference/Episodes are back to private fields _show/_reference/_episodes, OnParametersSetAsync always calls LoadAsync (no restore-skip path), matching how Watch Next handles it. The _loading/_loaded flash fix stays untouched. Build passes. I also updated docs/prerender-flash-fix.md to record why (unbounded embedded Episodes list hit the same 32 KB SignalR ceiling as Watch Next's aggregate list).
… and Album, reproducing Book's exact pattern end-to-end Backend: added CustomImageUrl to VideoGameModel/AlbumModel (Domain), VideoGame/Album entities (custom_image_url BSON field), VideoGameDto/AlbumDto (with matching doc comments), and updated VideoGameController/AlbumController.OnListMappedAsync to apply the override after reference-cover hydration, same as BookController. No mapper changes needed — it's a plain matching-name scalar, auto-mapped by Mapperly. UI: both detail pages now compute coverUrl with the same precedence (CustomImageUrl if set, else the linked reference's cover), and have a "Cover image URL" text input with the same conditional placeholder and @onchange save pattern as BookDetail.razor. Tests: added *ResourceList_CustomImageUrlOverridesTheLinkedReferencesCover integration tests for both types (mirroring Book's), plus CustomImageUrl fuzzing in the existing full-cycle round-trip tests. All 12 Book/VideoGame/Album integration tests pass against real MongoDB. Visual verification: extended MobileScreenshotTest (the project's established visual-review harness) to seed a custom cover URL on the seeded Album/VideoGame and capture their detail pages by title. Ran it with E2E_SCREENSHOTS=true — screenshots confirm both pages render the custom cover (overriding the real RAWG/Discogs art) and the populated input field, matching Book's UX exactly.
What it does: a new "generic video game transaction import" (/import/video-games, POST /api/import/video-games/preview|commit) that parses a CSV (Transaction Date, Game Name, Product Name, Platform, Vendor, Transaction Id, Order Id, Final Price (€)) into review rows and commits selected ones as VideoGame items — same review-then-commit UX as the existing Amazon import, merging same-titled rows into one game with multiple Platforms entries. Key changes: - Shared engine extracted: the title-merge/dedup logic (ComputeCommitPlan/FindImportedReferences) was pulled out of AmazonImportMergeService into a new OwnedItemImportMergeService, since it was already fully generic. Amazon's own code/tests were only touched for the resulting rename, no behavior change. - New VideoGamePlatformModel.ProductName field (store's specific edition/product text), now editable on VideoGameDetail.razor via a new ExtraFields slot added to the shared OwnedVersionFields component. - Bug found and fixed: a PSN transaction can bundle several different products under one shared Transaction Id/Order Id (confirmed against your real export — three "Far Cry 4" DLC packs, one transaction). The dedup Reference was colliding across those lines and silently dropping platforms. Fixed by disambiguating with Product Name, mirroring how Amazon's import already uses ASIN for the same purpose. - Reconciliation reporting: RowsImported/SkippedRowTitles were added so the import result screen shows "X of Y selected rows imported" plus a named list of anything actually skipped, since the created/merged-item counts alone can look short even when nothing was lost (rows sharing a title consolidate into one item).
Reuses the plain-scalar shape already established for these three types (no reference-linking, so no override/precedence logic needed): shown as a thumbnail on the list page and editable on the detail page header.
Mirrors the list pages' existing URL-state pattern (navigate, don't mutate) so browser back/forward and page reloads restore the tab that was showing, instead of always resetting to the first one.
…bum) Mirrors VideoGamePlatformModel.ProductName, generalized onto the one shared owned-copy shape so it's per-copy for free: a season 6 Blu-ray, a season 1 digital copy, and an old season 1 DVD can each carry their own store listing text. Rendered through OwnedVersionFields' existing ExtraFields slot in OwnedVersionsEditor, shared by all four types.
Scaffolded by direct analogy to Album (CLAUDE.md's "Adding a new trackable item type" recipe), minus reference-linking since neither type has an external data provider. Both are member-only, minimal (title, brand, year, notes, optional cover image, favorite, owned versions) and share the OwnedVersionsEditor/OwnedVersionFields components used by every other owned-copy type.
The new API clients were never registered in BlazorApp's AddWebApiHttpClient, so opening Collectibles or Gear threw InvalidOperationException at render time - a registration this codebase keeps in a separate file from where the client classes live, which the earlier implementation missed entirely. Also replaces the cramped ItemThumb-next-to-title treatment on detail pages (Car/House/HealthProfile/Collectible/Gear) with the same full-width banner-image pattern VideoGameDetail/AlbumDetail already use. On Car specifically, the banner and URL field now sit below the energy type buttons rather than above the other fields.
…here List pages (Cars/Houses/Collectibles/Gear) now pass ItemImageShape="wide" like VideoGames.razor, instead of the default portrait shape that badly cropped landscape photos of cars/houses/objects. HealthProfile keeps the portrait default since its subject is a person. Detail pages (Car/House/HealthProfile) now render the cover image as its own standalone kt-form-card p-0 banner, matching what Collectible/Gear already do (and what VideoGameDetail/AlbumDetail established) - not crammed inside the multi-field card after the last text field.
- Moved SetFieldAsync (fill + blur, for @onchange-bound fields) off BookDetailPage onto the shared DetailPageBase — it was never Book-specific, and I'd compounded the existing wart by copying BookDetailPage.SetFieldAsync into five new unrelated tests. Also fixed the three pre-existing misuses (BookSmokeTest, OwnershipSmokeTest, QuickAddSmokeTest) while I was in there. - Found ReferenceableDetailPageBase already had a CoverImage locator (role=img, alt ending in "cover"/"poster") for the reference-linked types. Promoted it to DetailPageBase instead of reinventing it, added ImageUrlInput alongside it, and fixed Car/House/HealthProfile's alt text (was just @name, now @name cover) so they actually match that convention. - Rewrote all five smoke tests (Car, House, Health, Collectible, Gear) to use detail.ImageUrlInput / detail.CoverImage / DetailPageBase.SetFieldAsync instead of raw inline selectors.
Reproduced the failure directly (ran the smoke test 5+ times against the real Google Books API) and found two separate, real issues: 1. Google Books genuinely is having a rough day — retries already existed (3 retries via AddStandardResilienceHandler), but weren't always enough to ride out sustained 500/503s. Fixed: added AddBookProviderResilienceHandler (ProviderResilienceExtensions.cs) with 5 retries / shorter backoff / wider total timeout, applied to Google Books + Open Library + BnF in Program.cs. A run after this change took 32s (visibly working through retries) and passed. 2. Google Books responses were needlessly heavy — confirmed with curl: an unfiltered search response was ~75KB vs TMDB's ~14KB for a comparable query, explaining the "always slower than other providers" feel independent of today's outage. Fixed: added Google's fields= partial-response parameter to both the search and details calls in GoogleBooksClient.cs, restricted to exactly the fields the app actually reads. 3. Bonus UI bug found while reproducing: InlineReferenceLinker.razor showed "No results" and the raw HttpRequestException message simultaneously on a search failure, and the raw exception text wasn't user-friendly. Fixed both.
Added Gear and Collectible as importable types in the Amazon order-history CSV import feature, mirroring the existing Movie/TvShow handling exactly (both use the same OwnedVersions shape): - AmazonImportMediaType — added Gear, Collectible enum members. - AmazonImportCommitResultDto — added Gear*/Collectibles* created/merged/skipped counts. - AmazonImportController — injected IGearRepository/ICollectibleRepository; extended Preview (already-imported check) and Commit (create/merge block) with two more cases. - AmazonImportPage.razor — added the two options to the type dropdown and two lines to the result summary. - Tests — extended AmazonFixtureCsvBuilder with Gear/Collectible fixture rows, and AmazonImportResourceTest with full round-trip assertions (preview, commit, re-import dedup, cleanup). - Updated a couple of doc comments (AmazonImportController, AmazonOwnedItemImportRequestItem) that used to enumerate only Book/Movie/TvShow/VideoGame.
Backend (5 domains: TvShow, Movie, Book, VideoGame, Album):
- Added DeleteAsync(string id) to each I<X>ReferenceRepository + Mongo implementation — the shared reference collections had no delete path before.
- Added Unlink<X>ReferenceAsync(model) to ReferenceEnrichmentService's partials — clears the tenant's ReferenceId and unconditionally deletes the shared reference document.
- Added POST /api/<x>/{id}/unlink-reference to each controller, gated by a method-level [Authorize(Policy = "AdminOnly")] on top of the controller's own policy.
Frontend:
- Added UnlinkReferenceAsync to each <X>ApiClient.
- Added an admin-only trash-icon button next to the ↻ refresh icon on all five detail pages, behind a ConfirmModal (since it's destructive/irreversible). On confirm, it unlinks, reloads the item, and shows an inline toast — InlineReferenceLinker reappears automatically since it's already conditioned on an empty ReferenceId, so the admin can immediately search and pick the correct match.
Tests: new UnlinkReferenceResourceTest.cs (5 integration tests, verifying the link is cleared and the reference document is actually deleted) and UnlinkReferenceAuthorizationTest.cs (reflection guard confirming the AdminOnly policy can't be silently dropped, since the shared Firebase test account is itself an admin and can't exercise a 403 over HTTP).
- New user-preferences pattern: UserPreferencesModel/Repository/StorageMapper (Domain + Infrastructure.MongoDb), UserPreferencesDto/DtoMapper/UserPreferencesController (WebApi, GET/PUT /api/user-preferences), a unique owner_id index, and a UserPreferencesApiClient on the Blazor side — designed so a future toggle is just one more named boolean, no new plumbing. - /account/manage: now has a "Preferences" section with a "Show chasse-aux-livres.fr link on book pages" toggle that persists immediately. - BookDetail.razor: the ISBN field is now an input-group; when an ISBN is set and the preference is on, a ↗ button opens https://www.chasse-aux-livres.fr/<isbn> in a new tab.
| try | ||
| { | ||
| #pragma warning disable S5693 | ||
| await using var stream = e.File.OpenReadStream(MaxFileSize); |
| try | ||
| { | ||
| #pragma warning disable S5693 | ||
| await using var stream = e.File.OpenReadStream(MaxFileSize); |
…es, date parsing, regex timeouts, cognitive complexity - Replace throw-on-first-match foreach loops with FirstOrDefault+throw (S1751) - Extract repeated string literals to constants (S1192) - Simplify Count-1 indexing and a synchronous filter loop to LINQ (S6608, S3267) - Deduplicate the identical refresh-reference nested ternary across five detail pages into ReferenceRefreshMessage.Compute, and the identical DateOnly HTML-input parsing across eight sites into DateOnlyInput.Parse - both fix S3358/S6580 while removing the underlying duplication - Add a match timeout to every Regex construction/replace missing one (S6444) - Extract per-row/per-item helper methods in HealthImportService and OwnedItemImportMergeService to bring cognitive complexity back under threshold (S3776), verified against existing unit tests plus a new DateOnlyInputTest S8970 (null-forgiving operator removal) and the S5332 http:// BnfClient XML namespace URIs were investigated and left alone - both are Sonar false positives confirmed by a real compiler error and by XML namespace semantics respectively. S107/S6960 (parameter counts, "split this controller") are architectural, matching this codebase's documented DI-heavy/one-controller- per-feature conventions, so left as-is. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E91RS1SmtWz2CBqk2YbEby
The commit button and its warning text now also account for selected video-game rows with no platform chosen, mirroring the existing "pick a type" guard, so the 400 case can no longer be triggered from the UI.
|
SaveAuthorAsync/SaveArtistAsync had copied the Title field's never-allow-empty guard, so clearing the input silently skipped the API call and the old value came back on reload. Only Title (and Name-equivalent identity fields on other types) should require a non-empty value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One reusable component (SuggestInput.razor in Components/Inventory/Shared/) instead of two half-solutions: - A real text input — filters suggestions as you type (Contains, capped to 8 matches, so a long category list never dumps in full), click one to fill it, or keep typing to enter something new. No "click and it's stuck" behavior — the input stays live. - Suggestions render as a small dark-themed dropdown menu (.kt-autocomplete-menu) built from the app's own CSS variables, not a native <datalist> popup, so it actually respects the dark theme and sits in normal flow instead of floating over other fields. List page: the earlier <select> was the actual bug behind "pushed to another line for nothing" — Bootstrap's .form-select forces width: 100%, and inside a flex row that percentage resolves against the row itself, which is what forced the wrap even with room to spare. SuggestInput sizes its wrapper to a fixed 150px flex item (.kt-category-select, same non-growing/non-shrinking shape as the existing .kt-sort-select), so it behaves predictably and sits inline with Favorites/Owned like it should. Detail page: replaced the wall of chips with the same component — type to filter, pick a match or type a new category, no dead popup overlapping Notes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




No description provided.