fix(plugin-grid): the link column renders a real anchor when the host publishes record URLs (#4490) - #4531
Merged
Merged
Conversation
… publishes record URLs (#4490) The list `link: true` column was a `span role="link"` with no href, so the surface users open records from had none of a link's native affordances and a weaker a11y contract than the real anchors PR #4489 gave detail/related lookup values. `LinkCell` now consumes the host-published `recordHref` on `RelatedRecordActionsContext` and renders a real `<a href>` with #4489's click split; `ObjectView` (the console list host) mounts the existing provider and publishes the SAME record-URL builder its "open in new window" action already used. No host URL, no anchor — today's span, unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…re the changeset (#4490) The reverse verification was run after committing the fix: revert ObjectGrid.tsx and ObjectView.tsx to origin/main, keep the new tests. Measured 16 red / 7 green — one more grid case than predicted, and the three click cases fail by not finding the anchor rather than by observing wrong click behaviour. The docblock now records what was measured instead of what was predicted. Both packages take a patch: each entry dist/index.d.ts is byte-identical (measured with dist/ and tsbuildinfo cleared between builds); only the module-local app-shell/dist/views/ObjectView.d.ts gains the two helper declarations, and it is not reachable from the entry. 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
|
PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)
Auto-merge armed (squash). Generated by Claude Code Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 13, 2026 05:21
This was referenced Aug 13, 2026
Merged
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.
Closes #4490
What was wrong
The list's
link: truecolumn — and the auto-linked primary field — rendered asa
span role="link"with nohref, navigating only throughnavigation.handleClick(row). So the surface users actually open records fromhad none of a link's native affordances: no middle-click / Cmd-click
open-in-new-tab, no "copy link address", no hover status-bar URL. And
role="link"without an href is a weaker contract for assistive tech than areal anchor.
It was also the odd one out. PR #4489 had just given record-detail and
related-list lookup VALUES real anchors, which left the list column as the
weakest of the three surfaces — the reverse of the situation #4336 opened with.
The seam (the measured question)
RelatedRecordActionsProvidermounted only on the record DETAIL body, viaRelatedRecordActionsBridgeinsideRecordDetailView. That is exactly why#4489's anchors reached lookup values there and never reached the list page.
Two candidate seams were measured:
app-shellObjectView)app-shellandplugin-gridboth already depend on@object-ui/react, where the context and itsrecordHref(added by #4489) live. No new schema key, no new context member.ObjectGridSchemamember in@object-ui/typesplus threading throughuseNavigationOverlay— a third package and a new public schema surface, for the same result.The first was taken: provider-mounting + context consumption, exactly the shape
the ruling allowed.
packages/react's context file is untouched — #4489already added
recordHref/openRecord, so no new optional member wasrequired.
The URL is not built in the grid.
ObjectViewpublishes its own builder,and that builder is the SAME expression its "open in new window" navigation
action has always used (strip a trailing
/view/:viewId, append/record/:recordId, derived from the live pathname). Both now call oneextracted
listRecordDetailUrl, so the anchor a user middle-clicks and the tabthe row's Cmd-click opens address the same record by construction rather than by
coincidence. That mirrors what #4489 did in the bridge, where
onViewandrecordHrefwere collapsed onto one builder.Two deliberate limits on what the list host publishes:
recordHrefaddresses THIS view's object only. The builder is derivedfrom the current list's pathname, so it cannot name a record of another
object; any other object returns
null, which consumers render as the plainvalue. A lookup cell pointing at a third object therefore renders exactly as
it does today. Inventing a URL for it here is the console: copied invitation link is unusable — buildAcceptUrl rebuilds the trailing-dot host that resolveHomeUrl already retired #4472 mistake — the
console-wide, any-object builder stays on the detail page's bridge, which has
the routable-object set this page does not.
resolvereturns no handlers. A list page has no related lists. Thecontext reads an omitted handler as "capability unavailable", i.e. the same
read-only outcome a consumer gets with no provider at all, so mounting this
provider grants nothing that was not there before. The record drawer mounts
RecordDetailView, whose bridge wraps the whole detail body — that nearerprovider keeps owning the detail surface, unchanged.
The change
LinkCellconsumesrecordHrefwhen the host publishes one and renders a realanchor, with #4489's click split:
preventDefault()+ the existingnavigation.handleClick(row),so the SPA path (drawer / modal / page, whatever the view configured) is
completely unchanged;
does with any link;
stopPropagation()on every click, including modifier clicks: without it aCmd-click would ALSO reach the row handler, which opens its own tab, for two
tabs from one click;
handler above turns into the SPA path). Space is the one affordance a link
does not do natively, so it is carried over from the span explicitly.
Accessible name is the display text, unchanged.
Graceful degradation, pinned
No host
recordHref(Studio designer, embedded renderers, standalone grids), ahost that cannot route to this object, or a row with no id — all render exactly
today's
span role="link". Pinned as the full markup string, not merely "noanchor", so a stray class or attribute cannot slip in unnoticed.
Red-first (measured, not predicted)
The fix was committed, then
ObjectGrid.tsxandObjectView.tsxwere revertedto
origin/mainwith the new tests kept. 16 red / 7 green.Grid file, 5 red:
Reported honestly rather than dressed up: the three click cases fail by not
finding the anchor, not by observing wrong click behavior. Against
origin/main, "no anchor" and "an anchor that mishandles modifier clicks" areindistinguishable, so those three discriminate in the FORWARD direction only —
they pin the split as
LinkCellis edited from here on. The case thatdiscriminates old from new is the first one.
Host file, 11 red — all
TypeError: listRecordDetailUrl is not a function/listRecordActionsValue is not a function. Also stated plainly in the file:these are pure functions that do not exist on
origin/main, so the module diesat import before an assertion runs. They pin the builder's contract (object
scoping, encoding, the
/view/strip, call-time pathname read) rather thanproving a behavior change.
Green in BOTH worlds (the controls, 7): the whole must-not-change block —
the byte-identical span, plain click still taking the SPA path with no
double-fire, non-link columns untouched, a lookup column pointing at another
object staying plain, a row with no id getting no anchor — plus the two "host
publishes no href" cases.
Verification
pnpm exec vitest run packages/plugin-grid/ …— 64 files, 605 testspassed.
pnpm exec vitest run packages/app-shell/— 366 files, 3539 passed, 1skipped.
existing
column-featureslink-cell tests: 6 files, 87 tests passed.tsc --noEmitandtsc -p tsconfig.test.jsonfor both touched packages:4/4 clean.
origin/mainon the two changed source files: 0errors both sides.
ObjectGrid.tsx244 warnings unchanged;ObjectView.tsx157 to 159 — the 2 new ones are
react-refresh/only-export-components, thesame warning this file already carries 8 times, one per exported helper its
tests import. CI deliberately sets no
--max-warnings.check-control-bytes,check-changeset-presence,check-changeset-no-major:all green.
Grading
Patch on both packages. Measured both ways with
dist/and*.tsbuildinfocleared between builds: each package's entry
dist/index.d.ts— the onlypath in either
exportsmap — is byte-identical. The full emitted treediffers in exactly one module-local file,
app-shell/dist/views/ObjectView.d.ts,which gains the two helper declarations; it is not reachable from the entry
(
grepfinds 0 occurrences there) andviews/index.tsre-exports onlyObjectView. Module-local additive, so patch — not minor, never major.Generated by Claude Code