Project workspace: shell, side menu, breadcrumb, General page (edit + soft delete) #18

Merged
bjoern merged 2 commits from feat/workspace-shell into main 2026-07-10 06:38:50 +02:00
Member

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) and RenameUpdateDetails(name, description, ts); AddProjectDescription migration.

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). ProjectDto carries Description; the store port grows FindBySlug/UpdateDetails/SoftDelete in the established store-completes-the-operation pattern.

The shell

  • WorkspaceShell: breadcrumb (Projects › name › section, clickable up the path), NavList with 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.
  • Load-by-slug is locally guarded, so a NotFound outcome can't re-dispatch forever.
  • Cross-feature reducers: a workspace rename re-sorts the already-loaded project list; a delete removes the row — the list stays fresh within the circuit.

General page

  • Prefilled title + description form — the sync is keyed on the project id, so re-renders never clobber in-progress typing. Save disabled on blank/while saving; use-case errors surface on the field.
  • Danger zoneConfirmDialog (danger variant, backdrop can't dismiss) → soft delete → back at the list.
  • One WorkspaceSectionPage stubs 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 (--active now 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)

  • Integration (real SQLite): get-by-slug + not-found; update persists + journals Update; delete soft-deletes, journals, and Undo restores the project — the confirm dialog's "recoverable" promise, proven; double-delete fails cleanly.
  • bUnit (real Fluxor pipeline): shell frame (menu groups, breadcrumb, General active), prefilled form, save persists, save disabled on blank title, confirmed delete → store soft-deleted + navigated home, cancelled delete → untouched, unknown slug → not-found, section stub (breadcrumb trail + Characters active, not General).
  • Unit: workspace reducers (incl. the cross-feature list update/removal), effects over real use cases.
  • Test infra consolidated: shared AdapterTestContext + one FakeProjectStore + 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

The [project-workspace story](https://git.kagaku.eu/TeamAI/Kagura/src/branch/main/docs/stories/project-workspace.md): 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) and `Rename` → **`UpdateDetails(name, description, ts)`**; `AddProjectDescription` migration. **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). `ProjectDto` carries `Description`; the store port grows `FindBySlug`/`UpdateDetails`/`SoftDelete` in the established store-completes-the-operation pattern. **The shell** - `WorkspaceShell`: breadcrumb (**Projects › name › section**, clickable up the path), `NavList` with 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. - Load-by-slug is **locally guarded**, so a NotFound outcome can't re-dispatch forever. - **Cross-feature reducers**: a workspace rename re-sorts the already-loaded project list; a delete removes the row — the list stays fresh within the circuit. **General page** - Prefilled title + description form — the sync is **keyed on the project id**, so re-renders never clobber in-progress typing. Save disabled on blank/while saving; use-case errors surface on the field. - **Danger zone** → `ConfirmDialog` (danger variant, backdrop can't dismiss) → soft delete → back at the list. - One `WorkspaceSectionPage` stubs 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 (`--active` now 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) - **Integration (real SQLite):** get-by-slug + not-found; update persists + journals `Update`; **delete soft-deletes, journals, and `Undo` restores the project** — the confirm dialog's "recoverable" promise, proven; double-delete fails cleanly. - **bUnit (real Fluxor pipeline):** shell frame (menu groups, breadcrumb, General active), prefilled form, save persists, save disabled on blank title, confirmed delete → store soft-deleted + navigated home, cancelled delete → untouched, unknown slug → not-found, section stub (breadcrumb trail + Characters active, not General). - **Unit:** workspace reducers (incl. the cross-feature list update/removal), effects over real use cases. - Test infra consolidated: shared `AdapterTestContext` + one `FakeProjectStore` + `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](https://claude.com/claude-code)
feat(workspace): the project workspace shell — side menu, breadcrumb, General page
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 21s
1de9dd83f9
The project-workspace story: opening a project now lands in a real workspace with a
breadcrumb bar, a grouped side menu, and a General page that edits meta and
soft-deletes with confirmation.

- Domain: Project.Description (blank normalizes to null); Rename becomes
  UpdateDetails(name, description, ts); AddProjectDescription migration.
- UseCases: GetProject(slug), UpdateProject (title required + trimmed),
  DeleteProject (soft, journaled — the UI confirms, reversibility is the safety
  net); ProjectDto carries Description; IProjectStore gains FindBySlug/
  UpdateDetails/SoftDelete in the store-completes-the-operation pattern.
- Workspace Fluxor feature: load-by-slug (locally guarded so NotFound can't
  re-dispatch), save + delete lifecycles; cross-feature reducers keep the
  already-loaded project list fresh (rename re-sorts, delete removes).
- WorkspaceShell: breadcrumb (Projects › name › section) + NavList with the
  corrected IA (General; Records: Characters/Locations/Lore/Relationships/Timeline;
  separator; Visual novel: Chapters — scenes live under chapters) + content area
  with loading/not-found states. Five menu icons added to the catalog.
- General page: prefilled title + description form (sync keyed on project id so
  re-renders never clobber typing), Save disabled on blank/saving, use-case errors
  on the field; Danger zone -> ConfirmDialog -> soft delete -> back to the list.
- One WorkspaceSectionPage stubs all six section routes (name/icon from the URL).
- NavItem CSS fix the browser pass caught: :hover outranked --active, so the
  current section lost its accent under the cursor; active now wins on hover too.
- Test infra consolidated: shared AdapterTestContext (Fluxor + use cases over one
  fake store), shared FakeProjectStore + TestDoubles replacing three nested copies.

Tests: +24 (245 total) — integration proves update/delete journal entries and that
undo restores a deleted project (the confirm dialog's promise); bUnit covers the
shell frame, prefilled form, save, confirmed + cancelled delete, not-found, and the
section stub's breadcrumb/active state. Verified end-to-end in the real browser:
create -> edit -> save (checked in SQLite) -> section nav -> delete -> journal rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 07/10/2026 - 04:38:44
Coverage date: 07/10/2026 - 04:38:39 - 07/10/2026 - 04:38:42
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 110
Files: 99
Line coverage: 92.4% (2447 of 2648)
Covered lines: 2447
Uncovered lines: 201
Coverable lines: 2648
Total lines: 5463
Branch coverage: 86.4% (429 of 496)
Covered branches: 429
Total branches: 496
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 58.6%
Name Line Branch
Kagura.BlazorAdapter 58.6% 72.6%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 93.7% 75%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 95% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 92.8%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.Projects.Project 100% 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.6%
Name Line Branch
Kagura.Infrastructure 95.6% 88%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 85.2% 85.2%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100% 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 95.2%
Name Line Branch
Kagura.Server 95.2% 68.4%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 0% 0%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.7%
Name Line Branch
Kagura.UI 97.7% 94.4%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UseCases - 96.1%
Name Line Branch
Kagura.UseCases 96.1% 96%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 42.8%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.DeleteProject 100% 100%
Kagura.UseCases.Projects.GetProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
Kagura.UseCases.Projects.UpdateProject 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 04:38:44 | | Coverage date: | 07/10/2026 - 04:38:39 - 07/10/2026 - 04:38:42 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 110 | | Files: | 99 | | **Line coverage:** | 92.4% (2447 of 2648) | | Covered lines: | 2447 | | Uncovered lines: | 201 | | Coverable lines: | 2648 | | Total lines: | 5463 | | **Branch coverage:** | 86.4% (429 of 496) | | Covered branches: | 429 | | Total branches: | 496 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 58.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**58.6%**|**72.6%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|93.7%|75%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|95%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|92.8%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.Projects.Project|100%|100%| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.6%**|**88%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|85.2%|85.2%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|100%| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**95.2%**|**68.4%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|0%|0%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.7%**|**94.4%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| </details> <details><summary>Kagura.UseCases - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.1%**|**96%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|42.8%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.DeleteProject|100%|100%| |Kagura.UseCases.Projects.GetProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| |Kagura.UseCases.Projects.UpdateProject|100%|100%| </details>
Member

🔮 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. UpdateDetailsAsync and SoftDeleteAsync in EfProjectStore both call SaveChangesAsync on a tracked entity, so the KaguraDbContext save override journals them transactionally — Update for details, SoftDelete for delete (via the IsDeleted transition in DetermineOperation). 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~

  • ProjectDto schema migration is complete. The Description field flows everywhere: domain → store port → EF store → DTO → DI registration → Fluxor state/reducers/effects → the prefilled form. The migration (AddProjectDescription), the model snapshot, and ProjectConfiguration all agree. Nothing dangles.

  • The sync-keyed form pattern (_syncedProjectId guard in SyncFormFromState) 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.OnWorkspaceSaved re-sorts the list by UpdatedAt after a workspace edit, and OnWorkspaceDeleted removes 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 != Slug check) prevents the infinite re-dispatch loop. Correct and locally contained.

  • Test infra consolidation — extracting AdapterTestContext, FakeProjectStore, and TestDoubles to 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:hover outranks .class--active and 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~

  • DeleteProject error path reuses ProjectSaveFailed — a minor naming smell but a correct UX choice: the delete-error surfaces on the title field's Error binding, which is visible. Not silently swallowed.

💡 Little ideas (non-blocking)~

  1. WorkspaceSectionPage.SegmentKey — parsing Navigation.ToBaseRelativePath(Navigation.Uri) with LastIndexOf('/') works but is fragile if query strings or fragments sneak into a section URL later. Since you already have @page route 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.

  2. ProjectSaveFailed for delete errors — functionally fine, but a future cleanup might introduce a ProjectDeleteFailed action 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.

  3. Description has no HasMaxLength — 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)

## 🔮 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.** `UpdateDetailsAsync` and `SoftDeleteAsync` in `EfProjectStore` both call `SaveChangesAsync` on a tracked entity, so the `KaguraDbContext` save override journals them transactionally — Update for details, SoftDelete for delete (via the `IsDeleted` transition in `DetermineOperation`). 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~* - **`ProjectDto` schema migration is complete.** The `Description` field flows everywhere: domain → store port → EF store → DTO → DI registration → Fluxor state/reducers/effects → the prefilled form. The migration (`AddProjectDescription`), the model snapshot, and `ProjectConfiguration` all agree. Nothing dangles. - **The sync-keyed form pattern** (`_syncedProjectId` guard in `SyncFormFromState`) 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.OnWorkspaceSaved` re-sorts the list by `UpdatedAt` after a workspace edit, and `OnWorkspaceDeleted` removes 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 != Slug` check) prevents the infinite re-dispatch loop. Correct and locally contained. - **Test infra consolidation** — extracting `AdapterTestContext`, `FakeProjectStore`, and `TestDoubles` to 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:hover` outranks `.class--active` and 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~ - **`DeleteProject` error path reuses `ProjectSaveFailed`** — a minor naming smell but a *correct* UX choice: the delete-error surfaces on the title field's `Error` binding, which is visible. Not silently swallowed. #### 💡 Little ideas (non-blocking)~ 1. **`WorkspaceSectionPage.SegmentKey`** — parsing `Navigation.ToBaseRelativePath(Navigation.Uri)` with `LastIndexOf('/')` works but is fragile if query strings or fragments sneak into a section URL later. Since you already have `@page` route 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. 2. **`ProjectSaveFailed` for delete errors** — functionally fine, but a future cleanup might introduce a `ProjectDeleteFailed` action 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. 3. **`Description` has no `HasMaxLength`** — 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)*
fix(workspace): strip query/fragment from the section-name derivation (review)
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 22s
f4f2630acc
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>
Author
Member

Thanks for the approval — took note 1 in hardened form in f4f2630:

  • #1 (SegmentKey fragility) — the real risk you spotted (query/fragment) is now stripped before the segment extraction (the same guard NavItem uses), with a test navigating to lore?tab=history. I deliberately didn't switch to a {Section} catch-all route: the six explicit @page routes 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.
  • #2 (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.
  • #3 (Description bound) — agreed; it follows Entry.Description if that ever gains a max length.

+1 test (246 total), build clean, CI running on f4f2630. Ready to merge.

Thanks for the approval — took note 1 in hardened form in `f4f2630`: - **#1 (`SegmentKey` fragility)** — the real risk you spotted (query/fragment) is now stripped before the segment extraction (the same guard `NavItem` uses), with a test navigating to `lore?tab=history`. I deliberately *didn't* switch to a `{Section}` catch-all route: the six explicit `@page` routes 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. - **#2 (`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. - **#3 (`Description` bound)** — agreed; it follows `Entry.Description` if that ever gains a max length. **+1 test (246 total)**, build clean, CI running on `f4f2630`. Ready to merge.
bjoern merged commit 67455b9053 into main 2026-07-10 06:38:50 +02:00
bjoern deleted branch feat/workspace-shell 2026-07-10 06:38:50 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagura!18
No description provided.