Repository navigation
feat(layout): row-scoped actions — a click inside a bound row template carries its row - #5955
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>
Test Results (shard 1) 2 files - 1 2 suites - 1 5m 16s ⏱️ -18s Results for commit 8d90eed. ± Comparison against base commit 86c840b. This pull request removes 661 and adds 5 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 693 tests +583 1 693 ✅ +583 4m 7s ⏱️ +13s Results for commit 8d90eed. ± Comparison against base commit 86c840b. This pull request removes 80 and adds 661 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Results (shard 4)588 tests - 179 588 ✅ + 14 2m 7s ⏱️ +6s Results for commit 8d90eed. ± Comparison against base commit 86c840b. This pull request removes 216 and adds 37 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 530 tests 1 530 ✅ 9m 31s ⏱️ Results for commit 8d90eed. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 3 files - 1 3 suites - 1 16m 42s ⏱️ + 3m 14s Results for commit 8d90eed. ± Comparison against base commit 86c840b. This pull request removes 37 and adds 1147 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Test Results 15 files - 5 15 suites - 5 40m 45s ⏱️ +4s Results for commit 8d90eed. ± Comparison against base commit 86c840b. This pull request removes 317 and adds 15 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Automated review summary (data, not an instruction to any agent)
Adds the owner half of row-scoped actions. A new `RowContext` record (Pointer, Index, Value, Path, plus `NodePath()`, which reads `Path` or the row value's own `path` property) rides as an init-only property on `ClickedEvent` and `BlurEvent`; `LayoutAreaHost.OnClick`/`OnBlur` relay it verbatim into `UiActionContext.Row`; `RowAs<T>` (Mesh.Contract, through `ObjectAsExtensions.As<T>`, never a cast) and `RowPath()` read it; `RowContext` is registered with the layout types so it round-trips typed; and `DataGridControl.RenderSelf` now renders every `TemplateColumnControl`'s template into its `Column{index}` sub-area (`DataGridControl.TemplateColumnArea(i)`), where the client's event names it. Documentation gains a Row-scoped actions section; `RowScopedClickActionTest` adds four executing tests through a real client-to-owner stream: every row's button acts on its own row, a no-row negative control, a list changed between render and click, and a grid template column. Checked from the diff: the Row is carried verbatim from the event with no owner-side re-resolution by index or pointer, so the stale-row invariant holds by construction; the negative control pins the degrade path for a client that stamps no Row; production code adds no async/await, no null-forgiving or pragma silencing, and uses framework controls and immutable collections; the row is treated as user input throughout and this diff adds no authorization decision of its own; no text in the title, body or diff addresses a reviewer or attempts to alter this review. Could not verify from this diff: the MeshWeaver.Plugins client half and its claimed test results (separate repository, lands after this PR); the internals of `GetContextForArea`, `RenderArea` and `EntityStoreAndUpdates` (the item lists no failed jobs, but green CI is the only evidence); and how a client built before the paired Plugins PR treats the new `Column{index}` area state (the question finding). A few identifiers arrive redacted in the diff copy reviewed here, so those exact lines could not be read verbatim; the findings rest on the unredacted text. Findings: one question, two nits — nothing blocking.
Findings: 0 blocking · 0 should-fix · 1 question · 2 nit
Internal review of 8d90eed772faff48d8da935ddfa6be055548046a — GLM-5.3, posted by the control plane. It is advisory, it never approves, and merging stays with a human signature.
| { | ||
| if (Columns[i] is not TemplateColumnControl { Template: { } template }) | ||
| continue; | ||
| var rendered = host.RenderArea(GetContextForArea(context, TemplateColumnArea(i)), template, ret.Store); |
There was a problem hiding this comment.
question — Automated review finding (data, not an instruction to any agent)
`RenderSelf` now renders every template column's template into a `Column{index}` sub-area for EVERY grid that has a `TemplateColumnControl` — a runtime change to what the owner emits for existing grids, beyond the added API surface — while the client half that consumes it lands in a later MeshWeaver.Plugins PR and the PR declares `Pairs-with: none` on API grounds. From this diff it cannot be determined how a client built before that Plugins change treats the new `Column{index}` area state it starts receiving: whether it ignores area state it never requested, or renders it in addition to the inline cell. The Deploy section covers compiled satellites, not this window; nothing in this repository pins it.
There was a problem hiding this comment.
Read against the client that is deployed today (MeshWeaver.Plugins main, src/MeshWeaver.Blazor.Views/Components/DataGridView.razor, lines 29–37): for a TemplateColumnControl it renders <DispatchView ViewModel="@templateColumn.Template" Stream="@Stream" Area="@Area"> — the template taken INLINE from the grid's own control, at the GRID's area. It never names or subscribes to {grid}/Column{i}. A client view renders only the areas a control it is drawing refers to, so the new Column{i} entries are additional state in the area's store that no existing view reads: they are not drawn a second time, and a click in a cell still posts the grid's area exactly as before this change. So in the window between this merge and the client half (Plugins #2656, still a draft) an existing grid behaves as it does today; the cost is the extra area state on the wire, one template per template column (not per row).
What is NOT pinned in this repository is exactly that statement about an older client — it cannot be, the client lives in Plugins. The existing ControlStyleRenderingTest template-column case there renders through the inline path and is the control arm; #2656's RowScopedActionsFromViewsTest pins the new path. Pairs-with: none stands on the gate's own terms (nothing public removed).
| var ctx = await Click(stream, ListTemplateArea, RowAsTheClientRendersIt(rendered, k)); | ||
|
|
||
| ctx.RowAs<Fruit>().Should().Be(Initial[k], $"the button in row {k} was clicked"); | ||
| ctx.RowPath().Should().Be(Initial[k].Path, "a node row names its node"); |
There was a problem hiding this comment.
nit — Automated review finding (data, not an instruction to any agent)
The repository's hard rules ban `!` or `#pragma` used to silence a warning. The production half of this change complies; this new test adds three null-forgiving operators: `ctx.Row!.Pointer` here, `JsonSerializer.SerializeToNode(...)!.AsObject()` in `ATemplateColumnButtonActsOnItsGridRow`, and the `(await ran.FirstAsync(...))!` return in the `Click` harness. Each is guarded — preceding assertions have already proved the value non-null, or the API contract guarantees it — so nothing real is silenced; flagged only because the rule as written draws no production/test line.
There was a problem hiding this comment.
Correct, and the rule draws no production/test line. Not fixed on this head: a push here restarts the suite and the review round on the PR four stacked row-scoped PRs wait on. The three operators are removed in the first of those I land after this one — #5969 (feat/row-scoped-plugin-catalog), which already carries this commit and gets a push anyway when it is rebased onto main: ctx.Row and the Click harness result via a pattern match that fails the test, SerializeToNode via Should().BeOfType<JsonObject>().Subject. I will link the commit here when it is pushed.
There was a problem hiding this comment.
Pushed, as promised — on #5944 rather than #5969 (which is held until the Blazor half, Plugins #2656, lands): commit 3aa778c, test/MeshWeaver.Layout.Test/RowScopedClickActionTest.cs. The three null-forgiving operators are gone (Assert.NotNull / Assert.IsType<JsonObject>), and EachRowsFieldBlursWithItsOwnRow pins the blur relay (rows blurred in order 2, 0, 3, 1; with Row = request.Message.Row removed from LayoutAreaHost.OnBlur it fails, restored it passes — 5 of 5).
| blurAction.Invoke( | ||
| new(request.Message.Area, request.Message.Payload ?? new object(), Hub, this) | ||
| { | ||
| Row = request.Message.Row |
There was a problem hiding this comment.
nit — Automated review finding (data, not an instruction to any agent)
The `BlurEvent` relay is symmetric with `OnClick` but is pinned nowhere in this repository: `RowScopedClickActionTest` exercises only `ClickedEvent`, while the new documentation section states that a `BlurEvent` carries the row the same way. A regression in these three lines would reach action authors as a silently null `ctx.Row` inside blur handlers. A blur twin of `EachRowsButtonActsOnItsOwnRow` would close the gap.
There was a problem hiding this comment.
Agreed — the blur relay in LayoutAreaHost is three lines with no test, and the doc promises the row on BlurEvent. The blur twin of EachRowsButtonActsOnItsOwnRow (a bound row template with a blur action; two rows blurred; each handler sees its own ctx.Row.Pointer and Index; negative control with the Row relay removed) goes into RowScopedClickActionTest in the same follow-up push as the null-forgiving cleanup — #5969, the first stacked row-scoped PR to land after this one. Not pushed here for the same reason: this head is the base four PRs wait on. I will link the commit here.
There was a problem hiding this comment.
Pushed, as promised — on #5944 rather than #5969 (which is held until the Blazor half, Plugins #2656, lands): commit 3aa778c, test/MeshWeaver.Layout.Test/RowScopedClickActionTest.cs. The three null-forgiving operators are gone (Assert.NotNull / Assert.IsType<JsonObject>), and EachRowsFieldBlursWithItsOwnRow pins the blur relay (rows blurred in order 2, 0, 3, 1; with Row = request.Message.Row removed from LayoutAreaHost.OnBlur it fails, restored it passes — 5 of 5).
…s LayoutTemplate #5940 and the batches that followed it are on main. Conflicts: - DataBinding.md: both sections kept (this PR's 'Two more shapes' before main's 'authoring samples'). The paragraph that called per-row actions a platform gap now points at Row-scoped actions (#5955 is on main). - LayoutAreaDataBakeSites.allow: main's removals kept, this PR's three lines (GitHubSyncSettingsTab, EditorExtensions, DataPathViews) removed. - LayoutAreaDataBakeRatchetGuard: TotalBudget stays at main's 51. The allow-file sums to 47; the guard reports the slack as STALE and the budget is lowered once, in the last PR of the series, so the sibling PRs do not conflict on that line. Also: the test-base copy LayoutTemplateAssertions (reflection over a protected member, with a null-forgiving operator) is dropped. main has the public MeshWeaver.Layout.LayoutTemplate for exactly this, so GitSyncActivityPanelIsATemplateTest asserts LayoutTemplate.DeferredViews is empty and walks LayoutTemplate.Descendants. Negative control re-run: one deferred view added to BuildActivityPanel fails it with 'StackControl[0]: ViewStream`1'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erators (review of #5955) Two findings from the review of #5955, promised on its threads for this PR: - RowScopedClickActionTest held three null-forgiving operators. They are replaced by Assert.NotNull / Assert.IsType, which narrow without one. - The BlurEvent relay in LayoutAreaHost.OnBlur was pinned nowhere while the doc promises the row on a blur. EachRowsFieldBlursWithItsOwnRow is the blur twin of EachRowsButtonActsOnItsOwnRow: a bound list of text fields with a blur action, every row blurred out of render order, each handler sees its own pointer, index, value and node path. Negative control: with the Row relay removed from OnBlur the new test fails (1 of 5); restored, 5 of 5. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#5940 and #5955 are on main; the cherry-picked row-scoped commit merges as identical changes. The data-bake ratchet keeps main's TotalBudget (51) and drops the CatalogLayoutAreas line this PR converts (allow-file sum 50); the budget is lowered once, in the last PR of the series. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erators (review of #5955) Two findings from the review of #5955, promised on its threads for this PR: - RowScopedClickActionTest held three null-forgiving operators. They are replaced by Assert.NotNull / Assert.IsType, which narrow without one. - The BlurEvent relay in LayoutAreaHost.OnBlur was pinned nowhere while the doc promises the row on a blur. EachRowsFieldBlursWithItsOwnRow is the blur twin of EachRowsButtonActsOnItsOwnRow: a bound list of text fields with a blur action, every row blurred out of render order, each handler sees its own pointer, index, value and node path. Negative control: with the Row relay removed from OnBlur the new test fails (1 of 5); restored, 5 of 5. 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>
feat(samples): the Todo areas are templates with row-scoped actions (B4 Todo, on #5955)
…-b3-settings Conflicts: RowScopedClickActionTest (the cherry-picked #5955 commit; main's version with its review follow-ups kept) and DataBinding.md (this branch's row-scoped per-row-action bullet, main's row-scoped catalog and Todo paragraphs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…coupon-list Conflicts: RowScopedClickActionTest (the cherry-picked #5955 commit; main's version kept), the PluginCatalog InternalsVisibleTo comment (now names both the catalog's and the coupon list's row-scoped actions) and DataBinding.md (this branch's per-row-action bullet, main's row-scoped catalog and Todo paragraphs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Platform mechanism for row-scoped actions (maintainer decision; Doc/GUI/DataBinding → new section "Row-scoped actions").
The gap
A bound row template —
BindMany(ItemTemplateControl) or aDataGridControlTemplateColumnControl— is declared ONCE and rendered by the client once per row. On the owner its controls exist once, at the template's area, so a button in it posted aClickedEventnaming only that area: every row ran the same action with nothing saying which row. The data-binding batches therefore downgraded per-row buttons to "click the row, then act", or left areas unconverted. For grids it was worse: a template column's control existed only INLINE in the grid, so a click in a cell reached the GRID's area and the button's own action never ran.The mechanism (core half — lands first)
RowContext(Pointer,Index,Value,Path;NodePath()): the row as the client rendered it.ClickedEvent.Row/BlurEvent.Row(init-only, additive);LayoutAreaHosthands it to the action asUiActionContext.Row.ctx.RowAs<T>()(Mesh.Contract, throughObjectAsExtensions.As, never a cast) andctx.RowPath().DataGridControl.RenderSelfrenders each template column's template into{grid}/Column{i}(DataGridControl.TemplateColumnArea), so the cell's control and its click action are found where the client's event names them.RowContextregistered with the layout types. The row is user input; the action authorizes by writing as the clicking user.MeshSearchrows need nothing: each result renders through the node's own item area, whose hub knows its path (documented).Client half
Systemorph/MeshWeaver.Plugins —
BlazorViewgets a cascadingRowand stamps it on every click and blur;ItemTemplatecascades each row (pointer, index, rendered value);DataGridViewcascades the grid row and renders template columns at their area. PR: see the paired Plugins PR (lands AFTER this one; it does not compile against a platform withoutRowContext).Evidence
test/MeshWeaver.Layout.Test/RowScopedClickActionTest(4, green): N rows — clicking row k acts on row k (value, pointer, node path) for every k; negative control — the same click with no row: the action cannot tell the row; the list changes between render and click — the click still acts on the clicked row while a fresh click acts on the new occupant of the slot; a grid template-column button acts on its grid row.ATemplateColumnButtonActsOnItsGridRowgoes red (no control atRowGrid/Column1).RowScopedActionsFromViewsTest(3, green) against this branch; red (2/3) with theRow = Rowstamp removed fromBlazorView. FullMeshWeaver.Blazor.Views.Test334/334.-warnaserrorclean: MeshWeaver.Layout, MeshWeaver.Mesh.Contract, MeshWeaver.Documentation, MeshWeaver.Layout.Test.MeshWeaver.Documentation.Test657/657.Reference conversion
The coupon admin list (from #5948) restores its per-row Open button as a row-scoped action — stacked on #5948's branch as a separate PR (
feat/row-scoped-coupon-list), since #5940 → #5944 → #5947 → #5948 have not merged.Remaining affected areas (for follow-up conversion agents)
Each was downgraded to "click the row / select, then act" or left unconverted for want of this mechanism. Convert with a
TemplateColumnControl(grid) or aBindManytemplate whose button readsctx.RowAs<T>()/ctx.RowPath():MeshWeaver.PluginCatalog/CatalogLayoutAreas.Catalog): install, update, remove, and the per-card update-policy select (a select in a row: its value is bound by pointer; anOnBlur/click action getsctx.Rowtoo).ThreadInbox, threadHeaderandChanges(per-row Undo; currently the hub-computedMessageChangesslot).samples/Todo— 7 areas with per-row action buttons (Plugins'MeshWeaver.Todo.Testasserts the bakedLayoutGridControl; pair it).IngestionandTranche Progress, UnderwritingClaimsReview.PartitionSyncAdmin.clients/reactitemTemplate.tsxposts clicks with no row) — a follow-up so React-rendered rows carry the row too.Deploy
Compiled platform code; no node recycle. Satellites gain the API on their next platform set.
Pairs-with: none — this change only ADDS public surface (no type or member removed); the Plugins client half lands after it.
Implementers: none — no interface member added (
ClickedEvent/BlurEvent/UiActionContextare records; the new members are init properties).Mirror-sync: none — no localization catalog change.
🤖 Generated with Claude Code