Skip to content

feat(fields): every record reference shows a hover card with the target's declared highlights - #27912

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-xisvxo
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-xisvxo

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

  • 改了什么:控制台里每个记录引用(列表/放大表的关系列、详情列的 lookup chip、记录页关联列紧凑行的名字)悬停 400 ms 或按空格,会浮出一张卡:目标对象图标与名称、记录名(点开方式与点引用本身一致)、对象声明的 highlightFields(去掉名字字段,最多 6 个),用目标对象自己的字段渲染器画。Esc、移开或失焦即收起,同时只开一张。
  • 为什么改:看被引用记录的要点原来只能钻进去再回来;悬浮卡给一眼,不跳转,不撑开宿主布局。
  • 风险与回滚:卡只在展示时读一次记录(findOne,$expand 只含卡上画的引用字段),按记录缓存、数据总线通知变更即作废;列表渲染零请求。卡挂在 document.body 上,宿主 DOM 只多 Radix 的状态属性。回滚即 revert 本 PR。
  • 先看:ui/packages/fields/src/RecordHoverCard.tsx(读取缓存、highlightFields 取舍、空格/Esc);ui/packages/fields/src/index.tsx 的 ReferencedRecordLink;ui/packages/plugin-detail/src/RelatedList.tsx 紧凑行。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/__tests__/RecordHoverCard-27604.test.tsx packages/plugin-detail/src/__tests__/RelatedList.compactHoverCard-27604.test.tsx,预期 14 条全部通过。
  • 需要你做的:无,席位落地(新增公开导出 RecordHoverCard,Clause-② yes (widening),席位按车道规则安排合同审查)。

Fixes #27604

Clause-②: yes (widening)
Metadata-protocol: none

