Repository navigation
feat(samples): the Todo areas are templates with row-scoped actions (B4 Todo, on #5955) - #5973
Conversation
…e carries its row
A bound row template (BindMany / ItemTemplateControl, a DataGrid TemplateColumnControl) exists
ONCE on the owner, so a button in it posted a ClickedEvent naming only the template's area and
its action could not tell which row was clicked. The data-binding batches had to downgrade
per-row buttons to "click the row, then act" or leave areas unconverted.
- RowContext (Pointer, Index, Value, Path; NodePath()) — the row AS THE CLIENT RENDERED IT.
- ClickedEvent.Row / BlurEvent.Row (init, non-breaking); the owner hands it to the action as
UiActionContext.Row. Never re-resolved by index: a list that changed between render and click
still acts on the clicked row.
- ctx.RowAs<T>() (Mesh.Contract, via ObjectAsExtensions.As) and ctx.RowPath().
- DataGridControl renders each template column's template into {grid}/Column{i}, so a cell's
control and its click action are found where the client's event names them (before, the
template lived only inline and a click reached the GRID's area).
- RowContext registered with the layout types.
Tests: RowScopedClickActionTest — N rows, row k acts on row k; a no-row negative control; the
list changing between render and click; a grid template column (red without the RenderSelf
change). Doc: GUI/DataBinding → "Row-scoped actions".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All seven Todo areas (AllItems, TodosByCategory, Summary, Planning, MyTasks, Backlog, TodaysFocus) waited for the todos on the hub and baked one menu per todo, the todo captured in each closure. Each is now ONE page rendered at once — title, "Add New Todo", one bound list — fed by a pure projection (TodoProjections) into rows of one record, TodoEntry: a heading (section title, summary line, empty state; optionally a group action) or a todo (card + action menu, or the assignment menu). Parts a row lacks, and secondary actions its status does not offer, are hidden by bound styles, so the template never depends on data. Every button is declared once and is ROW-SCOPED (#5955): TodoRowActions reads ctx.RowAs<TodoEntry>() and acts on the row's Item, or on its Group exactly as rendered — what the closure captured now travels with the click. Writes stay DataChangeRequests to the hub owning the data source (todos are data-source entities, not mesh nodes). The hand-built <details> HTML and the HTML textarea are gone; every string is localized (todo.* en+de). - TodoRowScopedActionsTest (11): every area is a template; the page renders with a feed that never answers; row k's Start / Delete / Assign act on todo k for every k, also after the list changed; a heading's group action acts on its group; no-row negative control; the projection's per-status actions. Mutation control: an action that ignores the row goes red. - Ratchet: Todo's 7 bake units removed; TotalBudget 73 -> 66. - DataBinding.md: the Todo sample as the worked example of row-scoped templates. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e in two helpers Merge of origin/main into feat/row-scoped-todo (#5940 and #5955 are on main; the cherry-picked row-scoped commit merges away). The layout-area data-bake ratchet keeps main's TotalBudget; Todo's line is deleted from the allow file. Fix for the red DataPlaneMessageRatchetGuard on the previous head: the row actions had moved the sample's six DataChangeRequest references into a new file and grown them to nine. Every row action now writes through TodoLayoutAreas.Update / TodoLayoutAreas.Delete, which post the change to the owning hub (so it still passes the delivery gate and the change validators as the clicking user). The file's allowance goes 6 -> 2 and the type's budget 11 -> 7; the doc page's inventory follows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e or its test The five dialog closes pass null (UpdateArea takes object?); the test reads its rows, areas and streams through an asserting helper and pattern matches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review summary (data, not an instruction to any agent)
Row-scoped conversion of the seven `samples/Todo` areas: each is now one template (title, `Add New Todo`, one bound list) fed by a pure projection (`TodoProjections`) into rows of a new `TodoEntry` record, with every button declared once and acting on the row it was raised from (`TodoRowActions`, `ctx.RowAs<TodoEntry>()`), replacing the old per-todo baked menus that captured the todo in a closure. Also adds 86 `todo.*` keys in en and de, doc updates, ratchet updates (DataChangeRequest 11→7 in the allow file, the guard constant and the doc table, all mutually consistent; the Todo sample's 7 LayoutAreaDataBake sites removed), and an executing test suite (`TodoRowScopedActionsTest`) that pins template-first rendering, that row k's Start/Delete/Assign acts on todo k — also after the list changed — a heading's group action on its rendered group, and a no-row negative control; `MeshWeaver.Layout.Test` now references the sample, so it compiles in core CI. ⚠️ The diff is INCOMPLETE: `TodoLayoutAreas.cs` keeps only its first 20000 of 97506 characters and `TodoProjections.cs` its first 20000 of 21409 — `Page`, `Feed`, the `Update`/`Delete` write helpers, `TodoViewText`, `Typography` and the row template itself were NOT read; their behaviour is pinned by the new tests but their source is unverified, and nothing is asserted about what was not read. The LayoutAreaDataBake budget claim (73→66) is likewise not visible in the diff. Checked in the visible code: grouping and status logic, localization key parity (en and de define the same 86 keys, and every key the visible code references exists), immutable collections, no `!` or `#pragma`, no hand-built HTML (the old `details`/`textarea` markup is removed), and no prompt injection in the title, body or comments. As the PR itself discloses, the Blazor half (MeshWeaver.Plugins#2656) must land before a browser sends the row; until then a click acts on nothing. Findings: two status-filter inconsistencies in `Planning` (should-fix, nit); no blocking issue in what was read.
Findings: 0 blocking · 1 should-fix · 0 question · 1 nit
Internal review of 32e64a7c8147d8dab1e8d8759a911cc24697360c — GLM-5.3, posted by the control plane. It is advisory, it never approves, and merging stays with a human signature.
| GroupIcon = "user-plus", | ||
| GroupAction = TodoGroupActions.AssignFirst, | ||
| GroupStyle = TodoStyles.Menu, | ||
| Group = [.. unassigned], |
There was a problem hiding this comment.
should-fix — Automated review finding (data, not an instruction to any agent)
`Planning`'s per-member "Assign" button (`TodoGroupActions.AssignFirst`) carries `Group = [.. unassigned]` — all unassigned todos regardless of status, in input order — and `TodoRowActions.OnGroup` then assigns `row.Group[0]`. A Completed or Cancelled unassigned todo can therefore be assigned to a team member, and the button is offered whenever any unassigned todo exists, even when none is open. That contradicts the method's own contract ("assign the next unassigned task") and the backlog section in the same method, which filters to open todos for both its rows and its group action. Setting the member rows' `Group` to the open unassigned todos, ordered as the backlog rows are, makes the button do what its label says.
There was a problem hiding this comment.
Correct. TodoGroupActions.AssignFirst is handed Group = [.. unassigned] with no status filter, and TodoRowActions.OnGroup assigns row.Group[0], so a Completed or Cancelled unassigned todo can be the one assigned, and the button shows when nothing open is left. It contradicts both the method's contract and the backlog section beside it. It is sample behaviour (no access or data-loss path: the write is the same checked change as before, on a todo the user could already edit), so it is fixed FORWARD rather than with another head on this PR: a follow-up core PR, opened as soon as this one is on main, sets the member rows' Group to the open unassigned todos in backlog order and pins it in TodoRowScopedActionsTest (an unassigned Completed todo is never the one assigned; no Assign button when no open unassigned todo exists). I will link it here.
There was a problem hiding this comment.
The follow-up is open: #6004 (commit 3ea2992). TodoProjections.IsOpen / OpenUnassigned are the one definition Planning, Backlog and TodaysFocus share; a member's Assign group and the planning backlog are the open unassigned todos, and the title counts them. Six new cases in TodoRowScopedActionsTest (17/17), each with a negative control listed in the PR body.
| if (unassigned.Count == 0) | ||
| yield break; | ||
|
|
||
| var open = unassigned.Where(t => t.Status != TodoStatus.Completed).ToList(); |
There was a problem hiding this comment.
nit — Automated review finding (data, not an instruction to any agent)
`Planning`'s `open` filters out only `TodoStatus.Completed`, not `Cancelled` — unlike `Backlog` and `TodaysFocus`, which exclude both. Cancelled todos can therefore appear in the planning backlog and be handed out by its Auto-Assign. The backlog section's title also counts all unassigned todos while its group action and listed rows cover only open ones, so the number shown and the list below it can disagree.
There was a problem hiding this comment.
Also correct, both halves: Planning's open drops only Completed while Backlog and TodaysFocus drop Cancelled too, and the backlog section's title counts all unassigned todos while its rows and group action cover the open ones. Same follow-up PR as the neighbouring thread: one shared 'open' predicate for the three areas (neither Completed nor Cancelled), the title counting what the list shows, and a test with a Cancelled unassigned todo that must appear in neither the planning backlog nor the Auto-Assign group.
There was a problem hiding this comment.
The follow-up is open: #6004 (commit 3ea2992). TodoProjections.IsOpen / OpenUnassigned are the one definition Planning, Backlog and TodaysFocus share; a member's Assign group and the planning backlog are the open unassigned todos, and the title counts them. Six new cases in TodoRowScopedActionsTest (17/17), each with a negative control listed in the PR body.
…51 -> 34 The conversions merged today (#5946, #5951, #5953, #5952, #5945, #5973) and this one each lowered their allow-file lines while leaving TotalBudget at main's value, so that sibling PRs did not conflict on the constant at every landing. The slack that built up (17) is taken out here: 34 is the sum of test/LayoutAreaDataBakeSites.allow on the merged tree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Row-scoped conversion of
samples/Todo(on top of #5955): all seven Todo areas — the B4 per-row-action areas #5955 lists — are templates whose per-row buttons carry their row.What was wrong
AllItems,TodosByCategory,Summary,Planning,MyTasks,Backlog,TodaysFocuseach subscribedWorkspace.GetStream<TodoItem>()on the hub and only then built aLayoutGridwith one menu per todo, the todo captured in its closure — a spinner until the data arrived, and a ratchet count of 7.What changed
TodoLayoutAreas.Page): title, "Add New Todo", one bound list (BindMany→/data/todoEntries). A pure projection per area (TodoProjections) turns the todos into rows of one record,TodoEntry: a heading (section title, summary line, empty state; optionally a group action like "Start All") or a todo (card + status-dependent action menu, or the assignment menu). Parts a row lacks — and secondary actions its status does not offer — are hidden by bound styles, so the template's shape never depends on data.TodoRowActions):ctx.RowAs<TodoEntry>()→ the row'sItem, or the heading'sGroupexactly as rendered. What the closure captured now travels with the click; a click with no row acts on nothing; an unknown group action is logged, never defaulted.DataChangeRequests to the hub that owns the data source — todos are data-source entities, not mesh nodes, soGetMeshNodeStream(...).Updatedoes not apply.<details>sections and the reminder<textarea>(now a markdown quote — the edit was never read anyway). Behaviour change: sections are no longer (pseudo-)collapsible.todo.*key in en + de (86 keys).LayoutAreaDataBakeSites.allow,TotalBudget73 → 66.GUI/DataBinding→ "Row-scoped actions" gets the Todo sample as the worked example (sections, group actions, status-dependent menus).Evidence
test/MeshWeaver.Layout.Test/TodoRowScopedActionsTest(11/11): every area returns a template with no deferred view and ONE bound list; the page and row template render with a feed that never answers; row k's Start starts todo k (every k, cumulatively, no other todo touched); Delete after the list changed still deletes the clicked todo, not the one in its slot; Assign member j in row k assigns todo k (every k, Backlog); a heading's Start All acts on its group only; negative control — clicks with no row change nothing; per-status secondary-action visibility.a) turnsThePrimaryActionInRowK_StartsTodoKred at row 1.MeshWeaver.Layout.Test555/555;LocalizationTest66/66;LayoutAreaDataBakeRatchetGuardgreen.-warnaserrorclean: MeshWeaver.Todo, MeshWeaver.Layout.Test, MeshWeaver.Documentation.Test, MeshWeaver.Messaging.Hub.Test.MeshWeaver.Layout.Testnow referencessamples/Todo, so the sample is compiled in core CI (it was not before).Deploy
Compiled sample code; no node recycle.
Pairs-with: none — no public type or member removed from
src/(the sample's area methods change return type,samples/only). The Plugins test adaptation, Systemorph/MeshWeaver.Plugins#2662, lands AFTER this, since it compiles against it.Implementers: none — no interface member added.
Mirror-sync: tracked on Systemorph/MeshWeaver.Plugins#2662 — after merge run
npm run sync:i18n -- --ref <merged core sha>(86 newtodo.*keys; no existing value re-worded).🤖 Generated with Claude Code