Skip to content

Commit 2594f5b

Browse files
fix(plugin-detail): the record page's inline edit reads the stored row's field locks (#27226)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8YRPwyS92zNyQH7W2xooZeG)_ Fixes #27146 ## 维护者速读 - 改了什么:记录详情页的行内编辑(`record:details` 正文与 `record:highlights` 高亮条)改问服务器对这条记录的字段投影,不再只读不含行的 `/auth/me/permissions`。 - 为什么改:带 `when` 的字段锁(如审批后锁定摘要)在草稿行上也被禁用,而服务器其实允许写;编辑对话框已在 #27137 修好,详情页是剩下的一处。 - 风险与回滚:只在行内编辑会话可编辑、记录有 id、数据源有 `getRecordFieldAccess` 时才多发一次请求;两个区块同页时各问一次。回滚即还原三个 plugin-detail 文件。 - 先看:`ui/packages/plugin-detail/src/recordFieldAccess.tsx`、`renderers/record-details.tsx`、`renderers/record-highlights.tsx`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx`,预期 10 个测试全绿。 - 需要你做的:无,席位落地。 Clause-②: no Metadata-protocol: none ## What changes - New `ui/packages/plugin-detail/src/recordFieldAccess.tsx` (`RecordFieldAccessForRecord`, internal, not exported): mounts `RecordFieldAccessScope` (`@object-ui/permissions`) for the block's stored record when the page's inline-edit session may edit (`canEdit`), the block offers in-place edit, the record has an id and the data source carries `getRecordFieldAccess`. Below it, the unchanged `resolveFieldAffordance('recordEdit', …)` calls in `DetailSection` and `HeaderHighlight` read the server's projection for that row (ruling 6457467702, letter B: the server's answer, no client `when` evaluator). It asks again on a write the data source reports for the record and on the data-invalidation bus for it, the triggers the form's `RecordFieldAccessForForm` holds. - `record:details` wraps its `DetailView` in it (gated on its own `inlineEdit` verdict); `record:highlights` wraps its `HeaderHighlight`. - `content/docs/permissions/field-level-security.mdx`: the record-scoped projection paragraph now names the record page's in-place edit as a reader, and states the card's declared limitation — a list view's inline edit locks a whole column and keeps the row-blind answer. - Changeset: `@object-ui/plugin-detail` patch. ### Why the mount sits on the two blocks, not on the page host Measured on `origin/main` `b322b5da`: `record:details` and `record:highlights` are sibling blocks of the page schema; their nearest common ancestors are the page host's providers in `app-shell` `RecordDetailView` and the generic page renderer, so no `plugin-detail` point covers both. The dispatch's fallback was one wrapper in `RecordDetailView`, but a scope there would also cover the page's action dialogs and related lists: `RecordFieldAccessScope` dedupes only a nested scope for the SAME record, so a form editing ANOTHER row of the same object (a self-referencing child, say) would read this row's answer until its own arrived, or for good if its ask failed. Mounting on the two bodies keeps the answer on the surfaces that edit this row. The cost: a page drawing both blocks asks twice per load and per save. ### Why a copy of the trigger lines rather than a shared export `RecordFieldAccessForForm` lives in `@object-ui/plugin-form` and is not exported; `plugin-detail` does not depend on `plugin-form`, and `@object-ui/permissions` does not depend on `@object-ui/react` (the bus hook). Sharing it would mean a new published export (Clause-② yes) or a new dependency edge, so the ~20 trigger lines are repeated with a note naming the twin. ## Tests `ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx`, for each of `record:details` and `record:highlights` under the record page's providers (`MePermissionsProvider` serving the conditioned entry as its lock, record context, inline-edit session): - a draft the server unlocks: `summary` edits in place, `ref_no` (locked on every row) stays a read display; - an approved row: `summary` stays locked; - a write after which the server judges the row approved: asked again, `summary` locks; - CONTROL: a data source without `getRecordFieldAccess` keeps the row-blind lock; - CONTROL: a session that may not edit asks nothing. Ablation (one-shot, not kept): `scripts/ablation-replace.mjs` turned the wrapper's `if (!asks)` into an always-true condition (the scope never mounts; anchor 1 → 0, blob `2cbc16ec79d0` → `c38590de709a`): 6 of 10 red (the draft, approved and re-ask rows for both blocks), the 4 controls green; restored blob == HEAD, `git diff HEAD` empty. ## Acceptance notes - The record overlay payload `RecordDetailPanel` (kanban / calendar / gantt record drawers) draws the same `DetailSection` and its in-place edit still reads the row-blind answer. Not changed here: its saves run through the host's `onFieldSave`, so the re-ask trigger needs its own reading. Noted for the seat. - `HeaderHighlight` inside `DetailView` (an authored `highlightFields` header) is covered on the record page through the `record:details` mount. ## Verification <details><summary>Local runs</summary> - New suite: 10/10 passed. - `pnpm --dir ui exec vitest run packages/plugin-detail/` + `pnpm --filter @object-ui/plugin-detail type-check` on `7d0d12c8`: Test Files 322 passed, 1 skipped; Tests 2951 passed, 18 skipped; type-check exit 0 (lock VERDICT command-exit 0). - After merging `origin/main` (#27120 landed in `plugin-detail`, `components`, `i18n`): rebuilt `@object-ui/i18n` and `@object-ui/components`, re-ran `packages/plugin-detail/src/renderers/` plus `DetailSection.fieldWriteGrant-12103`: 108 files, 1090 tests passed. - `dispatch-gates --commands`: 68 derived families, each run on `9baa1e1a` and recorded exit 0 (`check:skill-examples` first exited 3, PREREQUISITE NOT MET, until `@objectstack/client-react` was built, then 0); `--ran` reconciliation: 68 run, 0 NOT-MEASURED, 0 UNRUN. `check-sdui-manifest` exit 0 among them. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` untouched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the dispatch's required Chinese digest). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbZ6ALxKhRMeuJrvUeU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dfe85c9 commit 2594f5b

6 files changed

Lines changed: 349 additions & 16 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
fix(plugin-detail): the record page's in-place edit reads the stored row's field locks. `record:details` and `record:highlights` now ask the server's record-scoped field projection (`DataSource.getRecordFieldAccess`, `GET /data/{object}/{id}/field-access`) when the page's inline-edit session may edit, so a field permission whose `when` locks a field only from some state onward disables it only on the rows it names, as the record form's edit dialog already does. The answer is asked again after a write to the record. A data source without the method keeps the row-blind `/auth/me/permissions` answer, and a list view's column-wide inline edit keeps it too.

‎content/docs/permissions/field-level-security.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ fields: {
6363
**that** record, and its `editable` list is the write gate's own verdict on
6464
an update of the stored row. So a lock that applies from a stage onward
6565
disables the field only on the rows its condition names — this is the
66-
answer a record's edit form reads. A row that is absent or that the caller
66+
answer a record's edit form and the record page's in-place edit read. A
67+
list view's inline edit locks a whole column, which cannot split per row,
68+
so it keeps the row-blind answer. A row that is absent or that the caller
6769
cannot read answers the record read's `404`. The answer is field-level
6870
only: the object grant and the record's row-level write scope (sharing,
6971
row-level security, ownership) are not part of it, so on a row the caller
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
* The record a detail body edits in place answers its own field locks
11+
* (objectstack#27146, the residual of objectstack#26594 ruling B).
12+
*
13+
* `DetailSection` and `HeaderHighlight` ask a field's in-place edit of the
14+
* `recordEdit` row (`resolveFieldAffordance`), which reads `usePermissions()`.
15+
* Under the page's provider alone that is `/auth/me/permissions`' answer, which
16+
* holds no row: a field permission whose `when` names only some rows is served
17+
* there as its lock, and the field was disabled on every row, the draft the
18+
* server lets the caller write included. Below `RecordFieldAccessScope`
19+
* (`@object-ui/permissions`) the same call answers from the server's
20+
* projection for THIS stored row (`DataSource.getRecordFieldAccess`, the
21+
* `GET /data/{object}/{id}/field-access` route) — the one source the record
22+
* form's edit dialog asks. Nothing here evaluates a `when`.
23+
*
24+
* Mounted by the two record blocks that edit the page's record in place,
25+
* `record:details` and `record:highlights`, around their bodies only. They are
26+
* sibling blocks of the page schema with no ancestor in this package, and a
27+
* mount above them (the page host) would also cover the page's action dialogs
28+
* and related lists, where a form editing ANOTHER row of the same object would
29+
* read this row's answer until its own arrives. So each block asks for itself:
30+
* a page drawing both asks twice.
31+
*
32+
* It asks only when the block's in-place edit is on: the page's inline-edit
33+
* session may edit (`canEdit`), the block offers it (`enabled`), the record is
34+
* stored (an id) and the data source carries the method. Otherwise the
35+
* row-blind answer stands, as it does for a non-ObjectStack backend.
36+
*
37+
* The answer belongs to the stored row, so it is asked again whenever the row
38+
* may have moved: a write the data source reports for this record (the inline
39+
* save bar's included) and a change on the data-invalidation bus for it (the
40+
* signal the page re-reads its record on). These are the triggers
41+
* `RecordFieldAccessForForm` (`@object-ui/plugin-form`) holds; that wrapper is
42+
* not exported and this package does not depend on that one, so the few lines
43+
* are repeated here rather than published as a new entry of either package.
44+
*/
45+
46+
import React, { useEffect, useState } from 'react';
47+
import { RecordFieldAccessScope } from '@object-ui/permissions';
48+
import { useDataInvalidation, useInlineEdit } from '@object-ui/react';
49+
import type { DataSource } from '@object-ui/types';
50+
51+
export interface RecordFieldAccessForRecordProps {
52+
objectName?: string | null;
53+
recordId?: string | number | null;
54+
dataSource?: DataSource | null;
55+
/** Whether the block draws in-place editors at all. Defaults to `true`. */
56+
enabled?: boolean;
57+
children: React.ReactNode;
58+
}
59+
60+
export function RecordFieldAccessForRecord({
61+
objectName,
62+
recordId,
63+
dataSource,
64+
enabled = true,
65+
children,
66+
}: RecordFieldAccessForRecordProps) {
67+
const inline = useInlineEdit();
68+
const load = dataSource?.getRecordFieldAccess;
69+
const asks = enabled
70+
&& !!inline?.canEdit
71+
&& !!objectName
72+
&& recordId !== undefined && recordId !== null && recordId !== ''
73+
&& typeof load === 'function';
74+
const id = asks ? String(recordId) : undefined;
75+
const busRevision = useDataInvalidation(asks ? (objectName as string) : undefined, id);
76+
const [writeRevision, setWriteRevision] = useState(0);
77+
useEffect(() => {
78+
if (!asks || typeof dataSource?.onMutation !== 'function') return;
79+
const object = (objectName as string).toLowerCase();
80+
return dataSource.onMutation((event) => {
81+
if ((event?.resource ?? '').toLowerCase() !== object) return;
82+
if (event.id != null && String(event.id) !== id) return;
83+
setWriteRevision((n) => n + 1);
84+
});
85+
}, [asks, dataSource, objectName, id]);
86+
if (!asks) {
87+
return <>{children}</>;
88+
}
89+
return (
90+
<RecordFieldAccessScope
91+
object={objectName as string}
92+
recordId={id as string}
93+
load={(object, recordKey) => (load as NonNullable<typeof load>).call(dataSource, object, recordKey)}
94+
revision={busRevision + writeRevision}
95+
>
96+
{children}
97+
</RecordFieldAccessScope>
98+
);
99+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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#27146 (the residual of objectstack#26594, ruling B) — the record
11+
* page's in-place edit asks the server for THIS record's field projection, the
12+
* answer the record form's edit dialog already reads.
13+
*
14+
* The persona is the contract app's finance role: `summary` is locked from
15+
* `approved` onward (`FieldPermissionSchema.when`). `/auth/me/permissions`
16+
* holds no row, so it serves that entry as its lock, and before this card the
17+
* details body and the highlights strip disabled `summary` on the draft the
18+
* server lets the caller save. `ref_no` is locked on every row: the control
19+
* that a lock the server keeps stays locked.
20+
*
21+
* Rows, for `record:details` and for `record:highlights`, each under the record
22+
* page's providers (record context, inline-edit session):
23+
* - a draft the server unlocks → `summary` edits in place, `ref_no` stays a
24+
* read display;
25+
* - an approved row → both stay read displays;
26+
* - a save after which the server judges the row approved → asked again,
27+
* and `summary` locks (the answer follows the stored row, not the mount);
28+
* and the controls:
29+
* - a data source without the method keeps the row-blind lock;
30+
* - a session that may not edit asks nothing.
31+
*/
32+
import * as React from 'react';
33+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
34+
import { render, screen, waitFor, act, cleanup, fireEvent } from '@testing-library/react';
35+
import '@testing-library/jest-dom';
36+
import { HighlightFieldsProvider, InlineEditProvider, RecordContextProvider, useInlineEdit } from '@object-ui/react';
37+
import { MePermissionsProvider, type MePermissionsResponse } from '@object-ui/permissions';
38+
import { RecordDetailsRenderer } from '../record-details';
39+
import { RecordHighlightsRenderer } from '../record-highlights';
40+
41+
const OBJECT = 'rfa_contract';
42+
const FIELDS = ['region', 'summary', 'ref_no'];
43+
44+
const OBJECT_SCHEMA = {
45+
name: OBJECT,
46+
label: 'Contract',
47+
// The title field the details body draws in the header, not as a row.
48+
nameField: 'name',
49+
highlightFields: FIELDS,
50+
fields: {
51+
name: { type: 'text', label: 'Name' },
52+
region: { type: 'text', label: 'Region' },
53+
summary: { type: 'text', label: 'Summary' },
54+
ref_no: { type: 'text', label: 'Reference number' },
55+
},
56+
};
57+
58+
const PRINCIPAL: MePermissionsResponse = {
59+
authenticated: true,
60+
userId: 'u-1',
61+
tenantId: null,
62+
roles: ['finance'],
63+
permissionSets: ['rfa_fixture_member'],
64+
objects: { [OBJECT]: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false } },
65+
// As `/auth/me/permissions` serves them: the conditioned entry as its lock.
66+
fields: {
67+
[`${OBJECT}.summary`]: { readable: true, editable: false },
68+
[`${OBJECT}.ref_no`]: { readable: true, editable: false },
69+
},
70+
} as MePermissionsResponse;
71+
72+
const RECORD = { id: 'c1', name: 'MSA', region: 'EMEA', summary: 'Draft terms', ref_no: 'R-1' };
73+
74+
const ACCESS = {
75+
draft: { object: OBJECT, recordId: 'c1', readable: ['id', ...FIELDS], editable: ['region', 'summary'] },
76+
approved: { object: OBJECT, recordId: 'c1', readable: ['id', ...FIELDS], editable: ['region'] },
77+
};
78+
79+
function makeDS(access: (typeof ACCESS)[keyof typeof ACCESS], withMethod = true) {
80+
const listeners = new Set<(e: any) => void>();
81+
return {
82+
getObjectSchema: vi.fn(async () => OBJECT_SCHEMA),
83+
findOne: vi.fn(async () => ({ ...RECORD })),
84+
find: vi.fn(async () => ({ data: [] })),
85+
update: vi.fn(async (resource: string, id: string, data: any) => {
86+
const record = { ...RECORD, ...data };
87+
// As the adapter does: every successful write is reported to `onMutation` subscribers.
88+
for (const listener of listeners) listener({ type: 'update', resource, id, record });
89+
return record;
90+
}),
91+
onMutation: (listener: (e: any) => void) => {
92+
listeners.add(listener);
93+
return () => listeners.delete(listener);
94+
},
95+
...(withMethod ? { getRecordFieldAccess: vi.fn(async () => access) } : {}),
96+
};
97+
}
98+
99+
/** Opens the page's inline-edit session, as the pencil does. */
100+
function EnterEdit() {
101+
const inline = useInlineEdit();
102+
return <button type="button" onClick={() => inline?.enter()}>enter-edit</button>;
103+
}
104+
105+
const BLOCKS = [
106+
{ block: 'record:details', mount: () => <RecordDetailsRenderer schema={{} as never} /> },
107+
{ block: 'record:highlights', mount: () => <RecordHighlightsRenderer schema={{ fields: FIELDS } as never} /> },
108+
];
109+
110+
function renderPage(ds: ReturnType<typeof makeDS>, block: React.ReactNode, canEdit = true) {
111+
return render(
112+
<MePermissionsProvider initialPermissions={PRINCIPAL}>
113+
<RecordContextProvider
114+
objectName={OBJECT}
115+
recordId={RECORD.id}
116+
data={RECORD}
117+
objectSchema={OBJECT_SCHEMA}
118+
dataSource={ds as never}
119+
>
120+
<HighlightFieldsProvider>
121+
<InlineEditProvider canEdit={canEdit}>
122+
<EnterEdit />
123+
{block}
124+
</InlineEditProvider>
125+
</HighlightFieldsProvider>
126+
</RecordContextProvider>
127+
</MePermissionsProvider>,
128+
);
129+
}
130+
131+
/** With the session open: which fields draw an input (edit in place) rather than their read display. */
132+
function editable(): Record<string, boolean> {
133+
return {
134+
region: screen.queryByDisplayValue(RECORD.region) !== null,
135+
summary: screen.queryByDisplayValue(RECORD.summary) !== null,
136+
ref_no: screen.queryByDisplayValue(RECORD.ref_no) !== null,
137+
};
138+
}
139+
140+
async function openSession() {
141+
await screen.findByText(RECORD.summary);
142+
fireEvent.click(screen.getByText('enter-edit'));
143+
await waitFor(() => expect(screen.getByDisplayValue(RECORD.region)).toBeInTheDocument());
144+
}
145+
146+
beforeAll(() => Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1280 }));
147+
148+
beforeEach(() => {
149+
// `useRecordEditable` probes `POST /api/v1/security/explain`; serve it from a
150+
// double so no real socket opens. Its answer is orthogonal to the field locks.
151+
vi.stubGlobal('fetch', vi.fn(async () => ({
152+
ok: true,
153+
status: 200,
154+
json: async () => ({ allowed: true }),
155+
text: async () => '{"allowed":true}',
156+
})) as never);
157+
});
158+
159+
afterEach(() => {
160+
cleanup();
161+
vi.unstubAllGlobals();
162+
});
163+
164+
describe.each(BLOCKS)('[#27146] $block', ({ mount }) => {
165+
it('on a draft the server unlocks, the conditioned field edits in place and the unconditioned lock holds', async () => {
166+
const ds = makeDS(ACCESS.draft);
167+
renderPage(ds, mount());
168+
await openSession();
169+
await waitFor(() => expect(editable()).toEqual({ region: true, summary: true, ref_no: false }));
170+
expect(ds.getRecordFieldAccess).toHaveBeenCalledWith(OBJECT, 'c1');
171+
});
172+
173+
it('on a row the condition names, the field stays locked', async () => {
174+
const ds = makeDS(ACCESS.approved);
175+
renderPage(ds, mount());
176+
await openSession();
177+
await waitFor(() => expect(ds.getRecordFieldAccess).toHaveBeenCalled());
178+
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
179+
expect(editable()).toEqual({ region: true, summary: false, ref_no: false });
180+
});
181+
182+
it('a save that moves the row into the state the condition names locks the field again', async () => {
183+
const ds = makeDS(ACCESS.draft);
184+
renderPage(ds, mount());
185+
await openSession();
186+
await waitFor(() => expect(editable().summary).toBe(true));
187+
// The server now judges the stored row approved: the next answer locks `summary`.
188+
ds.getRecordFieldAccess!.mockResolvedValue(ACCESS.approved);
189+
await act(async () => { await ds.update(OBJECT, 'c1', { region: 'APAC' }); });
190+
await waitFor(() => expect(ds.getRecordFieldAccess).toHaveBeenCalledTimes(2));
191+
await waitFor(() => expect(editable().summary).toBe(false));
192+
expect(editable().ref_no).toBe(false);
193+
});
194+
195+
it('a data source without the method keeps the row-blind lock', async () => {
196+
const ds = makeDS(ACCESS.draft, false);
197+
renderPage(ds, mount());
198+
await openSession();
199+
expect(editable()).toEqual({ region: true, summary: false, ref_no: false });
200+
});
201+
202+
it('a session that may not edit asks nothing', async () => {
203+
const ds = makeDS(ACCESS.draft);
204+
renderPage(ds, mount(), false);
205+
await screen.findByText(RECORD.summary);
206+
await act(async () => { await new Promise((r) => setTimeout(r, 0)); });
207+
expect(ds.getRecordFieldAccess).not.toHaveBeenCalled();
208+
});
209+
});

