Skip to content

Commit 8bb2417

Browse files
fix(plugin-detail): the record strip is the declared highlights only; the path's stage and the owner show once (#28907)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb6wxbsWyHo3SfTcNubseV95)_ Fixes #28868 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:记录页的要点条(highlight strip)只画对象声明的 `highlightFields`(去掉标题字段,最多 6 个)。没声明就不画:默认记录页、手机记录头、记录悬停卡三处都一样,字段全部留在正文。路径条(`record:path`)画了阶段字段时,正文不再重复画这个字段。要点条里有 `owner_id` 时,正文下方的元信息行不再重复写负责人。系统字段 `owner_id` 的中文平台标签从「所有者」改为「负责人」,和 ui 的 `detail.owner` 统一。 - **为什么改**:维护者裁决 A1「没声明就不挑」(#28868)。以前没声明时按字段名猜 6 个(owner 打头)并从正文隐藏,小对象的正文因此被掏空;阶段在路径条和正文各出现一次;负责人在要点条和元信息行各出现一次,还用了「所有者」「负责人」两个词。 - **风险与回滚**:低到中。没声明 `highlightFields` 的对象(含 `examples/app-crm`)会失去要点条,直到 #28870 给 app-crm 补声明;HotCRM 已声明。没有新的元数据键,没有新门禁。回滚即还原本 PR。 - **先看**:`ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts`(`deriveHighlightFields`、`headerDrawnFields`、`drawnPathField`)、`ui/packages/plugin-detail/src/RecordMetaFooter.tsx`(负责人只出现一次)、`synth/__tests__/highlights-declaredOnly-28868.test.ts`。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/plugin-detail/src/synth/__tests__/highlights-declaredOnly-28868.test.ts packages/plugin-detail/src/__tests__/RecordMetaFooter.ownerOnce-28868.test.tsx`,预期 2 个文件、19 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes - **No guessed strip.** `deriveHighlightFields` (`@object-ui/plugin-detail`) returns the object's declared `highlightFields` minus the title field (the one `resolveTitleField` names), at most `max`, or `[]`. The preferred-name walk (`owner` / `owner_id` first, then amount, rating, industry, …), the type filter, the system-field skip list, the gated-group skip and the retired-type gate on that walk are deleted, not kept behind a flag. The export and its signature stay. `statusField` is accepted and no longer read; the docblock says so. - **The three readers follow it.** - The default page (`buildDefaultHighlights`, and the body's `hideFields`) asks it as before. An undeclared object now gets no `record:highlights` node and an empty hide list. - The phone header (`recordPhoneHeader.ts`) uses `declared ?? []`: no strip and no subtitle when nothing is declared, as on any width. - The hover card (`RecordPreviewCard.tsx`) asks `deriveHighlightFields(def, null)`, so an undeclared object shows the title alone. With no key field it now sends no `$expand`. `buildExpandFields` reads an empty column list as "every reference field", which the card would never draw. The new card test caught this. - **The stage once.** `buildDefaultPageSchema` reads the strip it actually placed: the default one, or an authored `highlights` slot. When that strip holds a `record:path`, its `statusField` joins the Details body's `hideFields`. `buildDefaultTabs` on its own hides what the default strip for the same options draws. Two private helpers do this, `drawnPathField` and `headerDrawnFields`. No public type moves: the exported `buildDefaultTabs` keeps its options type, and the page builder calls a private `synthTabs` with the computed list. - **The body's hide list is cut at six too** (a bounded in-place fix, same defect class). The strip has always drawn at most six, but `buildDefaultTabs` hid the whole list `RecordDetailView` passes in. So a declared seventh or eighth entry was drawn by neither the strip nor the body. `headerDrawnFields` cuts at `RECORD_HIGHLIGHTS_MAX`. The fix is pinned, and its ablation goes red. - **The owner once.** `RecordMetaFooter` leaves the owner out of the meta line when `deriveHighlightFields(objectSchema, null)` holds `owner_id`. Otherwise it keeps it. - **One word for the owner.** The ui key `detail.owner` and the spec's system-field label of `owner_id` (`SYSTEM_FIELD_LABELS` in `packages/spec/src/system/i18n-resolver.ts`) now say the same word in every language both declare: | Language | `detail.owner` (ui pack) | `owner_id` (spec) | Change | |---|---|---|---| | en | Owner | Owner | none | | zh-CN | 负责人 | 负责人 (was 所有者) | spec value | | ja-JP | 所有者 | 所有者 | none | | es-ES | Propietario | Propietario | none | | ko, de, fr, pt, ru, ar | 소유자, Inhaber, Propriétaire, Proprietário, Владелец, المالك | (no spec row) | none | The seat chose 「负责人」 for zh: the person who follows the record up. Every other language keeps the word its ui pack already uses. None of them is wrong for a record's owner. "Inhaber" is the common German CRM term (Datensatzinhaber). No ui pack value changes. A label an author wrote is still never overridden. ## Tests - **New: `synth/__tests__/highlights-declaredOnly-28868.test.ts`, 11 cases, rules 1–3.** - The card's account (industry, revenue, website, owner, no declared list) gets no strip and an empty body hide list. - A declared list is drawn exactly, minus the title, and the body hides exactly those fields. A declared entry past the sixth stays in the body. - An opportunity with a path hides `stage` from the body. Controls: `stageField: false`, and a status field with no stages. An authored highlights slot with a path hides the stage, and one without a path does not. `buildDefaultTabs` on its own follows the same rule. - **New: `__tests__/RecordMetaFooter.ownerOnce-28868.test.tsx`, 8 cases, rules 3 (owner) and 4 (one word).** A declared strip naming `owner_id` drops the owner from the meta line. With no `owner_id` in the strip, or no strip at all, the owner stays. For en / zh-CN / ja-JP / es-ES, `translateObject`'s `owner_id` label equals the matching ui pack's `detail.owner`. 负责人 and Owner are pinned by name. - **Hover card and phone header.** `RecordPreviewCard.test.tsx` gains "an object that declares no highlightFields shows the title alone". It asserts no guessed field and a bare `findOne` with no `$expand`. In `recordPagePush-27058.test.tsx`, the "no declared highlights" case now asserts `{ subtitle: '', highlightFields: [] }` instead of "derived, at most four". - **Tests that pinned today's guess, changed:** - `__tests__/buildDefaultPageSchema.retiredFieldType.test.ts` is deleted. It pinned the retired-type gate on the two selection passes, and both passes are gone. A retired type is still refused loudly by the form, inline edit and the body (`InlineFieldInput.retiredFieldType.test.tsx` and the field widgets). - `deriveHighlightFields.groupGate-12720.test.ts` is rewritten. An undeclared object has no strip at all, a declared gated entry is left as written, and the page body hides nothing and draws the gated group. - `buildDefaultPageSchema.test.ts`: the four heuristic cases become one "nothing is guessed" case, and the title-skip case now uses a declared list. The retired `compactLayout` case now expects `[]`. Three page and sub-builder cases use a `declaredLeadDef`, and the unused `getRecordDisplayName` import is dropped. - In `buildDefaultPageSchema.strictPayload.test.ts` and `record-highlights.groupGate-12720.test.tsx`, the fixtures declare the strip they render. - In `resolveTitleField.sharedLadder-7287.test.ts`, three strip cases declare their fields, so the title skip still decides. - **The label pins move from 所有者 to 负责人**: spec `i18n-resolver.test.ts` (the custom-label case now uses 客户经理, so it still proves an author's label wins), rest `meta-object-injected-column-i18n.test.ts`, and the app-shell `DraftChangesPanel.generatedGroup-25890.test.tsx` fixture that mirrors the read. ## Acceptance notes - **app-crm loses its strips until #28870.** `examples/app-crm` declares no `highlightFields`. #28870 declares them (same seat, in parallel); app-crm is not edited here. - **Phone width, owner in the 5th or 6th declared slot.** The meta line asks the declared strip, which holds at most six fields. On a phone, the header shows at most four after its subtitle. So an object declaring `owner_id` fifth or sixth, behind four non-text highlights, names the owner in neither place on a phone. Closing that needs `RecordDetailView` to hand the footer the strip it drew. That file is outside this card's surface, and `domain:ui#1` holds it for #28483. carrier: seat. - **An authored `highlights` slot** still leaves the body hiding the declared list, as before. Only the stage half now follows the slot. - **Release-owned text** (`packages/*/CHANGELOG.md`) still describes the old heuristic in released entries. It is not edited. ## Verification All readings are on this branch. The final head is `f8e75710`, a merge of `main` (11 commits, none touching this diff's files except an unrelated `cancelText` hunk in `i18n-resolver.ts`), unless a block names another head. <details><summary>plugin-detail · 362 of 362 files</summary> Run at `d57d0f80` + fixture commits (`208ff9bd`, `68776a00`) under `os-verify-lock`, `--maxWorkers=2`, in four batches: - `src/synth` + `src/renderers`: 126 files. First run 3 files / 7 tests red, all fixtures that relied on the guess to draw a strip: `strictPayload` coverage, three `buildDefaultPageSchema.test.ts` page cases, and the three `record-highlights.groupGate-12720` legs. After the fixtures declare their strip: 17/17 re-run, 204 tests. - `src/__tests__` first half: 113 passed, 835 tests. - `src/__tests__` second half: 112 passed, 1 skipped, 982 tests. - The package root files plus the new footer test: 11 passed, 83 tests. </details> <details><summary>app-shell and console · every test naming the record page, the strip, the hover card, the phone header or the footer · 265 files</summary> `git grep -l` over `packages/app-shell/src/**/*.test.ts*` and `apps/console/src/**/*.test.ts*` for the touched modules, `record:highlights`, `highlightFields`, `RecordMetaFooter` and 所有者 gave 265 files. Two batches at `--maxWorkers=3`: 138/138 files (1380 tests), then 127 files with 1 red. The red was the new hover-card pin: an undeclared object sent `$expand: ['account']`. Fixed in `d4833e5a`, and `record-preview/` re-ran 3/3 files, 43 tests. This is a declared narrowing; CI's Test UI runs the rest of app-shell. At the merged head: plugin-detail `synth/`, the new footer test, `recordPageV3Face-27055`, `record-highlights.groupGate-12720` and the 20 app-shell files naming `phoneRecordHeader` / `RecordPreviewCard` / `RecordMetaFooter` / `highlightFields` / the phone record page: 39/39 files, 421 tests. </details> <details><summary>spec and rest label pins</summary> `pnpm --filter @objectstack/spec exec vitest run src/system/i18n-resolver.test.ts src/system/i18n-resolver.object-validations.test.ts`: 2/2 files, 386 tests. `pnpm --filter @objectstack/rest exec vitest run src/meta-object-injected-column-i18n.test.ts`: 1/1, 4 tests. `pnpm --filter @objectstack/spec check:generated` at the merged head: exit 0, every generated artifact up to date (a label value moves none). </details> <details><summary>Ablations · 7 legs, all red as predicted</summary> Each leg went through `scripts/ablation-replace.mjs` against the committed fix. Each restore read blob == HEAD with `git diff HEAD` empty. 1. The guess put back (undeclared → the first fields): 8 of 11 cases in `highlights-declaredOnly-28868` red. 2. The path's stage not pushed into the hide list: 3 red (path page, authored slot, `buildDefaultTabs`). 3. The hide list not cut at six: 1 red (the past-sixth case). 4. The footer ignoring the strip: 1 red (strip naming `owner_id`). 5. The hover card expanding with no key field: 1 red (the new card case). 6. The phone header guessing again: 1 red (the no-declared case). 7. The spec label back to 所有者: 4 red in spec `i18n-resolver.test.ts`. The ui cross-package pin (`detail.owner` equals the spec label) reads `@objectstack/spec`'s dist, so leg 7 was run on the spec's own suite, which reads source. </details> <details><summary>Type check</summary> `pnpm --filter @object-ui/plugin-detail run type-check && pnpm --filter @object-ui/app-shell run type-check` (each `tsc --noEmit` plus `tsc -p tsconfig.test.json`): exit 0, both before and after the merge. Each run followed `turbo run build --filter='@object-ui/app-shell^...'` (64 tasks). </details> <details><summary>Derived gates · 83 commands</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 83 commands at `d4833e5a`: - 81 exited 0. - `check-plugin-teardown-shape.mjs --self-test` exited 3 because a shallow clone could not read its pinned fixture. After `git fetch --depth=1 origin 621a487…` it exited 0 (48 cases). - `pnpm check:dual-build-cjs-loads` exited 3, "Run `pnpm build` first … nothing was measured", because 30 packages outside this diff's closure have no `dist`. NOT MEASURED locally; CI's Lint & Repo Gates runs it. - `pnpm --dir ui exec vitest run scripts/` ran after a spec rebuild: 169 files passed, 2 skipped; 4900 tests. Re-run at the merged head, each exit 0: `check-sdui-manifest.mjs`, `check:nul-bytes`, `check:cross-package-test-inputs`, `check:test-source-alias`, `check-empty-changeset.mjs --base origin/main`, `check:issue-citations` and `check:doc-authoring`. `pnpm --dir ui run check:new-line-citations` found 0 new citations. </details> <details><summary>PR-body gates</summary> Run with this body as `GITHUB_EVENT_PATH` (or `PR_BODY`), before the create, each exit 0: - `check-changeset-no-major.mjs --base origin/main`, which reads `Metadata-protocol: none` and a `patch` changeset; - `check-adr-0087-registration.mjs --base origin/main`, which found 1 non-breaking changeset; - `check-closing-keyword-parity.mjs`; - `check-partof-closing-keyword.mjs`. </details> Diff: 19 files, about 1,000 changed lines, under the 3,000-line line. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jU8qzB9Vbw2NdbFxiG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a364093 commit 8bb2417

