Skip to content

Commit 0be247c

Browse files
fix(plugin-detail): the record meta line leaves the owner out only when the strip the page drew carries it (#28994)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbUa7sLpBRCGenWhqEBnN1ho)_ Fixes #28909 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - **改了什么**:记录页正文下方的元信息行,现在只在「页面实际画出的要点条」里有 `owner_id` 时才省略负责人。`RecordMetaFooter` 新增可选属性 `drawnHighlightFields`,由画要点条的宿主(`RecordDetailView`)把它实际画出的字段交给页脚;不传时仍按对象声明的 `highlightFields` 判断,行为不变。 - **为什么改**:#28868 让负责人只出现一次,但页脚问的是声明列表(最多 6 个),而手机记录头在副标题后最多只画 4 个。`owner_id` 声明在第 5、6 位且前面是四个非文本字段时,手机页上要点条和元信息行都不显示负责人,用户看不到记录归谁。 - **风险与回滚**:低。只影响默认记录页和带插槽的记录页上「负责人写不写在元信息行」这一处;桌面端结果不变。无新元数据键、无新门禁。回滚即还原本 PR。 - **先看**:`ui/packages/app-shell/src/views/RecordDetailView.tsx`(`drawnHighlightFieldsOf` 与两个 `RecordMetaFooter` 调用点)、`ui/packages/plugin-detail/src/RecordMetaFooter.tsx`(`stripFields` 一行)、`RecordDetailView.phoneOwnerLine-28909.test.tsx`。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/app-shell/src/views/RecordDetailView.phoneOwnerLine-28909.test.tsx packages/plugin-detail/src/__tests__/RecordMetaFooter.drawnStrip-28909.test.tsx`,预期 2 个文件、6 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes - **`RecordMetaFooter` gains one optional prop, `drawnHighlightFields?: readonly string[]`**: the fields the page's highlight strip actually draws, handed over by the host that draws it. The meta line leaves the owner out only when this list carries `owner_id`. When the prop is omitted, the footer asks `deriveHighlightFields(objectSchema, null)` as before, so other hosts (the standalone `DetailView`, the Studio page preview) are unchanged. - **`RecordDetailView` hands the footer the strip it rendered, at both call sites** (the laid-out page's details foot and the page-foot append). A module-level reader, `drawnHighlightFieldsOf(page)`, collects the `fields` of the `record:highlights` blocks among `renderedPage`'s top-level blocks, in either form the block accepts (a bare name or a `{ name }` entry). That is where the synthesizer places the strip, so the list is: - on a phone, the header's strip after the subtitle (at most `PHONE_HIGHLIGHTS_MAX`, four); - on a desktop, the declared strip (at most six); - on a slotted page, the page's own `highlights` slot. - **The plugin-detail README** ("The provenance footer belongs to the record page") now tells a host that draws a strip to pass `drawnHighlightFields`. - **File surface:** the claim, amended in 6569734511 to add the drawn-strip reader with its one const in `RecordDetailView.tsx` and `ui/packages/plugin-detail/README.md`. - `recordPhoneHeader.ts` is read, not edited. `DetailView.tsx` is not edited: its footer runs outside `DefaultRecordPageContext`, so it never names the owner, and on a record page the page draws the footer instead. ## Tests - **New: `ui/packages/app-shell/src/views/RecordDetailView.phoneOwnerLine-28909.test.tsx`**, a real `RecordDetailView` mount on the `phoneFace-27058` harness. The lead declares `highlightFields: ['name', 'email', 'status', 'phone', 'website', 'owner_id']`: the title, four non-text highlights, then the owner fifth. - At 390 px the strip has 4 entries without the owner, and the meta line names 「Ada Admin」. - At 1440 px the strip has 5 entries with the owner, and the meta line has no owner entry (its id entry is still drawn). - **New: `ui/packages/plugin-detail/src/__tests__/RecordMetaFooter.drawnStrip-28909.test.tsx`**, 4 cases on the prop: drawn strip without the owner (declared with it) keeps the owner; drawn strip with the owner (declared without it) drops it; an empty drawn strip keeps it; omitted, the declared strip decides. - `RecordMetaFooter.ownerOnce-28868.test.tsx` and `RecordDetailView.phoneFace-27058.test.tsx` re-run green unchanged. ## Acceptance notes - **The strip is read from top-level blocks only.** A `record:highlights` block nested inside a container in an authored `highlights` slot is not collected. The meta line then keeps the owner, which can name it twice but never zero times. The synthesizer itself always places the strip at the top level. - **Field-level security is not subtracted.** The `record:highlights` renderer also drops fields the reader may not see (`enforceFieldSecurity`, `redactFields`). When the owner is withheld from the strip that way, it is withheld from the record row too, so the meta line has no owner value to show either. - The shared `HighlightFieldsContext` registry (what the highlights renderer registers for the details body) was not used: the claim asks for an explicit prop from the host, and the registry fills in an effect after the first paint. ## Verification All readings are on this branch at head `be4cc7d0` (base `ff881050`). `origin/main` has moved 10 commits since; none touches `plugin-detail` or `app-shell/src/views`, so `main` is not merged in. Commands ran under `os-verify-lock`. <details><summary>New and neighbouring tests · 4 files, 24 tests</summary> `pnpm --dir ui exec vitest run --maxWorkers=2` on `RecordMetaFooter.drawnStrip-28909`, `RecordMetaFooter.ownerOnce-28868`, `RecordDetailView.phoneOwnerLine-28909` and `RecordDetailView.phoneFace-27058`: Test Files 4 passed (4), Tests 24 passed (24). </details> <details><summary>Ablation · two legs, both red, both restored</summary> Both legs ran through `scripts/ablation-replace.mjs` (anchor hit 1 to 0, blob moved, restore proved by blob equal to HEAD and an empty `git diff HEAD`). `@object-ui/plugin-detail` resolves to its `src` under ui's vitest alias, so no `dist` rebuild was needed. - Host leg: `drawnHighlightFieldsOf(renderedPage)` replaced by `undefined` in `RecordDetailView`. The phone case went red ("on a phone: owner_id declared 5th …"), 1 failed and 5 passed. The desktop case stays green, as it should: there the strip carries the owner either way. - Footer leg: the `drawnHighlightFields ??` read removed in `RecordMetaFooter`. 4 failed and 2 passed: the three prop cases and the phone page case. </details> <details><summary>Footer sweep · 13 files, 109 tests</summary> Every ui test file that names `RecordMetaFooter`, `record-meta-footer`, `data-record-meta`, `drawnHighlightFields` or the plugin-detail README (`git grep -l` over `packages/**` and `apps/console/**` tests): Test Files 13 passed (13), Tests 109 passed (109). This narrows the run; CI's Test UI runs the rest of app-shell. </details> <details><summary>Type-check and ui scripts</summary> - `pnpm exec turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/app-shell --concurrency=2`: 66 tasks successful (both packages run `tsc --noEmit` and `tsc -p tsconfig.test.json`). - `pnpm --dir ui exec vitest run scripts/`: Test Files 169 passed, 2 skipped (171); Tests 4900 passed, 2 skipped. - `pnpm check:new-line-citations` (ui): 0 new citations. - `node ui/scripts/check-readme-exports.mjs`: NOT MEASURED. It exits 1 on a prerequisite (7 packages unbuilt in this worktree), not on this README. The README edit adds no fenced code block. </details> <details><summary>Derived gates · 34 of 34 exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands` derived 34 families from the changed paths, and re-derived the same 34 after the README commit. All 34 ran with a draft of this body as `GITHUB_EVENT_PATH` and exited 0; the four body-reading gates (`check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity`, `check-partof-closing-keyword`) re-ran on this final body and exited 0. The families run include `check-changeset-no-major.mjs --base origin/main`, `check-adr-0087-registration.mjs --base origin/main`, `check-closing-keyword-parity.mjs`, `check-empty-changeset.mjs --base origin/main`, `check-sdui-manifest.mjs` and `vitest run scripts/` above. `dispatch-gates --ran`: 34 derived families accounted for, 34 run, 0 NOT MEASURED. </details> Full dogfood battery: not owed (no `!` in the title, no `(narrowing)` arm). Diff: 6 files, 359 insertions and 5 deletions. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTfAbHWaonqGhm5rxk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab085a0 commit 0be247c

6 files changed

Lines changed: 359 additions & 5 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(plugin-detail): the record's meta line leaves the owner out only when the highlight strip the page actually drew carries `owner_id`, so a phone page names the owner somewhere (#28909)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
**The owner on a phone.** The meta line under the details leaves the owner out when the strip already shows it. It used to ask the object's declared `highlightFields` (up to six), while a phone's record header draws at most four highlights after its subtitle. With `owner_id` declared fifth or sixth behind four non-text highlights, a phone page named the owner neither in the header nor in the meta line. The meta line now asks the strip the page drew, on every width: on a phone that owner is back on the meta line; on a desktop, where the strip draws it, the meta line still leaves it out.
12+
13+
**`RecordMetaFooter` gains `drawnHighlightFields`** (optional, `readonly string[]`): the fields the host's highlight strip draws. The meta line leaves the owner out only when this list carries `owner_id`. Omitted, the strip is taken to be the object's declared highlights, as before. `RecordDetailView` hands it the `record:highlights` fields of the page it renders, which covers the phone header's shorter strip and a slotted page's own `highlights` slot.
14+
15+
No metadata key is added or changed.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#28909 — the owner is named somewhere on the page at every width,
11+
* and once. From a real `RecordDetailView` mount (the `phoneFace-27058`
12+
* harness), with `owner_id` declared 5th in `highlightFields`, behind four
13+
* non-text highlights (so the phone subtitle takes none of them):
14+
*
15+
* - at 390 px the phone header draws four highlights after its subtitle, so
16+
* the owner is not in the strip and the meta line names it;
17+
* - at 1440 px the strip draws all five, the owner among them, and the meta
18+
* line leaves it out (objectstack#28868, the owner once).
19+
*/
20+
21+
import * as React from 'react';
22+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
23+
import { render, cleanup, waitFor } from '@testing-library/react';
24+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
25+
import { MetadataCtx } from '@object-ui/react';
26+
import { RelatedCountStore } from '@object-ui/components';
27+
28+
vi.mock('@object-ui/auth', async (importOriginal) => ({
29+
...(await importOriginal<Record<string, unknown>>()),
30+
useAuth: () => ({ user: { id: 'u1', name: 'Ada', image: null }, activeOrganization: null }),
31+
createAuthenticatedFetch: () => vi.fn(),
32+
}));
33+
34+
vi.mock('@object-ui/collaboration', async (importOriginal) => ({
35+
...(await importOriginal<Record<string, unknown>>()),
36+
useRecordPresence: () => [],
37+
PresenceAvatars: () => null,
38+
}));
39+
40+
vi.mock('sonner', () => ({
41+
toast: Object.assign(vi.fn(), {
42+
success: vi.fn(),
43+
error: vi.fn(),
44+
info: vi.fn(),
45+
warning: vi.fn(),
46+
loading: vi.fn(),
47+
dismiss: vi.fn(),
48+
}),
49+
}));
50+
51+
vi.mock('./ActionConfirmDialog', () => ({ ActionConfirmDialog: () => null }));
52+
vi.mock('./ActionParamDialog', () => ({ ActionParamDialog: () => null }));
53+
vi.mock('./ActionResultDialog', () => ({ ActionResultDialog: () => null }));
54+
vi.mock('./FlowRunner', () => ({ FlowRunner: () => null }));
55+
vi.mock('./MetadataInspector', () => ({
56+
MetadataPanel: () => null,
57+
useMetadataInspector: () => ({ showDebug: false, toggle: () => {} }),
58+
}));
59+
60+
import { RecordDetailView } from './RecordDetailView';
61+
62+
const APP = 'demo';
63+
const LEAD = 'crm_lead';
64+
65+
const OBJECTS = [
66+
{
67+
name: LEAD,
68+
label: 'Lead',
69+
pluralLabel: 'Leads',
70+
fields: {
71+
id: { type: 'text', label: 'Id' },
72+
name: { type: 'text', label: 'Name' },
73+
email: { type: 'email', label: 'Email' },
74+
status: { type: 'select', label: 'Status', options: [{ value: 'new', label: 'New' }] },
75+
phone: { type: 'phone', label: 'Phone' },
76+
website: { type: 'url', label: 'Website' },
77+
owner_id: { type: 'lookup', reference: 'sys_user', label: 'Owner' },
78+
},
79+
// The title field first (it is the page's name, never a highlight), then
80+
// four non-text highlights, then the owner, 5th.
81+
highlightFields: ['name', 'email', 'status', 'phone', 'website', 'owner_id'],
82+
},
83+
];
84+
85+
const LEAD_ROW = {
86+
name: 'Mira Costa',
87+
email: 'mira@atlas.test',
88+
status: 'new',
89+
phone: '555-0100',
90+
website: 'https://atlas.test',
91+
owner_id: { id: 'u-ada', name: 'Ada Admin' },
92+
created_at: '2026-10-16T00:00:00Z',
93+
};
94+
95+
let rootWidth = 390;
96+
97+
function setWidth(width: number) {
98+
rootWidth = width;
99+
Object.defineProperty(window, 'innerWidth', { value: width, configurable: true, writable: true });
100+
}
101+
102+
function makeDataSource() {
103+
return {
104+
find: vi.fn(async () => ({ data: [], total: 0 })),
105+
create: vi.fn(async (_o: string, row: unknown) => row),
106+
findOne: vi.fn(async (_objectName: string, recordId: string) => ({ id: recordId, ...LEAD_ROW })),
107+
update: vi.fn(async () => ({})),
108+
delete: vi.fn(async () => ({})),
109+
};
110+
}
111+
112+
function metadataFor(objects: unknown[]) {
113+
return {
114+
objects,
115+
pages: [],
116+
loading: false,
117+
error: null,
118+
refresh: async () => {},
119+
invalidate: () => {},
120+
ensureType: async () => [],
121+
getItem: async () => null,
122+
getItemsByType: () => [],
123+
} as never;
124+
}
125+
126+
function mountPage() {
127+
render(
128+
<MemoryRouter initialEntries={[`/apps/${APP}/${LEAD}/record/r-1`]}>
129+
<MetadataCtx.Provider value={metadataFor(OBJECTS)}>
130+
<Routes>
131+
<Route
132+
path="/apps/:appName/:objectName/record/:recordId"
133+
element={<RecordDetailView dataSource={makeDataSource() as never} objects={OBJECTS} onEdit={() => {}} />}
134+
/>
135+
</Routes>
136+
</MetadataCtx.Provider>
137+
</MemoryRouter>,
138+
);
139+
}
140+
141+
/** The strip and the meta line, once both are drawn. */
142+
async function stripAndMetaLine(): Promise<{ strip: HTMLElement; metaLine: HTMLElement }> {
143+
return waitFor(
144+
() => {
145+
const strip = document.querySelector('[data-highlights-face]') as HTMLElement | null;
146+
const metaLine = document.querySelector('[data-testid="record-meta-footer"]') as HTMLElement | null;
147+
expect(strip).toBeTruthy();
148+
expect(metaLine).toBeTruthy();
149+
// The record has loaded: the strip shows its values.
150+
expect(strip!.textContent).toContain('mira@atlas.test');
151+
return { strip: strip!, metaLine: metaLine! };
152+
},
153+
{ timeout: 5000 },
154+
);
155+
}
156+
157+
const ownerEntry = (metaLine: HTMLElement) => metaLine.querySelector('[data-record-meta-owner]');
158+
159+
beforeEach(() => {
160+
cleanup();
161+
setWidth(390);
162+
window.localStorage.clear();
163+
RelatedCountStore._reset();
164+
vi.stubGlobal(
165+
'fetch',
166+
vi.fn(async () => new Response(JSON.stringify({ data: [] }), { status: 200, headers: { 'content-type': 'application/json' } })),
167+
);
168+
const measure = HTMLElement.prototype.getBoundingClientRect;
169+
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
170+
if (!this.hasAttribute('data-record-page-root')) return measure.call(this);
171+
const width = rootWidth;
172+
return { x: 0, y: 0, top: 0, left: 0, right: width, bottom: 800, width, height: 800, toJSON: () => ({}) } as DOMRect;
173+
});
174+
});
175+
176+
afterEach(() => {
177+
vi.unstubAllGlobals();
178+
vi.restoreAllMocks();
179+
setWidth(1440);
180+
});
181+
182+
describe('the owner at every width, once (objectstack#28909)', () => {
183+
it('on a phone: owner_id declared 5th is past the header\'s four, so the meta line names the owner', async () => {
184+
mountPage();
185+
const { strip, metaLine } = await stripAndMetaLine();
186+
// The phone header: four highlights, the owner not among them.
187+
expect(strip.children).toHaveLength(4);
188+
expect(strip.textContent).not.toContain('Ada Admin');
189+
const owner = ownerEntry(metaLine);
190+
expect(owner).toBeTruthy();
191+
expect(owner!.textContent).toContain('Ada Admin');
192+
});
193+
194+
it('on a desktop: the strip names the owner and the meta line does not', async () => {
195+
setWidth(1440);
196+
mountPage();
197+
const { strip, metaLine } = await stripAndMetaLine();
198+
expect(strip.children).toHaveLength(5);
199+
expect(strip.textContent).toContain('Ada Admin');
200+
expect(ownerEntry(metaLine)).toBeNull();
201+
// The rest of the meta line is still drawn.
202+
expect(metaLine.querySelector('[data-record-meta-id]')!.textContent).toBe('r-1');
203+
});
204+
});

