Project workspace: shell, side menu, breadcrumb, General page (edit + soft delete) #18
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/workspace-shell"
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?
The project-workspace story: opening a project now lands in a real workspace — breadcrumb bar on top, grouped side menu, content area — with a General page that edits the project's meta and soft-deletes with confirmation. Composes the components from #16.
What's in it
Domain + persistence
Project.Description(blank → null) andRename→UpdateDetails(name, description, ts);AddProjectDescriptionmigration.Use cases
GetProject(slug)·UpdateProject(title required + trimmed) ·DeleteProject(soft, journaled — the UI confirms, but per ADR 0020 reversibility is the safety net, not gating).ProjectDtocarriesDescription; the store port growsFindBySlug/UpdateDetails/SoftDeletein the established store-completes-the-operation pattern.The shell
WorkspaceShell: breadcrumb (Projects › name › section, clickable up the path),NavListwith the corrected IA — General; Records: Characters, Locations, Lore, Relationships, Timeline; separator; Visual novel: Chapters (no Scenes entry — a scene belongs to a chapter, per Björn's correction recorded in the story) — plus loading/not-found states. Five new menu icons in the catalog.General page
ConfirmDialog(danger variant, backdrop can't dismiss) → soft delete → back at the list.WorkspaceSectionPagestubs all six section routes (name/icon derived from the URL).The browser pass caught a real bug
.kg-nav__item:hover(class + pseudo-class) outranked.kg-nav__item--active(single class), so the current section lost its accent whenever the cursor was over it — invisible to bUnit (which sees classes, not the cascade). Measured via computed styles, fixed (--activenow also wins on:hover), re-measured. The full walk was done in real Chrome: create → General (shell/breadcrumb/menu/prefilled form) → edit + save (verified in SQLite: value persisted, journal shows Create → Update) → section nav (active state + breadcrumb trail) → delete confirm → soft-deleted row + SoftDelete journal entry → back at the list. Both themes screenshotted.Tests — +24 (245 total)
Update; delete soft-deletes, journals, andUndorestores the project — the confirm dialog's "recoverable" promise, proven; double-delete fails cleanly.AdapterTestContext+ oneFakeProjectStore+TestDoubles, replacing three nested copies.Verification
Build Debug + Release — 0 warnings / 0 errors · 245/245 tests · browser walk incl. DB checks, both themes.
Not in this PR
Preview image (needs the asset-storage foundation — its own slice) · a trash/restore UI (undo already restores; a proper trash page is future) · the real section pages (each is its own story, landing in these stubs).
🤖 Generated with Claude Code
Summary
Summary
Coverage
Kagura.BlazorAdapter - 58.6%
Kagura.Domain - 96.4%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 95.6%
n
Kagura.Kernel - 90%
Kagura.Server - 95.2%
Kagura.UI - 97.7%
Kagura.UseCases - 96.1%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! A whole workspace landing at once — shell, breadcrumb, side menu, General page, soft delete, cross-feature reducers, a CSS specificity bug caught by a real browser pass... this is a meal, scarlet~ ♪ I read every line of the diff and every full file it touches, plus the siblings (CreateProject, EfProjectStore, KaguraDbContext journaling, EfUndoStore, NavItem, Modal). Let's see if it holds together~
Verdict: ✅ Looks good to me~
This is a really well-constructed PR. The architecture is honored faithfully, the journaling claim is real (not hand-waved), the test coverage is thorough, and the little CSS cascade bug you caught and measured with computed styles is exactly the kind of thing that separates "it compiled" from "it works." fufu~ ♡
✅ What I liked~
Journaling is automatic and correct.
UpdateDetailsAsyncandSoftDeleteAsyncinEfProjectStoreboth callSaveChangesAsyncon a tracked entity, so theKaguraDbContextsave override journals them transactionally — Update for details, SoftDelete for delete (via theIsDeletedtransition inDetermineOperation). The integration tests (DeleteProject_soft_deletes_journals_and_undo_restores,UpdateProject_persists_meta_and_journals_an_update) prove it with real SQLite. No journaling code was hand-written in the use cases, and none needed to be — the architecture does the work. Wonderful~ProjectDtoschema migration is complete. TheDescriptionfield flows everywhere: domain → store port → EF store → DTO → DI registration → Fluxor state/reducers/effects → the prefilled form. The migration (AddProjectDescription), the model snapshot, andProjectConfigurationall agree. Nothing dangles.The sync-keyed form pattern (
_syncedProjectIdguard inSyncFormFromState) is the right answer to the Blazor re-render-vs-clobber problem. Prefilling on project-id change instead of every render means typing in the title field won't get stomped. Elegant.Cross-feature reducers are clean.
ProjectsReducers.OnWorkspaceSavedre-sorts the list byUpdatedAtafter a workspace edit, andOnWorkspaceDeletedremoves the row — both tested. The list stays fresh within the circuit without a refetch. Nice DRY composition over the existing store.The NotFound guard in
WorkspaceShell.OnParametersSet(_requestedSlug != Slugcheck) prevents the infinite re-dispatch loop. Correct and locally contained.Test infra consolidation — extracting
AdapterTestContext,FakeProjectStore, andTestDoublesto replace three nested copies is a genuine maintainability win. The fake correctly mirrors the soft-delete query filter (!p.IsDeleted), which matters for test fidelity.CSS specificity fix (
.kg-nav__item--active:hover) — catching that.class:hoveroutranks.class--activeand verifying with computed styles rather than trusting bUnit (which only sees classes, not the cascade) is exactly the right instinct. fufu~ you wouldn't believe how many people ship that bug~DeleteProjecterror path reusesProjectSaveFailed— a minor naming smell but a correct UX choice: the delete-error surfaces on the title field'sErrorbinding, which is visible. Not silently swallowed.💡 Little ideas (non-blocking)~
WorkspaceSectionPage.SegmentKey— parsingNavigation.ToBaseRelativePath(Navigation.Uri)withLastIndexOf('/')works but is fragile if query strings or fragments sneak into a section URL later. Since you already have@pageroute parameters, consider adding a[Parameter] string Section { get; set; }to the route templates (e.g.@page "/projects/{Slug}/{Section}") and deriving the name from that — Blazor parses it for you. Not urgent; the current approach is correct for today's six fixed routes.ProjectSaveFailedfor delete errors — functionally fine, but a future cleanup might introduce aProjectDeleteFailedaction so the General page could surface delete errors distinctly from save errors (e.g. a toast near the danger zone). Pure naming/clarity nicety; the behavior is correct as-is.Descriptionhas noHasMaxLength— intentional per the comment ("free-form, unbounded like Entry.Description"), but if Entry.Description ever gets a bound, Project.Description should follow for consistency. Just something to keep in the back of your mind~Automated review by Jibril · 2026-07-10
CI/CD: ✅ passed for head SHA
1de9dd8(forgejo-actions: 245/245 tests, 92.3% line / 86.2% branch coverage) · Local checks: skipped (CI green)Jibril's non-blocking note 1, taken in hardened form: SegmentKey now strips ?/# before extracting the last segment (the same guard NavItem uses), with a test navigating to lore?tab=history. The suggested {Section} catch-all route is deliberately not adopted — an unknown section must 404, not render a stub; the rationale is now a comment on the six explicit routes. Notes 2 (a distinct ProjectDeleteFailed action) and 3 (Description max length) deferred: the first earns its keep when a delete-specific surface exists, the second follows Entry.Description if that ever gains a bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Thanks for the approval — took note 1 in hardened form in
f4f2630:SegmentKeyfragility) — the real risk you spotted (query/fragment) is now stripped before the segment extraction (the same guardNavItemuses), with a test navigating tolore?tab=history. I deliberately didn't switch to a{Section}catch-all route: the six explicit@pageroutes mean an unknown section 404s instead of rendering a stub, which is the better routing behavior — that rationale is now a comment at the routes.ProjectDeleteFailed) — deferred until a delete-specific surface exists (e.g. a toast at the danger zone); until then the distinct action would change nothing observable.Descriptionbound) — agreed; it followsEntry.Descriptionif that ever gains a max length.+1 test (246 total), build clean, CI running on
f4f2630. Ready to merge.