19 files changed

Lines changed: 493 additions & 502 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/app-shell': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
fix(plugin-detail): the record page's highlight strip is the object's declared `highlightFields` only; the path's stage and the owner each show once, under one word for the owner (#28868)
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
**No guessed strip.** An object that declares no `highlightFields` now has no highlight strip, so every field stays in the details body. This holds on the default record page, the phone record header and the record hover card. `deriveHighlightFields` (`@object-ui/plugin-detail`) used to pick up to six fields from a preferred-name list (`owner` / `owner_id` first, then amount, rating, industry, …) and hide them from the body. That guess is deleted. The function keeps its signature and returns the declared list minus the record's title field, at most six, or nothing. A declared strip is unchanged.
13+
14+
**The stage once.** When the page draws `record:path`, its `statusField` joins the body's `hideFields`, so the stage is not drawn again as a body field. The body's hide list is also cut at the six fields the strip shows: a declared entry past the sixth is now drawn by the body instead of by neither.
15+
16+
**The owner once, under one word.** When the strip carries `owner_id`, the meta line under the details leaves the owner out. The platform label of the injected `owner_id` column (`@objectstack/spec`'s system-field fallback) now reads 「负责人」 in zh-CN, the word the ui's `detail.owner` already used. It was 「所有者」. en (`Owner`), ja-JP (`所有者`) and es-ES (`Propietario`) already matched the ui packs. A label an author wrote is still never overridden.
17+
18+
**What an app sees.** An object without declared `highlightFields` loses its strip. Declare the fields the header should show: `highlightFields: ['amount', 'close_date', 'owner_id']`.

‎packages/rest/src/meta-object-injected-column-i18n.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const ZH_CN = {
144144
created_by: '创建人',
145145
updated_at: '更新时间',
146146
updated_by: '更新人',
147-
owner_id: '所有者',
147+
owner_id: '负责人',
148148
owning_business_unit_id: '所属业务单元',
149149
queue: '队列',
150150
};

‎packages/spec/src/system/i18n-resolver.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,7 +3242,7 @@ describe('translateObject system-field label fallback', () => {
32423242
it('localizes injected system-field labels even without a bundle', () => {
32433243
const out = translateObject(contract, undefined, { locale: 'zh-CN' });
32443244
const fields = out.fields as Record<string, any>;
3245-
expect(fields.owner_id.label).toBe('所有者');
3245+
expect(fields.owner_id.label).toBe('负责人');
32463246
expect(fields.created_at.label).toBe('创建时间');
32473247
expect(fields.created_by.label).toBe('创建人');
32483248
expect(fields.updated_at.label).toBe('更新时间');
@@ -3255,7 +3255,7 @@ describe('translateObject system-field label fallback', () => {
32553255

32563256
it('applies BCP-47 fallback for base-language and variant locales', () => {
32573257
const zh = translateObject(contract, undefined, { locale: 'zh' });
3258-
expect((zh.fields as any).owner_id.label).toBe('所有者');
3258+
expect((zh.fields as any).owner_id.label).toBe('负责人');
32593259
const ja = translateObject(contract, undefined, { locale: 'ja' });
32603260
expect((ja.fields as any).created_by.label).toBe('作成者');
32613261
});
@@ -3270,10 +3270,10 @@ describe('translateObject system-field label fallback', () => {
32703270
it('never overrides an author-customized system-field label', () => {
32713271
const custom = {
32723272
name: 'contracts',
3273-
fields: { owner_id: { name: 'owner_id', type: 'lookup', label: '负责人' } },
3273+
fields: { owner_id: { name: 'owner_id', type: 'lookup', label: '客户经理' } },
32743274
};
32753275
const out = translateObject(custom, undefined, { locale: 'zh-CN' });
3276-
expect((out.fields as any).owner_id.label).toBe('负责人');
3276+
expect((out.fields as any).owner_id.label).toBe('客户经理');
32773277
});
32783278

32793279
it('prefers an explicit bundle entry over the built-in fallback', () => {
@@ -3294,7 +3294,7 @@ describe('translateObject system-field label fallback', () => {
32943294
fields: [{ name: 'owner_id', type: 'lookup', label: 'Owner' }],
32953295
};
32963296
const out = translateObject(arrayDoc, undefined, { locale: 'zh-CN' });
3297-
expect((out.fields as any)[0].label).toBe('所有者');
3297+
expect((out.fields as any)[0].label).toBe('负责人');
32983298
});
32993299
});
33003300

@@ -3359,7 +3359,7 @@ describe('translateObject localises every platform-injected column', () => {
33593359
created_by: '创建人',
33603360
updated_at: '更新时间',
33613361
updated_by: '更新人',
3362-
owner_id: '所有者',
3362+
owner_id: '负责人',
33633363
owning_business_unit_id: '所属业务单元',
33643364
queue: '队列',
33653365
});

‎packages/spec/src/system/i18n-resolver.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4013,7 +4013,7 @@ function lookupObjectFieldOption(
40134013
*/
40144014
const SYSTEM_FIELD_LABELS: Record<string, Record<string, string>> = {
40154015
organization_id: { en: 'Organization', 'zh-CN': '组织', 'ja-JP': '組織', 'es-ES': 'Organización' },
4016-
owner_id: { en: 'Owner', 'zh-CN': '所有者', 'ja-JP': '所有者', 'es-ES': 'Propietario' },
4016+
owner_id: { en: 'Owner', 'zh-CN': '负责人', 'ja-JP': '所有者', 'es-ES': 'Propietario' },
40174017
created_at: { en: 'Created At', 'zh-CN': '创建时间', 'ja-JP': '作成日時', 'es-ES': 'Creado el' },
40184018
created_by: { en: 'Created By', 'zh-CN': '创建人', 'ja-JP': '作成者', 'es-ES': 'Creado por' },
40194019
updated_at: { en: 'Last Modified At', 'zh-CN': '更新时间', 'ja-JP': '更新日時', 'es-ES': 'Actualizado el' },

‎ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.generatedGroup-25890.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { DraftChangesPanel, computeChangeDetail } from '../DraftChangesPanel';
5151
const ZH_LABELS: Record<string, string> = {
5252
created_at: '创建时间',
5353
created_by: '创建人',
54-
owner_id: '所有者',
54+
owner_id: '负责人',
5555
updated_at: '更新时间',
5656
updated_by: '更新人',
5757
};
@@ -109,7 +109,7 @@ describe('computeChangeDetail — what the platform wrote is not an author row (
109109
{ name: 'nameField', kind: 'added', scope: 'key' },
110110
]);
111111
// A generated field keeps its label, for the row the group shows when opened.
112-
expect(d.fields?.labels.get('owner_id')).toBe('所有者');
112+
expect(d.fields?.labels.get('owner_id')).toBe('负责人');
113113
});
114114

115115
it('CONTROL: a field shaped like an audit column without the system mark is the author’s', () => {

‎ui/packages/app-shell/src/views/__tests__/recordPagePush-27058.test.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,10 @@ describe('the phone header of a default record page (objectstack#27058, Target 4
235235
});
236236
});
237237

238-
it('no declared highlights: no subtitle, and the strip is derived as on any width', () => {
238+
it('no declared highlights: no subtitle and no strip, as on any width (objectstack#28868)', () => {
239+
// Nothing is guessed: the object above has seven highlight-friendly
240+
// fields, and none of them is picked.
239241
const header = phoneRecordHeader(def, undefined, { company: 'Atlas' });
240-
expect(header.subtitle).toBe('');
241-
expect(header.highlightFields.length).toBeLessThanOrEqual(PHONE_HIGHLIGHTS_MAX);
242+
expect(header).toEqual({ subtitle: '', highlightFields: [] });
242243
});
243244
});

‎ui/packages/app-shell/src/views/record-preview/RecordPreviewCard.test.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,21 @@ describe('RecordPreviewCard — loading and readable (objectui#12029)', () => {
144144
expect(ds.findOne).toHaveBeenCalledWith(OBJECT, 'INV-1');
145145
});
146146

147+
it('an object that declares no highlightFields shows the title alone: no key field is guessed (objectstack#28868)', async () => {
148+
const { highlightFields: _declared, ...undeclared } = DEF;
149+
const ds = source(() => ({ ...RECORD }));
150+
const { container } = mount(
151+
<RecordPreviewCard objectName={OBJECT} recordId="INV-1" dataSource={ds} />,
152+
{ [OBJECT]: undeclared },
153+
);
154+
await settle(container, 'readable');
155+
expect(screen.getByTitle('Laptop refresh')).toHaveTextContent('Laptop refresh');
156+
// `amount` and `note` are highlight-friendly fields the old guess picked.
157+
expect(screen.queryByText('Amount')).toBeNull();
158+
expect(screen.queryByText('Quarterly batch')).toBeNull();
159+
expect(ds.findOne).toHaveBeenCalledWith(OBJECT, 'INV-1');
160+
});
161+
147162
it('expands a reference field only when the card shows it', async () => {
148163
const withAccount = { ...DEF, highlightFields: ['account', 'note'] };
149164
const ds = source(() => ({ ...RECORD, account: { id: 'A1', name: 'Northwind' } }));

‎ui/packages/app-shell/src/views/record-preview/RecordPreviewCard.tsx‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@
2929
* ladder (`nameField`, then its deprecated alias, then `titleFormat`, then
3030
* type-aware derivation, then `Record #<id>`). The record page's H1 and
3131
* breadcrumb read the same function.
32-
* - **Key fields** — `deriveHighlightFields(def, detectStatusField(def))` from
33-
* `@object-ui/plugin-detail`, the exact call the synthesized record page uses
34-
* for its highlights strip (`buildDefaultHighlights`): the object's declared
35-
* `highlightFields` (ADR-0085) first, else the same heuristic.
32+
* - **Key fields** — `deriveHighlightFields` from `@object-ui/plugin-detail`,
33+
* the call the synthesized record page uses for its highlights strip
34+
* (`buildDefaultHighlights`): the object's declared `highlightFields`
35+
* (ADR-0085) minus the title field. An object that declares none shows the
36+
* title alone: nothing is guessed (objectstack#28868).
3637
* - **Field values** — `HeaderHighlight`, the strip the record page draws those
3738
* fields with, so a value reads here exactly as it reads on the record page.
3839
* - **Field-level security** — the served row passes through
@@ -106,7 +107,7 @@
106107
import { useEffect, useMemo, useState } from 'react';
107108
import { Card, CardContent, CardHeader, CardTitle, EmptyValue, cn } from '@object-ui/components';
108109
import { buildExpandFields, getRecordDisplayName, withoutDeniedFields } from '@object-ui/core';
109-
import { deriveHighlightFields, detectStatusField, HeaderHighlight } from '@object-ui/plugin-detail';
110+
import { deriveHighlightFields, HeaderHighlight } from '@object-ui/plugin-detail';
110111
import { usePermissions } from '@object-ui/permissions';
111112
import {
112113
InlineEditProvider,
@@ -169,12 +170,15 @@ export function RecordPreviewCard({ objectName, recordId, dataSource, className
169170

170171
// Memoised for cost only; nothing below depends on the identity returned.
171172
const highlightNames = useMemo<string[]>(
172-
() => (def ? deriveHighlightFields(def, detectStatusField(def)) : []),
173+
() => (def ? deriveHighlightFields(def, null) : []),
173174
[def],
174175
);
175176
// What the read SENDS, held as a string so the effect keys on the data it
176177
// sends rather than on the objects that data is derived from.
177-
const expandable = def ? buildExpandFields(def.fields, highlightNames) : [];
178+
// No key field, nothing to expand: `buildExpandFields` reads an empty column
179+
// list as "every reference field", which a card that shows only the title
180+
// never draws (an object with no declared highlights, objectstack#28868).
181+
const expandable = def && highlightNames.length > 0 ? buildExpandFields(def.fields, highlightNames) : [];
178182
const expandKey = JSON.stringify(
179183
targetObject === null || !perms?.isLoaded
180184
? expandable

‎ui/packages/app-shell/src/views/recordPhoneHeader.ts‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
* highlights, at most four, in the strip's two columns. The rest of the
1414
* fields are in the sections anyway. It narrows the default page's header by
1515
* width, the record page v4 header's highlight rule (#27524 item 12); an
16-
* authored page draws its own header.
16+
* authored page draws its own header. An object that declares no highlights
17+
* has no strip and no subtitle here either, as on any width: nothing is
18+
* guessed (objectstack#28868).
1719
*/
1820

19-
import { deriveHighlightFields, detectStatusField } from '@object-ui/plugin-detail';
20-
2121
/** The highlights a phone's header strip shows after its subtitle. */
2222
export const PHONE_HIGHLIGHTS_MAX = 4;
2323

@@ -46,8 +46,8 @@ export interface PhoneRecordHeader {
4646
* The phone header of a default record page.
4747
*
4848
* `declared` is the object's declared `highlightFields`, the record's title
49-
* field already dropped (`undefined` when it declares none: the strip is then
50-
* derived, as on any width, and there is no subtitle). The subtitle reads the
49+
* field already dropped (`undefined` when it declares none: there is then no
50+
* strip and no subtitle, as on any width). The subtitle reads the
5151
* values the reader may see (`record` is the row the page reads), so a field
5252
* the server masked adds nothing; the separator joins only the values that
5353
* are there.
@@ -65,8 +65,7 @@ export function phoneRecordHeader(
6565
.filter((value): value is string => typeof value === 'string' && value.trim() !== '')
6666
.map((value) => value.trim())
6767
.join(' · ');
68-
const objectDef = def as Parameters<typeof deriveHighlightFields>[0];
69-
const strip = declared ?? deriveHighlightFields(objectDef, detectStatusField(objectDef));
68+
const strip = declared ?? [];
7069
return {
7170
subtitle,
7271
highlightFields: strip.filter((name) => !subtitleFields.includes(name)).slice(0, PHONE_HIGHLIGHTS_MAX),

‎ui/packages/plugin-detail/src/RecordMetaFooter.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useDisplayLocale } from '@object-ui/i18n';
1616
import { useDetailTranslation } from './useDetailTranslation';
1717
import { hasCellValue } from './emptiness';
1818
import { useDefaultRecordPage } from './defaultRecordPage';
19+
import { deriveHighlightFields } from './synth/buildDefaultPageSchema';
1920

2021
/**
2122
* The record's reassignable owner column — the canonical injected
@@ -246,7 +247,12 @@ export const RecordMetaFooter: React.FC<RecordMetaFooterProps> = ({
246247
const hasUpdated =
247248
!!(updatedAt || updatedBy) && !(updateIsSameAsCreate && (sameUser || !updatedBy));
248249

249-
const owner = v3MetaLine ? actorOrNone(data[OWNER_FIELD]) : undefined;
250+
// The owner is named ONCE on the page (objectstack#28868): when the
251+
// highlight strip carries the owner field, the strip names it and this line
252+
// leaves it out. The strip is the object's declared highlights, read by the
253+
// one function every reader of the strip asks.
254+
const stripNamesOwner = deriveHighlightFields(objectSchema, null).includes(OWNER_FIELD);
255+
const owner = v3MetaLine && !stripNamesOwner ? actorOrNone(data[OWNER_FIELD]) : undefined;
250256
const recordId = v3MetaLine && hasCellValue(data.id) ? String(data.id) : undefined;
251257

252258
if (!hasCreated && !hasUpdated && !owner && !recordId) return null;

0 commit comments

Comments
 (0)