‎ui/packages/app-shell/src/views/RecordDetailView.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,31 @@ const PAGE_NODE_PROPERTY_KEYS = ['children', 'items'] as const;
300300
*/
301301
const TABS_URL_SYNC_KEYS: ReadonlySet<string> = new Set(['regions', 'components', 'children']);
302302

303+
/**
304+
* The fields the highlight strip of a synthesized record page draws: the
305+
* `fields` of the `record:highlights` blocks among the page's top-level blocks,
306+
* where the synthesizer places the strip (a phone's cut-short one included)
307+
* and where a slotted page's own `highlights` slot lands. A field is read in
308+
* either form the block accepts, a bare name or a `{ name }` entry, from the
309+
* block's `properties` bag over its own keys.
310+
*/
311+
function drawnHighlightFieldsOf(page: unknown): string[] {
312+
const out: string[] = [];
313+
const regions = (page as { regions?: unknown } | null | undefined)?.regions;
314+
for (const region of Array.isArray(regions) ? regions : []) {
315+
const components = (region as { components?: unknown } | null | undefined)?.components;
316+
for (const node of Array.isArray(components) ? components : []) {
317+
if ((node as { type?: unknown } | null | undefined)?.type !== 'record:highlights') continue;
318+
const props = { ...(node as object), ...((node as { properties?: object }).properties ?? {}) } as { fields?: unknown };
319+
for (const field of Array.isArray(props.fields) ? props.fields : []) {
320+
const name = typeof field === 'string' ? field : (field as { name?: unknown } | null | undefined)?.name;
321+
if (typeof name === 'string' && name.length > 0 && !out.includes(name)) out.push(name);
322+
}
323+
}
324+
}
325+
return out;
326+
}
327+
303328
/**
304329
* Whether a node of one of `types` is on screen when `page` opens with
305330
* `activeTab` as its `?tab=` value — that is, whether it is mounted.
@@ -4416,6 +4441,10 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
44164441
// data), so the early `effectivePage` synth never contains it and checking
44174442
// that tree would double-render the panel (tab + bottom fallback).
44184443
const hasApprovalsNode = hasExplicitApprovals(renderedPage as any);
4444+
// The strip this page draws (objectstack#28909), handed to the meta line so
4445+
// it leaves the owner out only when the strip shows it: on a phone the
4446+
// header draws fewer highlights than the object declares.
4447+
const drawnHighlightFields = drawnHighlightFieldsOf(renderedPage);
44194448

44204449
// objectstack#26678 / objectstack#27524 — the page as its tier lays it out:
44214450
// the record header, then the details and the right column (动态, then the
@@ -4905,6 +4934,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
49054934
data={readablePageRecord}
49064935
objectSchema={objectDef}
49074936
objectName={objectName}
4937+
drawnHighlightFields={drawnHighlightFields}
49084938
/>
49094939
}
49104940
/>
@@ -4979,6 +5009,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
49795009
data={readablePageRecord}
49805010
objectSchema={objectDef}
49815011
objectName={objectName}
5012+
drawnHighlightFields={drawnHighlightFields}
49825013
/>
49835014
</DefaultRecordPageContext.Provider>
49845015
)}

‎ui/packages/plugin-detail/README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ hidden draws nothing. The console's record page (`RecordDetailView` in
7171

7272
A host that mounts `RecordContextProvider` itself renders `RecordMetaFooter`
7373
once, where its page ends, with the record as `data` and the object definition
74-
as `objectSchema`. A `record:details` node with a `dataSource` binding (below)
74+
as `objectSchema`. A host that draws a highlight strip also passes
75+
`drawnHighlightFields`, the fields that strip actually draws: the meta line
76+
leaves the owner out only when that list carries `owner_id`, so a shorter strip
77+
(a phone's record header) never hides the owner from both places. Without it,
78+
the footer takes the object's declared `highlightFields` as the strip. A `record:details` node with a `dataSource` binding (below)
7579
shows a record the page does not, so it keeps its own footer.
7680

7781
### Record blocks without a record page (`dataSource`)

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export interface RecordMetaFooterProps {
3838
objectSchema?: any;
3939
/** Object name for future i18n hooks (currently unused). */
4040
objectName?: string;
41+
/**
42+
* The fields the page's highlight strip actually draws, handed over by the
43+
* host that draws it. The meta line leaves the owner out only when this
44+
* list carries `owner_id`, so a strip cut short (a phone's header) never
45+
* hides the owner from both places. Omitted, the strip is taken to be the
46+
* object's declared highlights, as `deriveHighlightFields` reads them.
47+
*/
48+
drawnHighlightFields?: readonly string[];
4149
className?: string;
4250
}
4351

@@ -220,6 +228,7 @@ export const RecordMetaFooter: React.FC<RecordMetaFooterProps> = ({
220228
data,
221229
objectSchema,
222230
objectName: _objectName,
231+
drawnHighlightFields,
223232
className,
224233
}) => {
225234
const { t } = useDetailTranslation();
@@ -248,10 +257,12 @@ export const RecordMetaFooter: React.FC<RecordMetaFooterProps> = ({
248257
!!(updatedAt || updatedBy) && !(updateIsSameAsCreate && (sameUser || !updatedBy));
249258

250259
// 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);
260+
// highlight strip the page draws carries the owner field, the strip names it
261+
// and this line leaves it out. The host that draws the strip says which
262+
// fields it drew; without that, the strip is the object's declared
263+
// highlights, read by the one function every reader of the strip asks.
264+
const stripFields = drawnHighlightFields ?? deriveHighlightFields(objectSchema, null);
265+
const stripNamesOwner = stripFields.includes(OWNER_FIELD);
255266
const owner = v3MetaLine && !stripNamesOwner ? actorOrNone(data[OWNER_FIELD]) : undefined;
256267
const recordId = v3MetaLine && hasCellValue(data.id) ? String(data.id) : undefined;
257268

0 commit comments

Comments
 (0)