Metadata management: editors for 5 entity types, harmonized model, and library filtering #42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/entity-management-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
This PR turns the metadata layer into a first-class, editable feature. It adds
desktop management editors for five entity types — Tags, People, Circles,
and two new ones, Characters and Series — harmonizes all of them onto a
single shape (name + description + aliases), and makes the library search
bar filter and autocomplete by every type, including by alias.
It spans the .NET backend (entities, endpoints, search, one EF migration) and
the Flutter client (models, state, generic UI, filtering), and fixes a
pre-existing bug where the library autocomplete never had any data.
104 files changed, ~9.1k insertions. Full backend and client test suites pass;flutter analyzeis clean.What's new (user-facing)
each a desktop master-detail editor (fixed-width searchable list on the left,
a large inline editor on the right).
delete. Save fires a confirmation toast; delete and alias-removal ask for
confirmation by name; a newly-created entity stays open in the editor.
the entity is deletable; deleting an in-use entity returns 409 and shows a
"used by N doujin(s)" dialog.
name + description + aliases. Tags lost the confusingnamespace; People/Circles lost the unusedsortNameand theirnotesbecamedescription.tag/person/circle filters.
canonical name, e.g.
Tag: tg → gender bender, and commits the canonicalname. This makes aliases usable as bilingual (EN/JP) shortcuts.
a tooltip; characters and series are listed (read-only).
Backend (.NET)
Field harmonization
Tag: dropnamespace, adddescription.Person/Circle: dropsortName,rename
notes→description.every
TagDto/CircleDtoconstruction site (metadata, doujin-detail, search).New Character & Series entities (full Circle parity)
DoujinCharacter,DoujinSeries) and configs, services + interfaces, use-cases, DTOs, hypermediabuilders, and REST endpoints (
/api/characters,/api/series) with CRUD +alias + pagination + search + per-item links/actions.
doujinCountand referential-integrity delete (409 when in use), matchingCircles.
Linking + search
characterIds/seriesIds; the doujin detail responseincludes assigned characters/series.
characterNames/seriesNames(mirrorscircleNames).Migration
HarmonizeMetadataAddCharactersSeries) that renamesnotes→descriptionon people/circles (data preserved), dropsnamespace/sort_name, addsdescriptionto tags, and createscharacters,series,doujin_characters,doujin_series. Applied automatically atstartup via the existing
Database.MigrateAsync().Client (Flutter)
Data + state
Tag/Person/Circleharmonized; newCharacter/Seriesfreezed modelsimplementing a shared
EntityModel. Repository gains paginated + search reads,full write methods, and flat reads for the metadata cache.
EntityListState<T>slice, one genericaction set + reducer, and generic epics parameterized by an
EntityOps<T>strategy — the five types are type instantiations, not copy-paste.
UI
EntityManagementPage<T>master-detail widget + oneEntityEditor<T>(fields described declaratively per type; shared alias-chip section; Ctrl+S to
save) + thin per-type page wrappers + NavigationRail entries and routes.
Filtering
FilterTypegainscharacter/series;parseFilterToken,buildSuggestions,SearchQuery,LibraryState, the library reducer bucketsand the search-query builder all handle the two new types.
bar feeds them into suggestions.
any alias (five per-type blocks refactored into two generic
EntityModelhelpers). Alias matches render
Type: alias → Canonical Name.Bug fixes included
_loadMetadataEpicbailed onmetadata.isLoading, but the reducer sets that flagtruefor the sameLoadMetadataActionbefore the async epic runs — so it alwaysshort-circuited (the epic's unit test masked this with an identity reducer).
Removed the self-defeating guard; the cache-non-empty check still prevents
redundant reloads.
created afterwards never appeared in suggestions. Added a
forceflag toLoadMetadataActionand an epic that force-refreshes the cache after anyentity create/update/delete.
Key decisions
HypermediaLink/HypermediaAction/envelopes) instead of adding new client models.
eligibility is server-authoritative (list items previously had none).
aren't SQL-translatable); the no-search path keeps SQL-side pagination.
is a model + ops + slice + page + route.
Explicitly out of scope (future phase)
tags/people/circles/characters/series on the detail page — and the
create/upload doujin flow. No type has this today; associations are populated
by the backend create/ingest path. Filtering and read-only display work
regardless.
Testing
dotnet build+dotnet testgreen (326 tests). New coverage:Character/Series CRUD, alias, search, 409-in-use, detail include, and
search-by-name; per-item delete-action visibility.
RequestLoggingMiddlewareTestscase only fails undera local
appsettings.Development.jsonlogging override (Microsoft.AspNetCore→
Information); with the defaultWarningit passes. Unrelated to this PR.flutter analyzeclean;flutter testgreen (220 tests). Newcoverage: master-detail interactions, save/delete/alias confirmations,
create-stays-open, Character/Series page smokes, character/series + alias
filter-token tests, and the metadata force-refresh path.
Verify locally
Then run the app: all five rail entries open working editors; create a tag with
an alias (e.g. name "gender bender", alias "tg"); in the library search bar, type
tgand confirm it suggestsTag: tg → gender bender.Reviewer notes
EntityManagementPage<T>/ generic epics /entity_reducerare theDRY core — reviewing those once covers all five types.
RenameColumnfor notes→description (it does), not drop+add.Flutter Coverage
Total: 69.6% (1929 of 2773)
Summary
Summary
Coverage
DoujinManager.ApplicationCore - 85%
DoujinManager.Infrastructure - 93.6%
on
pshot
Series
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
DoujinManager.RestAdapter - 84%
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
DoujinManager.Server - 26%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! This is a wonderfully ambitious metadata spellbook — generic entity pages, alias-aware tokens, Character/Series parity... I do love seeing knowledge organized so neatly~ ♪
But fufu~ one little paging gremlin is hiding in the autocomplete cache. You wouldn't want the new metadata magic to forget everything after page 1, would you? ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing~
listTags(),listPeople(),listCircles(), and the newlistCharacters()/listSeries()) call paginated endpoints withoutpageSizeand then ignore pagination.PaginationParamsdefaults toPageSize = 20, so_loadMetadataEpiconly caches the first 20 entities of each type. That means alias parsing/autocomplete silently misses page 2+ — exactly where a large metadata library will keep most of its precious knowledge. Please page throughCollectionResponse.page.totalPages(preferablypageSize=100until exhausted) or add a true unpaged metadata-cache endpoint.💡 Little ideas (non-blocking)~
Saved "..."immediately after dispatching create/update, before the async result succeeds. Cute confidence, but a bit too eager~ Consider emitting the success toast fromEntityCreatedAction/EntityUpdatedActioninstead, so failures don't briefly claim success before the error snackbar arrives.negative: true, but_applyTokensonly treatsFilterType.excludeTagas an exclusion. This was partly pre-existing, but the new entity types inherit the same confusing red-chip-but-positive-filter behavior. Either restrict-suggestions to tags, or teach the query model real negative filters later.Automated review by Jibril · 2026-07-01
CI/CD: passed/current for head
3c75293(Flutter coverage #674 and backend coverage #675 posted after the head commit) · Local checks: full local checks skipped per CI policy; optional focused backend metadata/search test command exited 0, optional focused Flutter run was blocked by runner ENOSPC⛔ Blocking — fixed
Metadata cache truncated to page 1. The five flat loaders (listTags/listPeople/listCircles/listCharacters/listSeries) called the paginated endpoints without pageSize and ignored pagination, so only the first 20 of each type reached the autocomplete cache — page 2+ aliases/names were invisible. They now walk every page at pageSize=100 until page.totalPages is exhausted, via a shared _listAll helper. Added a repository test that serves two pages through a fake adapter and asserts both pages are fetched and concatenated with pageSize=100.
💡 Non-blocking #2 — fixed
Confusing red-but-positive - chips. Since only tags support exclusion in the query model, a leading - now yields tag-only suggestions (languages/people/circles/characters/series are suppressed for -, since they'd render as negative chips that actually filter positively). Added a test locking this.
💡 Non-blocking #1 — left as-is (deliberately), flagging for you
The eager Saved "…" toast firing on dispatch rather than on success is a valid critique — but bjoern explicitly asked earlier for "a toast independent of whether it works or not." So I kept it to honor that request; a failure still queues the error snackbar after. If you'd now prefer the reviewer's approach (emit the success toast from EntityCreatedAction/EntityUpdatedAction, so it only claims success on actual success), say the word and I'll switch it — it's a small change.
flutter analyze clean; 222 client tests pass.