‎ui/packages/plugin-detail/src/renderers/record-details.tsx‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
import { DetailView } from '../DetailView';
3636
import { hasCellValue } from '../emptiness';
3737
import { RecordPageProvenanceContext } from '../recordPageProvenance';
38+
import { RecordFieldAccessForRecord } from '../recordFieldAccess';
3839
import { deriveFieldGroupDetailEntries, type FieldGroupDetailEntry } from '../synth/buildDefaultPageSchema';
3940
import { useRecordAriaProps } from './recordComponentAria';
4041
import { recordBindingBlock } from './recordBinding';
@@ -1187,15 +1188,24 @@ const RecordDetailsBody: React.FC<RecordDetailsRendererProps> = ({
11871188
return (
11881189
<div className={className} {...designer} {...ariaProps}>
11891190
<RecordPageProvenanceContext.Provider value={readsPageRecord}>
1190-
{/* objectui#12701 — the record is the context host's (the record
1191-
page's read, or the binding's row above); this body never reads it
1192-
again, it waits for `ctx.data`. */}
1193-
<DetailView
1194-
schema={synthesized}
1191+
{/* objectstack#27146 — the in-place edit's field locks are this
1192+
stored row's, asked of the server (`../recordFieldAccess`). */}
1193+
<RecordFieldAccessForRecord
1194+
objectName={ctx.objectName}
1195+
recordId={ctx.recordId}
11951196
dataSource={ctx.dataSource}
1196-
inlineEdit={inlineEditDefault}
1197-
recordFromHost
1198-
/>
1197+
enabled={inlineEditDefault}
1198+
>
1199+
{/* objectui#12701 — the record is the context host's (the record
1200+
page's read, or the binding's row above); this body never reads it
1201+
again, it waits for `ctx.data`. */}
1202+
<DetailView
1203+
schema={synthesized}
1204+
dataSource={ctx.dataSource}
1205+
inlineEdit={inlineEditDefault}
1206+
recordFromHost
1207+
/>
1208+
</RecordFieldAccessForRecord>
11991209
</RecordPageProvenanceContext.Provider>
12001210
</div>
12011211
);

0 commit comments

Comments
 (0)