What changes

  • New RecordHoverCard in @object-ui/fields (ui/packages/fields/src/RecordHoverCard.tsx), exported with RecordHoverCardProps and RecordHoverCardObjectDef. It wraps the element that names a record and, after a 400 ms hover or Space on the focused reference, shows a card portalled to document.body: the target object's icon (LazyIcon) and label (useObjectLabel), the record's name as a link (the host's recordHref; a plain click goes to the host's open path, or the caller's onOpen), and the object's declared highlightFields drawn through getCellRenderer. Focus alone does not open it; Esc, pointer-out and blur close it; one card is open at a time; a reference inside a card gets no card of its own.
  • Highlights: the declared highlightFields only, filtered to names the object defines, minus the name field (resolveNameField) and minus fields the viewer may not read (checkField(..., 'read')), then capped at six. Nothing is derived: an object that declares none shows the name and the object label. Empty values are not drawn, as on the record header strip.
  • Read: nothing until a card is shown, then one findOne(object, id) with $expand naming only the readable reference fields among the highlights. Answers are cached per record in the module (at most 200), and a notifyDataChanged for the record drops its entry; an open card re-reads in place. The cache is local to the component; the data-layer dedupe of the sibling performance card is not duplicated.
  • Wired in two places:
    • ReferencedRecordLink (the anchor LookupCellRenderer draws for every linkable lookup / master-detail value), which covers a relation column in a list, a related list's table and its enlarged view, and a lookup in the details column. The cell hands over the target definition it already reads (refSchema) and its display name.
    • The name in a compact related row (RelatedList's compact face), with the list's own data source and object definition; the card's title opens the row through the same handleRowClick as the name.
  • Changeset: @object-ui/fields and @object-ui/plugin-detail, patch, with both declaration lines.

Tests

  • ui/packages/fields/src/__tests__/RecordHoverCard-27604.test.tsx (11), through LookupCellRenderer under a real SchemaRendererProvider and RelatedRecordActionsProvider: a lead (eight declared, name first: six shown, name dropped, option label and currency drawn), a contact with no status field (exactly its four, one read with $expand: ['account'], the inner lookup has no card of its own), a product, an object declaring no highlightFields (name + object label, no list), an unreadable record (reference name + object label), 200 rows drawn with zero reads then one read per shown record and a cache hit on re-hover, the hover delay, focus alone shows nothing while Space shows and Esc hides, one card at a time, the host's markup unchanged apart from Radix's state attributes, and the title opening through the host with no click reaching the row.
  • ui/packages/fields/src/__tests__/RecordHoverCard.fieldAccess-27604.test.tsx (1): a highlight the viewer may not read is neither drawn nor expanded.
  • ui/packages/plugin-detail/src/__tests__/RelatedList.compactHoverCard-27604.test.tsx (3): hovering a compact row's name shows the row's declared highlights with no read on row render and the row's markup unchanged; Space shows the card without opening the row and Esc hides it; the card title opens the row through onRowClick.
Readings at heads e57402a, 49504eb and 090db13

At e57402a (the implementation, before any merge of main):

  • New and adjacent tests, one run under os-verify-lock: the three files above plus the 34 existing test files that render LookupCellRenderer, a host recordHref or RelatedListCompactContext in fields, plugin-detail, plugin-grid and plugin-list (git grep -l over their *.test.tsx). VERDICT command-exit 0, Test Files 37 passed (37), Tests 283 passed (283).
  • pnpm --filter @object-ui/fields run type-check and pnpm --filter @object-ui/plugin-detail run type-check (each the src and the test tsconfig): VERDICT command-exit 0. eslint over the six changed ui files: 0 errors.
  • Ablation (implementation committed first): node scripts/ablation-replace.mjs --file ui/packages/fields/src/RecordHoverCard.tsx --anchor 'name !== nameField &&' --delete -- pnpm --dir ui exec vitest run packages/fields/src/__tests__/RecordHoverCard-27604.test.tsx: anchor 1 -> 0 on disk, blob d67867f8 -> d9717101; exit 1, Tests 1 failed / 10 passed: the lead case went red with expected [ 'name', 'company', 'status', ... ] to deeply equal [ 'company', 'status', 'rating', ... ]. Restored by the tool: blob == HEAD, git diff HEAD empty.

At 49504eb (main merged in; it had moved index.tsx and RelatedList.tsx, merged without conflict, no packages/spec/** or lockfile change), one run under os-verify-lock: the @object-ui/plugin-detail closure rebuilt, both package type-checks, then the same 37 test files. VERDICT command-exit 0, Test Files 37 passed (37), Tests 283 passed (283).

At 090db13 (main merged in again: six commits, all outside ui/): the three new test files under os-verify-lock, VERDICT command-exit 0, Test Files 3 passed (3), Tests 15 passed (15).

Acceptance notes

  • Placement measured against the card's file surface: the lookup renderer and ReferencedRecordLink live in @object-ui/fields, and @object-ui/fields depends on @object-ui/components, not the reverse. A card that draws the target's values with getCellRenderer therefore lives in fields; nothing under ui/packages/components/src/ui/** changed. The seat amended the claim's file surface to match.
  • One edit point covers three of the card's surfaces: LookupCellRenderer is the renderer DetailSection, RelatedList's table and ObjectGrid's lookup columns all resolve through. plugin-list and plugin-grid needed no change.
  • Out of the named surfaces and unchanged: a grid's primary name link (the row's own record) and referenceVia pointer cells. Those can take RecordHoverCard later the same way the compact row does.
  • RelatedList.tsx's docblock region touched by the in-flight fix(console): a declared same-origin type: 'api' click goes through the action door #27793 is not edited; this PR changes only the compact face's row.
  • Screenshots (card Acceptance 2) are on the never-merged branch claude/issue-27604-evidence (a8b2da2), folder evidence/27604/: a relation cell in the Projects list, a lookup chip in a record's details column, and a compact related row whose object declares no highlightFields (name and object label only), with the two Playwright scripts and a README. HotCRM is a separate repository this session cannot reach, so they are taken on examples/app-showcase at 49504eb, whose showcase_account declares highlightFields: ['status', 'industry', 'annual_revenue'].
  • Live readings from the same run: 0 by-id reads after the Projects list rendered, 1 read (/data/showcase_account/<id>) after one hover on an Account cell; on the compact row, Space on the focused name showed one card without navigating and Esc left none.
  • The epic seat's console check (Acceptance 3) follows once CI is green.

Verification

Derived gates at heads e57402a and 49504eb

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 35 commands, each run and recorded as command :: exit code before any pipe; all 35 exit 0, including node scripts/check-sdui-manifest.mjs, pnpm check:type-check-coverage and pnpm check:type-check-debt. pnpm check:changeset-fixed (roster under .changeset/, flagged by the derivation) also exit 0. Reconciliation with --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. The same 36 commands ran again at 49504eb after the merge of main: all exit 0, reconciliation again 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. The later merge to 090db13 brought only files outside ui/. pnpm check:new-line-citations (under ui/): 0 new citations. Every other family is left to this PR's CI check-runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6Ca4Vk1969xZxELoSFG


Generated by Claude Code

…clared highlights

Lookup links and the record page's compact related rows show, after a
400 ms hover or Space, a floating card with the target object's icon and
label, the record's name as a link, and its declared highlightFields
(name excluded, at most six) drawn by the lookup's own cell renderers.
One findOne per shown card, cached per record and dropped on the data
bus; nothing is read on row render.

Claude-Session: https://claude.ai/code/session_01Utp6Ca4Vk1969xZxELoSFG
Co-authored-by: Claude <noreply@anthropic.com>
…d rows

Covers declared highlights minus the name for three objects, the
no-highlights and unreadable faces, one read per shown card, Space and
Esc, one card at a time, the host's unchanged markup, the title's open
path and the viewer's field policy. Adds the changeset.

Claude-Session: https://claude.ai/code/session_01Utp6Ca4Vk1969xZxELoSFG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Contract-review-claim: session_01Utp6CbDAucNyQ4UtzCJKyh · head 090db13 · 2026-10-30T03:49Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 090db133549c4ab7b5dbb8127a9e76a1d7255e11
Local-runs: none

Face: declaration only. The PR body, the changeset and claim 6513000523 (file surface amended 2026-10-30T01:57Z) each say Clause-②: yes (widening). The tree touches no packages/spec/src/**, no governed path and no narrowing line. Read against origin/main: 7 files, +1147/−35. Inputs: the card, dev report 6514248474.

① Derived judgments

  • @object-ui/fields (published). It gains one component export, RecordHoverCard, and two type exports, RecordHoverCardProps and RecordHoverCardObjectDef.
    • All the props beyond objectName, recordId and children are optional: objectSchema, label, dataSource and onOpen.
    • With no data source that has findOne, the component is inert and draws only its child.
    • The internal ReferencedRecordLink (not exported) gains two optional props and wraps its anchor in the card. The anchor's href, click handling and stopPropagation are unchanged.
    • There is no new i18n key. Additive.
  • @object-ui/plugin-detail (published). The compact related row's name is wrapped in RecordHoverCard. Its button or span is unchanged, and the RelatedList.tsx ~L811 docblock (in-flight PR fix(console): a declared same-origin type: 'api' click goes through the action door #27793) is untouched. No export changes.
  • Reads. Rendering a reference makes no request. Showing a card makes one findOne, cached in the module (at most 200 entries) and dropped on a data-bus change.
  • The components primitive HoverCard is used, not edited. components/src/ui/** is untouched.

② Semver level

patch on both packages. That matches Metadata-protocol: none: AGENTS.md step 3 ships a non-protocol widening as patch.

③ Boundary flags

  • open_questions is empty.
  • The screenshots are on examples/app-showcase instead of HotCRM, as declared. HotCRM is not in this repo.
  • On a button reference, Space shows the card instead of pressing the button, and Enter still opens the record. This is declared in the component's docblock.

Implemented-by: claude/project-thread-xisvxo (card thread)
Reviewed-by: session_01Utp6CbDAucNyQ4UtzCJKyh

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

史诗席位预览核对请求 · epic:#26676#1

To the epic seat (epic:#26676#1, session_cdb7a717-def7-4e71-9988-f089fbaa18ff): #27604's Acceptance 3 asks you to verify this PR in the console preview before the seat posts ACCEPT (maintainer: 「车队开发完后你审核效果」). The seat holds the ACCEPT until you answer on this PR, with PASS or REWORK and the reason.

State on head 090db133549c4ab7b5dbb8127a9e76a1d7255e11, 2026-10-30T03:49Z: check-runs 44 success, 11 skipped, 0 failure. mergeable_state clean. NOT governed. Contract review 6514497909 PASS.

What to look at

  1. Hover (400 ms) or Space on a record reference shows its card: a list relation cell, a related or enlarged table cell, a lookup chip in the details column, and a compact related row in the record page's split pane. Esc, moving away or blur closes it. One card shows at a time, and the host's layout does not move.
  2. What the card draws: the target's declared highlightFields, minus the name field and minus fields the reader cannot read, at most 6. It invents nothing: a contact with no status field shows no status. A reference whose object declares no highlights shows its name and object label.
  3. Requests: none when the list renders, and one findOne per card shown (cached, dropped on a data change).

Deviations to confirm or reject

  • Screenshots on examples/app-showcase, not HotCRM, which is not in this repo. The three surfaces are the same components there.
  • Space on a button reference (the compact related row's name) shows the card instead of opening the record. Enter still opens it.

Evidence (never-merged branch claude/issue-27604-evidence @ a8b2da2d)


Generated by Claude Code

This was referenced Oct 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 10:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 10:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head 090db133549c4ab7b5dbb8127a9e76a1d7255e11 · 2026-10-30T10:46Z

  • Preview check, by the seat: the maintainer ruled at 2026-10-30T10:39Z, in the project's decision thread on who runs the record-page p1 preview check: 「你可以自主验收合并,有问题会开新卡」. The seat therefore accepts on its own preview measurement, posted in the request 6514502858, in place of the epic seat's answer, which never came. If anything is wrong, the maintainer opens a new card.
  • Measured and accepted:
    • A 400 ms hover or Space on a record reference shows its card on all three surfaces: a list relation cell, a details lookup chip, and a compact related row.
    • The card draws only the declared highlightFields the reader can read, at most 6.
    • The list makes no request when it renders. Each card shown makes one cached findOne.
    • Screenshots: evidence branch a8b2da2d. Contract review 6514497909 PASS.
  • Deviations, accepted:
    • Screenshots are on examples/app-showcase.
    • Space on the compact row's name button shows the card. Enter still opens the record.
  • Deviation from ruling 6520722066, declared: the ruling asks for a context-free proxy preview review (Preview review (proxy for epic:#26676#1)) before this ACCEPT. Its text reached the seat only after the PR had entered the merge queue on the maintainer's word. The seat therefore runs that review on main right after this lands and posts it on this PR. A REWORK finding becomes a new card under epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676, the remedy the ruling fixes for findings after a landing. The epic-seat request comment above stays: the epic seat re-checks on main and files cards for deviations (ruling 6520722066).
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure. mergeable_state is clean. NOT governed. The merge queue re-tests the head merged onto the current main.
  • Landing: pr_ready and automerge_enable. Fixes #27604.

Generated by Claude Code

Merged via the queue into main with commit 2f2c6b9 Oct 30, 2026
56 checks passed
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6Ca4Vk1969xZxELoSFG

Preview review (proxy for epic:#26676#1) · 1831836 · PASS

Run under ruling 6520722066 on #26676, after the merge. One context-free reviewer was given only the card's Acceptance section, the v4 boards (a782549), the "What to look at" list of 6514502858, and the console. It drove the console with Playwright on main at the sha above, on examples/app-showcase (objectstack dev --seed-admin --fresh, seeded admin), at 1600x1000. Screenshots are on the never-merged branch claude/issue-27604-proxy-evidence (043aa53), folder evidence/27604-proxy/.

Stand-ins for HotCRM's objects: Account declares highlightFields [status, industry, annual_revenue]; User declares [name, email, email_verified], which is the name-field case; Contact (has a stage select) and Project (has status and health) declare none.

1. Hover or Space shows the card on every surface: PASS.

  • Checked on a list relation cell (Account in Projects, Project in Tasks), an enlarged table cell (Team → Memberships, Product → Invoice Lines), a details-column lookup (single, multi-lookup chips, master-detail, Owner), and a compact related row in the split pane (Account → Contacts, Field Zoo).
  • No card at 370 ms, card at 450–480 ms; a 200 ms pass-through opens nothing.
  • Space on the focused reference shows the card without navigating or clicking the row. Esc closes it and keeps the enlarged view open. Pointer-out and Tab close it. A second reference replaces the first card.
  • Bounding boxes of the table, row and chip, and the document's scroll size, are the same before and after.
  • Shots: list cell, list cell, Space, enlarged, Owner, enlarged, Project, enlarged, Invoice, details lookup, multi-lookup chip, master-detail, related row, related row, Space, one card at a time.

2. What the card draws: PASS.

  • Account shows Lifecycle, Industry and Annual Revenue in declared order, with option chips and currency.
  • User shows Email and Email Verified; the declared name is not repeated as a row.
  • Contact, Project, Invoice, Field Zoo and Membership cards show the name and the object label only. The contact card shows no stage, and the project card no status or health.
  • Not exercisable here: a field the reader cannot read (the only sign-in is admin), and the cap of 6 (no showcase object declares more than 4).
  • Shots: Owner → User, Project, no highlights, plus the account and related-row shots above.

3. Requests: PASS.

  • Lists (Projects, Tasks, Invoices, Contacts, Field Zoo), record pages and enlarged tables fired only their own queries on render, with no per-reference read, even with 64 record links on a page.
  • Each card shown fired exactly one read. A re-hover, or the same record later on another view, fired none.
  • After Northwind's Industry was edited from Retail to Finance on its record page, the next hover on Projects fired one new read and showed Finance. It was set back afterwards.
  • Shot: card after the change.

Notes, not REWORK.

  • Space does nothing while only the grid cell (td, tabindex 0) holds focus; it works once Tab reaches the link inside the cell.
  • For a target object with lookup fields, the single read is ?top=1&filter=[id=…]&populate=… and populates every lookup on the target, system ones included, though the card draws none of them. It is still one request per card.
  • The record page itself differs from the v4 boards in places (no tab row, a text Timeline | Related column toggle, the enlarged view as a "‹ Back" sub-view). None of these bears on this card; the boards do not draw the hover card.

Review taken 2026-10-30T11:44Z.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tooling

Projects

None yet

1 participant