Repository navigation
feat: smart list browsing, wizard editing and cover management - #123
Conversation
27f6d89 to
3d81306
Compare
Surface the kmrs smart-list API: user-owned search filters created and edited through a two-step wizard (details, then filters inline), evaluated live by the server. - smartListsApi + SmartList DTOs; SSE wiring for the smart-list events - new 'entities' filter group kind (libraries / read lists / collections) with multi-select and is/is-not membership; referential options in the wizard follow the selected scope - /smart-lists browse page and /smart-lists/:id detail pages for both targets, reusing the browse grid/filter bar; page-side filters post as an overlay body; owner-scoped edit/delete (new isOwner permission idiom); read/peek entry points on book lists - two-step create/edit wizard reusing the filter building blocks inline (no drawer overlay), with a live match-count preview, stored filter round-tripping and a warning when a document contains conditions the editor cannot represent - PosterManager gains the 'smartlist' kind (upload/select/delete covers, generated mosaic listed alongside uploads) - smart-list cards use the generated/selected cover with cache busting - i18n: smartlists namespace in all 10 locales plus nav/filter/poster keys
everpcpc
left a comment
There was a problem hiding this comment.
Reviewed the full diff against the repo conventions; pnpm build (typecheck + bundle) and pnpm lint both pass on this branch, and I verified the smartlists key set is mirrored across all 10 locales (CLDR plural shapes accounted for). The API layer, SSE wiring, PosterManager smartlist kind, card structure, and the Dialog fill prop all match the existing idioms nicely. Two findings need attention, one of them a real round-trip bug.
1. (bug, verified) searchToFilterState loses match-all mode on round-trip. In the group-branch handling, the bucket is keyed by the raw condition key while absorbLeaf pushes values into the mapped group key:
const b = bucket(key as GroupKey) // 'tag'
...
if (!absorbLeaf(key, op)) lossy = true // values land in bucket('tags')The mode (b.mode = mode) is recorded on the wrong bucket, and the flush then pushes the invalid key 'tag' into state.matchAll — mode('tags') in the builder never sees it. I reproduced it with esbuild against this branch: tags=[manga, josei] + matchAll=['tags'] → buildBookSearch emits allOf, but searchToFilterState returns matchAll: [] with lossy: false, and re-emitting produces anyOf. So opening a list whose filter uses match-all in the wizard and saving it silently flips those groups to match-any, with no warning. Same for every mapped key (genre/publisher/sharingLabel/language/ageRating/mediaProfile/mediaStatus/oneShot/collectionId/readListId/author). Fix: resolve the condition key → GroupKey mapping once and use it for both the bucket and absorbLeaf.
2. (design mismatch) The wizard gates visibility to admins, but the API gives it to every owner. kmrs#150 lets any authenticated user share their own lists: share-targets is open to all, and create/PATCH accept visibility/sharedWithUserIds from any owner. Here the visibility picker renders only for admin and the body strips it otherwise (...(admin ? {...} : {}) with the "visibility is an admin-only concern" comment), so a non-admin owner can never create a PUBLIC/SHARED list through the UI — the sharing feature is unreachable for exactly the users it was built for. If admin-gated sharing is a deliberate product call, it belongs in the PR description (and arguably the API should reject non-admin visibility too); otherwise the gate should be ownership.
3. Admins get a broken "Manage posters" entry on other users' lists. canManage = isAdmin || isOwner gates the whole menu, but kmrs thumbnail writes are require_owner — an admin opening the PosterManager on someone else's list gets 403 on upload/select/delete. The posters item needs its own isOwner gate; edit/delete are correctly owner-or-admin.
4. SERIES detail passes SERIES_FILTER_GROUPS unfiltered. Non-admins see the admin-only deleted group; both existing series-side callers (SeriesGrid, collection detail) filter !g.adminOnly || admin.
Smaller items:
useFilterStateState: the name stutters, and its doc comment is glued to the export (*/export function); same glitch inFilterGroups.tsxwhereLetterOptions's signature line absorbedconst active.smartlists:editFiltersis added to all locales but never referenced.- The
continue-targetquery key isn't invalidated byReadProgressChanged, so the Read button's target can go stale after finishing a book until the page remounts. - The step-2 preview fires a
/listrequest per keystroke with no debounce (cheapsize=1, but a 200–300 ms debounce would match the search-input pattern used elsewhere).
Non-issues checked: PWA worker bypass unaffected (everything under /api); usersApi.list correctly gated enabled: admin; the card's fallback for a missing visibility field on older kmrs builds is a nice touch.
- searchToFilterState: track group mode through the same condition→group mapping as the values, so match-all groups survive editing instead of silently degrading to match-any; recognize single-group documents (whole-condition same-key anyOf/allOf and year-pair runs); fix the year-pair bounds check being off by one, which never matched and flagged every release year lossy; flag mixed is/isNot leaves as lossy instead of flipping the whole group's semantics - detail: hide Manage posters from admins on others' lists (cover writes are owner-only on the API) and filter adminOnly groups for non-admins - dialog: fetch share-targets only for admins (the endpoint is admin-only) - drop the unused smartlists:editFilters locale key
|
Taking over the PR to land the review fixes directly — pushed 1. Filter round-trip is now actually lossless. Beyond the mode/values bucket mismatch, verifying the fix empirically surfaced two more latent bugs in the same function:
Verified with an esbuild harness against this branch (the repo has no test targets): 13 round-trip cases — match-all/match-any tags, genres, authors, collections, negated groups, libraries, single/multiple/match-all release years, and a full combo — all preserve the emitted condition exactly, and 5 lossy-positive cases (mixed 2. Admins no longer see "Manage posters" on other users' lists — the menu item is gated on ownership, matching the API's owner-only cover writes; edit/delete stay owner-or-admin. 3. SERIES detail filters 4. Also cleaned up: |
…ount - invalidate smart-lists on ReadProgress(Series)Changed/Deleted: the items grid and the continue target both depend on read progress - debounce the wizard's live match-count preview (300ms, same pattern as the browse search input) instead of firing a /list request per keystroke
|
Follow-up |
a top-level same-key leaf run is outside the builder's vocabulary (it wraps multi-value groups), and parsing it as match-any would silently rewrite AND into OR — warn instead of degrading
|
Holistic review of the PR as it now stands (original commit plus the three fix commits I pushed). CI green on every commit, locale parity verified after the VerdictThe feature is complete and coherent with the merged backend (kmworks/kmrs#150 + #152). All round-1 findings are addressed, and the round-trip parser is now verified lossless across the entire builder vocabulary. What holds up
The round-trip parser, after the fixesVerified with an esbuild harness (the repo has no test targets): 13 round-trip cases preserve the emitted condition byte for byte (match-all/any tags, genres, authors, collections, negated groups, libraries, single/multi/match-all release years, full combo), and 7 lossy-positive cases all warn instead of silently rewriting (mixed The contract the parser now fulfills: anything the wizard can produce round-trips exactly; anything else shows the lossy warning. Nothing from my side remains open. Remaining accepted behavior (documented in the PR): documents with constructs outside the builder's vocabulary show the lossy warning on edit and are simplified on save. |
Need kmworks/kmrs#150
Surface the kmrs smart-list API: user-owned search filters created and edited through a two-step wizard (details, then filters inline), evaluated live by the server.