refactor(components): derive the row-menu predicate parameters from the authoring type (#4354) - #4423
Merged
Conversation
…ataTableSchema (#4354) One authoring shape had four declarations in data-table.tsx: the shared `isBuiltinRowActionVisible` gate and `planDataTableRowMenu` each hand-wrote `{ visibleWhen?: unknown }`, and `DataTableBuiltinRowActionItem` hand-wrote the full pair. None referenced the type whose values they receive. Each now derives from `DataTableSchema`. The planner keeps its deliberate visibility-only subset, `Pick`ed from the key its caller passes; the two consumers that serve both built-ins share one alias derived from the union of the twins, so they may only read keys both declare. Adds the observable pin the disabled-item counting rule never had: a row whose only action is `disabledWhen`-gated keeps its trigger, and that trigger opens the item, present and aria-disabled. The planner-level case that used to claim this is renamed to the verdict it actually decides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
ACCEPT — PM 复核 (session
Flipping ready + arming auto-merge. Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 12, 2026 05:31
This was referenced Aug 12, 2026
github-merge-queue Bot
pushed a commit
that referenced
this pull request
Aug 12, 2026
…m the spec-owned authoring type (#4429) (#4430) RowActionMenu.tsx carried its own `BuiltinRowActionPredicates` interface (`{ visibleWhen?: unknown; disabledWhen?: unknown }`) read at six declaration sites, tied to nothing. Measured true source: these predicates do NOT come from `DataTableSchema`'s twins — `ObjectGrid` resolves the object's `userActions` through `resolveRowCrudAffordances`, which returns `CrudAffordances`' `editPredicates` / `deletePredicates`, i.e. the spec-owned `RowCrudPredicates` (ADR-0103) re-exported by `@object-ui/core`. Each site now derives from that: per-key `Pick` for the planner's visibility-only parameters, one union alias for the two consumers that serve both built-ins. Inherits PR #4423's pattern wholesale, including its rider: the "a merely disabled item still counts toward the menu" rule was measured unpinned here too, so it gains a DOM pin where a user meets it (trigger survives, and opening it finds the item `aria-disabled`), and the planner-level case that claimed to pin it is renamed to the verdict it actually decides — the planner never reads `disabledWhen`. No runtime change. Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4354
Three hand-copies (four, once counted) collapse to one derivation
One authoring shape —
DataTableSchema.rowEditPredicates/rowDeletePredicates(objectui#2614) — had grown four independent declarations insidepackages/components/src/renderers/complex/data-table.tsx, none of which referenced the type whose values they receive:isBuiltinRowActionVisible{ visibleWhen?: unknown }visibleWhenplanDataTableRowMenu(x2 params){ visibleWhen?: unknown }visibleWhenDataTableBuiltinRowActionItem{ visibleWhen?: unknown; disabledWhen?: unknown }The issue counted three; the shared gate is a fourth, three lines above the planner, and leaving it hand-written would have kept a copy of the very shape this change exists to stop duplicating.
Per the ruling (option b, derivation over widening), each site now derives, and the signatures keep telling the truth about what each function reads:
planDataTableRowMenustays consumption-shaped — it decides visibility and nothing else — but each parameter isPicked from the exact schema key its production caller passes:editPredicates?: Pick< NonNullable< DataTableSchema['rowEditPredicates'] >, 'visibleWhen' >, and the delete twin fromrowDeletePredicates. Same subset as before, minus the drift.isBuiltinRowActionVisible, and the item component whose prop isname: 'edit' | 'delete') share one derived alias taken from the union of the twins, so they may only read keys that both schema keys declare — if either twin dropped one, the read stops compiling instead of silently readingundefined.The ITEM component: derived, not left
It was a hand-copy of the full pair with no reason to be one — it evaluates both keys, so it derives the whole authoring shape (
BuiltinRowPredicates) rather than aPick. This is the site with the most to gain; see the measurement below.No runtime code was touched.
@object-ui/typesis untouched — this derives from it.The rider: measured unpinned, now pinned
Measurement. The rule "an item that renders merely disabled still counts toward the menu's non-empty decision" was not pinned anywhere that could observe it:
data-table-row-menu-empty-guard.test.tsx'sstill counts an item that renders merely DISABLEDasserted the planner's return value, and the planner never readsdisabledWhen— the fixture behaved identically to{}, which is what made it vacuous.data-table-builtin-row-action-predicates.test.tsxdoes pin the disabled rendering observably (aria-disabled+ click suppressed), but it renders the item directly inside an already-open dropdown — it never goes through the table, so it cannot see whether the row got a trigger at all.visibleWhen. Nothing joined the two halves.Added (
keeps the trigger for a row whose only item renders merely DISABLED): a table whose single action isdisabledWhen-gated keeps its per-row trigger, and opening that trigger finds the item present andaria-disabled="true". End-to-end, because the two halves live in different functions and each half's own test stays green while the other regresses — demonstrated below.The vacuous case is renamed, not deleted:
an object with no visibleWhen does not hide the item. That is the verdict it actually decides, it is worth deciding, and the misleading name is gone. Its comment now points at where the disabled-counting claim really lives. The derived annotation on its fixture (tranche 4's work) is kept and still necessary: the planner's parameter remains a deliberatevisibleWhen-only subset, so a bare object literal still trips excess-property checking.Reverse verification
1. The derivation tracks its source — and the reported direction is not the one the template presumes. Renaming
visibleWhentovisibleWhenRenamedin@object-ui/types(scratch, reverted), then type-checking@object-ui/componentsboth ways:TS2344pointed straight at the derived declarations themselves — the compiler names the line that must be updated.origin/main): 2 errors, and this is the interesting half. I predicted green; it is not quite green, but the two errors are incidental — TS weak type detection firing at a call site ("has no properties in common"), never at a declaration. The declarations all compile clean against a type that no longer has the key.disabledWhenin common with the renamed type, so weak-type detection is satisfied, and neither its declaration nor either of its two call sites produces a single diagnostic. It would go on readingpredicates.visibleWhen === undefinedforever — i.e. the built-in gate silently becomes "always visible". Derived, that exact path isTS2345at the line where the item hands its predicates to the visibility gate.That is the real value here, and it is stronger than "before green, after red": the compiler goes from silent on the site that matters to naming it.
(Recorded for the next reader: this package's
tsconfig.jsonoverrides the rootpaths, so@object-ui/typesresolves through the built.d.ts, not sources. The first run of this simulation was a false green until@object-ui/typeswas rebuilt.)2. The new pin is red-first against the counting half. Scratch: make a
disabledWhen-gated item not count toward the trigger. Predicted the new pin red and every planner-level case green — measured exactly that:1 failed | 21 passed, the failure beingexpected [] to have a length of 2. Every pre-existing case, including the renamed planner one, stayed green — which is precisely why the pin was needed.3. And against the rendering half. Scratch: let the item return
nullwhen disabled. Predicted two reds — the new pin plus the pre-existing item-level case. Measured2 failed | 20 passed, bothUnable to find an element by: [data-testid="row-action-builtin-edit"].Both scratches reverted; the tree was re-verified green afterwards.
Verification
pnpm exec vitest run packages/components/ --maxWorkers=2(repo root) — 121 files / 1078 tests passed, no failures.tsc --noEmitandtsc -p tsconfig.test.jsonfor@object-ui/components— both exit 0 (build closure built first).eslinton both touched files — exit 0, 0 errors; the 5 remainingno-explicit-anywarnings are pre-existingas anycasts on untouched lines.check-control-bytes/check-phantom-dependencies/check-spec-symbol-derivation/check-changeset-presence/check-changeset-no-major— all green.patchfor@object-ui/components, nevermajor.Scope
Only
packages/componentsplus the changeset. Nothing inpackages/fields,plugin-charts,plugin-chatbotorplugin-dashboardwas touched. No out-of-scope findings to file.Generated by Claude Code