Repository navigation
Commit 2f2c6b9
feat(fields): every record reference shows a hover card with the target's declared highlights (#27912)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDSnrkE4tAZzd2PBTPkxkA8)_
## 维护者速读
- 改了什么:控制台里每个记录引用(列表/放大表的关系列、详情列的 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`.
<details><summary>Readings at heads e57402a, 49504eb and
090db13</summary>
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 d67867f -> 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).
</details>
## 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 #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
<details><summary>Derived gates at heads e57402a and 49504eb</summary>
`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.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6Ca4Vk1969xZxELoSFG
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6Ca4Vk1969xZxELoSFG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 40be9b5 commit 2f2c6b9
7 files changed
Lines changed: 1147 additions & 35 deletions
File tree
- .changeset
- ui/packages
- fields/src
- __tests__
- plugin-detail/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
0 commit comments