Skip to content

Commit 2f2c6b9

Browse files
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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/fields': patch
3+
'@object-ui/plugin-detail': patch
4+
---
5+
6+
feat(fields): every record reference shows a hover card with the target's declared highlights, without navigating
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
Hovering a record reference for 400 ms, or pressing Space on it, shows a floating card: the target object's icon and label, the record's name as a link that opens it the way a click on the reference does, and the object's declared `highlightFields` minus the name field, at most six, drawn by the same cell renderers a list uses (option labels, currency, dates, lookups). Esc, moving the pointer away or blurring the reference hides it, and one card is open at a time. Focus alone does not show it.
12+
13+
- **Where.** Every lookup and master-detail value that links to its record (`LookupCellRenderer`): a relation column in a list, in a related list's table and in its enlarged view, and a lookup in the record's details. On the record page's related column, the name in a compact related row shows the card too, and its title opens the row as the name does.
14+
- **What it reads.** Nothing until a card is shown; then one `findOne` for the record, with `$expand` naming only the reference fields the card draws. The answer is kept per record and read again after the data bus says the record changed, so a list of 200 rows reads nothing on render.
15+
- **Metadata only.** Only the object's declared `highlightFields` are drawn: a name the object does not define, the name field and a field the viewer may not read are left out. An object that declares none shows the record's name and the object label. A record the viewer cannot read shows the reference's name and the object label.
16+
- **Layout.** The card is portalled to the document body, so the row, cell or chip under it keeps its layout.
17+
18+
`@object-ui/fields` exports `RecordHoverCard` (with `RecordHoverCardProps` and `RecordHoverCardObjectDef`) for a host that draws a reference of its own: wrap the element that names the record and pass the target's `objectName`, `recordId` and the object definition the host already holds.

0 commit comments

Comments
 (0)