refactor: Fluxor 6/7 — the project list #35
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/fluxor-project-list"
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?
Sixth slice of the Fluxor page refactor (ADR 0011; series plan in #27). The project list moves onto its own slice; the last remaining page is the project workspace (7/7, coming next as its own PR — it's 484 lines over 13 use cases and deserves its own review).
What's in
Store (
ProjectListState) —Loaded,Projects,Error. View-local stays view-local:deleting(which project the confirmation dialog is aimed at) — an open modal is view state.Effects (
ProjectListEffects) — sole touchpoint forListProjects/DeleteProject. A successful delete chainsLoadProjectsso the list reflects the loss; a failed one dispatches onlyProjectDeleteFailed— the chained reload's load-request would wipe the very error the user needs to see (the #34 lesson, same shape).Stale-error handling — the list has no route id to guard the error render with, so it uses the wizard slice's
/projects/newpattern: theLoadProjectsdispatched on entry clears the slice's error via a[ReducerMethod(typeof(LoadProjects))]reducer, which is what makes the unguardedState.Value.Errorrender safe. Documented at the render site.Honest names —
ConfirmDeleteAsyncwasasync Task; it now only dispatches, so it'svoid ConfirmDelete(the #32 rule applied proactively).Tests
+2, adapter suite 119, full suite 395/395 green. All 8 pre-existing list tests pass unchanged. New pins:
A_failed_delete_surfaces_its_error_and_keeps_the_list— project vanishes server-side while the modal is open; asserts the Err arm renders as an alert AND the list stays as rendered rather than half-refreshing (this is the arm a chained reload would have swallowed).Reentering_the_list_clears_a_previous_visits_error— pins the clear-on-entry that justifies the unguarded error render.Browser-verified
Driven live against a seeded world: list renders the sample project card (title, byline, relative time) from the store → card click opens the ready project's workspace → back → Delete opens the modal naming the project → Cancel keeps it → Delete forever hard-deletes, the chained reload lands on the empty state → the empty-state CTA navigates into the wizard. Console clean (one favicon 404 only).
🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 91.4%
Orihon.Domain - 100%
Orihon.Infrastructure - 93.7%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 93.4%
Orihon.UseCases - 96.8%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The list slice, the sixth petal of the Fluxor refactor~ ♪ I've watched the whole garden bloom — Bible, PageWorkspace, Wizard, and now the list sits on the same action/effect/reducer spine as its siblings. This is wonderful craftsmanship. The shape is so consistent I could hum it from memory. ♡
Verdict: ✅ Looks good to me~
The project list now lives where it belongs — in the store, not in component fields — and the page is a thin view that dispatches and reads. Every architectural choice traces cleanly to a sibling precedent.
✅ What I liked~
LoadProjectsso the list reflects the loss; a failed one dispatches ONLYProjectDeleteFailedand leaves the list untouched — because the chained reload'sOnLoadRequestedreducer would wipe the very error the user needs to see. That's the #34 lesson applied proactively, and the comment inProjectListEffects.cs:7-9names it explicitly. Fufu~ you remembered the lesson and documented it. ♡ProjectId/PageId), so it borrows the wizard's/projects/newpattern: theLoadProjectsdispatched on entry clearsErrorvia[ReducerMethod(typeof(LoadProjects))]. The comment at the render site (ProjectListPage.razor:84-85) documents why the unguardedState.Value.Error is { } erroris safe. Load-bearing comment, load-bearing reducer.ConfirmDeleteAsyncwasasync Task; now it only dispatches, so it'svoid ConfirmDelete(the #32 rule). Thedeleting is { } doomedpattern-match is a nice touch too — no barenullderef possible.[FeatureState] sealed record ProjectListState, actions as sealed records, pure staticProjectListReducerswith explicit per-action[ReducerMethod](no base-type matching),ProjectListEffectsas the sole touchpoint forListProjects/DeleteProject. One public class per file. It matchesBibleState/PageWorkspaceState/ProjectWizardStateso precisely you could diff them for taste.OnLoadAsync's Match-to-empty-list (result.Match<IReadOnlyList<ProjectDto>>(list => list, _ => [])) is correct — a failed load doesn't detonate the page, it renders the empty state. Pragmatic and consistent with the pre-PR behavior.🔬 How I checked the tests (because green CI ≠ correct, fufu~)
Both new tests are genuine behavioral pins, NOT tautologies — I sabotaged the production code two ways and watched each test die precisely:
A_failed_delete_surfaces_its_error_and_keeps_the_list— I replaced the error-preserving branch with a silent chained-reload-always. The test failed at theWaitForAssertionfor "no longer exists", because the chainedLoadProjectswiped the error before it could render. Restored, passed. Genuine pin on the asymmetry.Reentering_the_list_clears_a_previous_visits_error— I madeOnLoadRequesteda no-op (stopped clearingError). The test failed atAssert.DoesNotContain("no longer exists", second.Markup)— the stale error bled through to the second visit exactly as the comment warns. Restored, passed. Genuine pin on the clear-on-entry invariant.The failure path is real:
Projects.Items.Clear()before "Delete forever" makesDeleteProject.ExecuteAsyncreturnErr<Unit>("The project no longer exists.")(verified inDeleteProject.cs:17+FakeProjectStore.DeleteAsync→RemoveAll > 0 = false), so the assertion string matches the real error message.📐 Sibling consistency verified
ProjectListState↔BibleState/PageWorkspaceState/ProjectWizardState: same[FeatureState] sealed recordshape,Loadedflag,Errorfield, explicit per-action reducers. ✓ProjectListEffects↔PageWorkspaceEffects:is not Ok<T>+(Err<T>)resultcast pattern identical.Result<T>is a closed two-variant type (abstractResult<T>with onlyOk<T>+Err<T>), so the cast is sound. ✓OnInitializeddispatchesLoadProjects— matches how siblings dispatchLoadBible/LoadPageWorkspace/LoadWizardon entry. ✓💡 Little ideas (non-blocking)~
ProjectListEffects.cs:13— theMatchdiscards the error on a failed load._ => []silently swallows aListProjectsfailure into an empty list, which the page then renders as the "No projects yet" empty state. This is pre-existing behavior (the oldReloadAsyncdid the sameresult.Match(list => list, _ => [])), so it's not a regression — but the store now has anErrorfield that didn't exist before. If you ever want a failed load to show an error instead of masquerading as an empty world, the plumbing is right there. Not blocking; the current behavior is defensible (don't block the user's front door on a transient backend hiccup).ProjectListEffects.cs:32—(Err<Unit>)resultcast. Theis not Ok<Unit>+ explicit cast pattern is identical to siblings and correct for the closedResult<T>type — but theReport<T>helper inBibleEffects/PageWorkspaceEffects(which doesresult.Match<object>(ok, err)) avoids the cast entirely. The list has only one fallible call so a helper would be over-engineering; just flagging the style difference for awareness. ♡Automated review by Jibril · 2026-07-25
CI/CD: absent for head SHA
2f120e0(PR just opened, 0 comments at review) · Local checks: build 0 warnings/0 errors (submodules 86d8b22/9544ff2), full suite 395/395 pass (119 BlazorAdapter + 81 Integration + 75 Domain + 120 UseCases — matches PR body exactly), 10/10 ProjectListPageTests, 2 sabotage reproductions run + reverted clean (git status empty post-restore)