Skip to content

Commit 76993f8

Browse files
fix(plugin-detail,app-shell): a field group's visibleWhen gates its record:details section through the spec's { group } reference (objectui#11630) (#11641)
Fixes #11630 Clause-②: yes ## What was wrong An object's `fieldGroups[].visibleWhen` gated the group on the entry form but not on the record detail page. `deriveFieldGroupDetailSections` rebuilt each group as an enumerated section and dropped the predicate that the shared derivation (`deriveFieldGroupLayout`, `@objectstack/spec`) passes through, and `record:details` read none. So the "Pro details" section of the card's reproduction rendered, header included, on a `basic` row. ## Route (patch round 1: route B, seat ruling on the card) `@objectstack/spec` 17.6.0 `RecordDetailsProps.sections[]` is strict. It declares `group`, the ADR-0085 §5 reference form that inherits a group's members and presentation, `visibleWhen` included. It declares `fields` as bare names, and it refuses `visibleWhen` on an enumerated section. So the fix writes pages in that vocabulary: - **Synthesizer** (`packages/plugin-detail/src/synth/buildDefaultPageSchema.ts`): `resolveDetailSections`, and through it `buildDefaultDetails` / `buildDefaultTabs` / `buildDefaultPageSchema`, now emit each declared group as `{ group: KEY, columns }`. The trailing ungrouped section lists bare field names. The derivation sits behind one package-internal function, `deriveFieldGroupDetailEntries`. It returns each resolved section with the group's `visibleWhen` carried BESIDE it, never on it. The public `deriveFieldGroupDetailSections` returns exactly what it returned before this card: resolved sections, no predicate. - **Renderer** (`packages/plugin-detail/src/renderers/record-details.tsx`): `record:details` resolves a `{ group }` reference against the object's `fieldGroups` and evaluates the group's `visibleWhen` against the bound record. It drops the section, heading and members, when the verdict is FALSE. It does **not** read `visibleWhen` off an enumerated section, a key the spec refuses there. - **Runtime default page** (`packages/app-shell/src/views/RecordDetailView.tsx`): the console's default record page builds its own section list, so it now writes `{ group: KEY, showBorder: true }` for each declared group. Without that change the card's reproduction path stays ungated, because an enumerated copy cannot carry the predicate. Its ungrouped primary and "More details" sections are unchanged. - The `record:details` registration's `sections` input description now says a `{ group }` reference inherits the group's `visibleWhen`, and that one written on a section itself is not read. ### One evaluator, the form's The form renderer gates a `section-divider` row with `resolveFieldRuleState` (`@object-ui/core`) under `usePredicateScope()` (`@object-ui/react`). `record:details` makes the same call with the same three inputs. Both modules are existing peer dependencies of `@object-ui/plugin-detail`: no `plugin-form` dependency and no copy of the evaluator. - `record`: every declared field seeded to `null`, then the defined values of the bound record, as the form's `ruleRecord` is built. A relation is collapsed to its stored id (`toPredicateRecord`), because the edit form reads its record without `$expand` and this page's record may arrive expanded. - `previous`: the same row. An edit form binds `previous` to the persisted row, and the row this page shows is the persisted row. - `scope`: the host predicate scope (`current_user`, `features`, ...). A predicate that cannot be evaluated SHOWS the section and warns once. That is the form's direction (`VISIBLE_WHEN_FAULTED`, ADR-0137 D3), taken from the shared evaluator rather than restated. ## Consumers of the synthesized sections, and what each does now | consumer | path | after this PR | | --- | --- | --- | | console default record page | `RecordDetailView` builds `synthParts.sections` from `deriveFieldGroupDetailSections(objectDef)` and passes them into `buildDefaultPageSchema` | changed: writes `{ group: KEY, showBorder: true }` per group; `RecordContextProvider` gets `objectSchema={objectDef}`, so the reference resolves | | console page-assignment probe | `RecordDetailView`'s early `buildDefaultPageSchema(objectDef, slots)` | no code change; it only decides whether a page exists | | Studio page create | `metadata-admin/anchors.ts` `createSeed` stores `buildDefaultPageSchema(objectDef).regions` | no code change; the seed now holds `{ group }` references and parses as `RecordDetailsProps` | | Studio slotted-page preview | `metadata-admin/previews/PagePreview.tsx` calls `buildDefaultPageSchema(recordSchema, { slots })` | no code change; its `RecordContextProvider` gets `objectSchema={recordSchema}`, so the references resolve | | authored `{ group }` reference | `record:details` resolves it through `deriveFieldGroupDetailEntries` | now gated by the group's `visibleWhen` | | `deriveFieldGroupDetailSections` callers | public export | output unchanged from before this card | Pins that cover them: `RecordDetailView.sectionHeadingsRenderPath-6190` (mounts `RecordDetailView`), `anchors.page-seed`, `PagePreview`, `defaultFieldGroupsPage.sectionHeadings` (real synthesized page through the real registry), and the two new files below. ## Published surface (Clause-②) The emitted shape changes for grouped objects: BREAKING for code that reads the synthesized sections (the changeset is `minor`, per AGENTS.md §9). - `buildDefaultPageSchema`, `buildDefaultTabs`, `buildDefaultDetails` and `resolveDetailSections` used to emit each group as an enumerated copy: `name`, `label`, `icon`, `description`, `collapsible`, `defaultCollapsed`, `columns`, and `fields` as rich `{ name, label, type, ... }` descriptors. They now emit `{ group: KEY, columns }`. The ungrouped section's `fields` are bare names. An explicit `sections` option is still returned unchanged. - `record:details` gates a `{ group }` reference with the group's `visibleWhen`. - `RecordDetailView` (`@object-ui/app-shell`) writes `{ group: KEY, showBorder: true }` per group. - Not added: a type member, zod member, registered `inputs` entry, prop or i18n key. `deriveFieldGroupDetailEntries` is exported from its module for the renderer and is not re-exported from the package entry. Migration (also in the changeset): read a group's heading or members via `deriveFieldGroupDetailSections(def)` instead of off a synthesized section. Replace a stored section copied from an older seed (`{ name: 'pro', label: ..., fields: [...] }`) with `{ group: 'pro' }`; only the reference form inherits the group's `visibleWhen`. **Spec legality, measured** (installed `@objectstack/spec` 17.6.0, `RecordDetailsProps.safeParse`): | input | verdict | | --- | --- | | `buildDefaultDetails(def).properties` for a grouped object | OK | | the `record:details` node inside `buildDefaultPageSchema(def)` | OK | | control: an enumerated section plus `visibleWhen` | `unrecognized_keys @ sections.0 [visibleWhen]` | | control: an enumerated section with a field object | `invalid_type @ sections.0.fields.0` | This also retires the round-0 finding that the synthesized grouped sections were refused at every field object. ## Tests Gates on `75c794c` (route B, after merging `origin/main` at `0abd4f9`), all under the shared verify lock, from the repo root: - Targeted, six files (the two new files, `buildDefaultPageSchema.test.ts`, `defaultFieldGroupsPage.sectionHeadings`, `recordDetailsInputs.spec-parity`, `buildDefaultPageSchema.strictPayload`): `Test Files 6 passed (6)`, `Tests 147 passed (147)`. - `pnpm exec vitest run packages/plugin-detail/`: `Test Files 238 passed | 1 skipped (239)`, `Tests 2382 passed | 8 skipped (2390)`. - `pnpm exec vitest run packages/app-shell/`: `Test Files 1001 passed | 1 skipped (1002)`, `Tests 9951 passed | 9 skipped (9960)`. - `turbo run build --filter='@object-ui/app-shell^...'`: exit 0. `pnpm --filter @object-ui/plugin-detail type-check`: exit 0. `lint`: exit 0 for both packages, 0 errors. - `pnpm --filter @object-ui/app-shell type-check` was red on `75c794c` (TS2345: `flatMap` inferred its element type from the first return). Fixed in `594b154` and `aeddcd7` by annotating the element type as unknown-valued records, a type-only change. Rerun on final HEAD `aeddcd7` under the lock: app-shell type-check exit 0; app-shell lint exit 0, 0 errors, with the warning count equal to `75c794c`; the three app-shell consumer pins (`RecordDetailView.sectionHeadingsRenderPath-6190`, `anchors.page-seed`, `PagePreview`): `Tests 15 passed (15)`; the two new files: `Tests 18 passed (18)`. The full plugin-detail and app-shell suites ran on `75c794c`, whose runtime code `aeddcd7` does not change. Unlocked root checks on `594b154`, each exit 0: `check:new-line-citations` (0 new), `check:control-bytes`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:phantom-deps`, `check:installed-pin-claims`, `check:esm-specifiers`, `check:spec-symbols`, `scripts/check-changeset-presence.mjs`. On `aeddcd7`, `check:new-line-citations`, `check:control-bytes`, `check:changeset-claims`, `check:test-path-roots`, `check:phantom-deps` and the changeset presence check were rerun, exit 0 each. `renderers/__tests__/record-details.groupVisibleWhen-11630.test.tsx` renders every case twice on the same row: through `record:details`, fed by `buildDefaultDetails` (now `{ group }` references), and through the entry form renderer (`@object-ui/components` `form`), given the section-divider row `projectSectionDivider` emits for a group, as an edit form of that row. Each leg asserts the form's verdict, the detail page's verdict and the expected verdict on `[basic, pro]`: | predicate | expected | | --- | --- | | envelope `record.kind == 'pro'` (the card) | hidden, shown | | bare string, same source | hidden, shown | | no predicate (control) | shown, shown | | bare identifier `kind == 'pro'` (unbound, so it faults) | shown, shown | | parse error | shown, shown | | `record.tier == 'gold'`, a declared field no row carries | hidden, hidden | | `record.owner == 'u2'`, a lookup that arrives expanded here | hidden, shown | | `previous.kind == 'pro'` | hidden, shown | | `'sales_manager' in current_user.positions`, denied then allowed | hidden, shown | Further legs: - An authored `{ group: 'pro' }` reference: hidden, shown, on both spellings. - The runtime default page's `{ group, showBorder }` shape: hidden, shown. - An enumerated section carrying a spec-refused `visibleWhen`: shown, shown, because it is not read. Every render reads an ungated anchor first and throws `HARNESS DEAD` before any verdict. `synth/__tests__/buildDefaultPageSchema.groupVisibleWhen-11630.test.ts` pins the emitted references, the absence of `visibleWhen` and field objects on every section the page writes, the two-control legality reading above, and the predicate travelling beside the resolved section. ### Ablation on route B (one-shot, on `75c794c`) Each mutation went through `ablation-replace.mjs`: an anchor that must hit once, a blob change verified on disk, and a restore proven by blob equal to HEAD with an empty `git diff HEAD`. Afterwards the diff against HEAD was 0 bytes and the two new files passed again (18/18). The red direction was predicted before running. | mutation | predicted | observed | | --- | --- | --- | | drop the gate in `resolveSectionGroup` | 8 gated legs red; 4 controls and 6 synth legs green | 8 failed, 10 passed | | emit the old enumerated copy instead of `{ group }` | 3 synth shape and legality legs, plus the 6 gated matrix legs fed by `buildDefaultDetails`, red | 9 failed, 9 passed | | read `visibleWhen` off an enumerated section | only the enumerated-not-read leg red | 1 failed, 17 passed | | bind `previous` to nothing | only the `previous` leg red | 1 failed, 11 passed | | skip the relation collapse | only the lookup leg red | 1 failed, 11 passed | | skip the `null` seed | only the declared-but-absent leg red | 1 failed, 11 passed | No `HARNESS DEAD` or partial-group error appeared in any ablation run. **NOT MEASURED: live before/after on a running backend.** Reason: the shared objectstack checkout has no built CLI (`packages/cli/dist` is absent). A backend needs a fresh objectstack worktree plus a full app-showcase closure build, and that build goes through the shared verify lock, which stayed saturated this session. The console path is covered in two halves, not by a live run. The `RecordDetailView` mount pin shows that its references resolve and the group headings render. The runtime-default-page leg shows that the same `{ group, showBorder }` shape is gated. ## Acceptance notes - The predicate is evaluated against the bound, persisted record, not an unsaved inline-edit draft. - A page stored from an older Studio seed keeps its enumerated copy of a group and is not gated. The changeset's migration line covers it. - `RecordDetailView`'s ungrouped primary and "More details" sections still carry rich field descriptors. That page is synthesized at runtime and never stored or parsed, and route B does not need them changed. - The spec's `deriveFieldGroupLayout` TSDoc calls the section gate fail-closed. The form, and now the detail page, fail open on a faulted predicate. This PR keeps parity with the form; the seat carries that question to the maintainer. Attribution: implemented in session `https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL`. --- _Generated by [Claude Code](https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c000398 commit 76993f8

8 files changed

Lines changed: 781 additions & 57 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'@object-ui/plugin-detail': minor
3+
'@object-ui/app-shell': minor
4+
---
5+
6+
A field group's `visibleWhen` now gates its section on the record detail page, the same way it gates the section on the entry form (objectui#11630). Take an object whose `fieldGroups` entry declares `visibleWhen: "record.kind == 'pro'"`. The edit form hid the "Pro details" section on a `basic` row, but the detail page drew it on every row, header included. The detail page now draws it only where the predicate holds.
7+
8+
**How it works.** The detail body now writes each field group as `@objectstack/spec`'s own reference form, `{ group: KEY }` (`RecordDetailsProps.sections[].group`). `record:details` resolves the reference against the object's `fieldGroups`: the group's members, label, icon, description and collapse state, and its `visibleWhen`. The predicate is evaluated per record with the form's own evaluator (`resolveFieldRuleState` from `@object-ui/core`, under `usePredicateScope`), and both spellings work as they do on the form: a bare CEL string or a `{ dialect: 'cel', source }` envelope.
9+
10+
- Values bind under `record.`, so a bare identifier is unbound.
11+
- A declared field the row does not carry compares as `null`.
12+
- A relation binds as its stored id, even when the page's record arrived expanded.
13+
- `previous` binds the persisted row, as on the record's edit form.
14+
- A predicate that cannot be evaluated SHOWS the section and warns once, which is what the form does.
15+
16+
A FALSE verdict removes the whole section, heading and members. This is display only: the record API serves those fields either way.
17+
18+
**Clause-②: yes (output shape change).** BREAKING for code that reads the synthesized sections of a grouped object. The bump is still `minor`: under this repo's release model, objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.
19+
20+
- `buildDefaultPageSchema`, `buildDefaultTabs`, `buildDefaultDetails` and `resolveDetailSections` (`@object-ui/plugin-detail`) change the `record:details` `properties.sections[]` they emit for an object with `fieldGroups`:
21+
- Each declared group becomes `{ group: KEY, columns }`. It used to be an enumerated copy of the group: `name`, `label`, `icon`, `description`, `collapsible`, `defaultCollapsed`, `columns`, and `fields` as rich `{ name, label, type, … }` descriptors.
22+
- The trailing ungrouped section keeps `columns` and lists `fields` as bare names.
23+
- The node now parses as `RecordDetailsProps`; the old one was refused at every field object. Pages Studio seeds from it (`createSeed`) change the same way. An explicit `sections` option is still returned unchanged.
24+
- `record:details` gates a `{ group }` reference with the group's `visibleWhen`. It does not read `visibleWhen` off an enumerated section, a key the spec refuses there.
25+
- The console's default record page (`RecordDetailView`, `@object-ui/app-shell`) writes `{ group: KEY, showBorder: true }` for each declared group. Its ungrouped primary and "More details" sections are unchanged.
26+
- The `record:details` registration's `sections` input description now says a `{ group }` reference inherits the group's `visibleWhen`, and that one written on a section itself is not read.
27+
- Unchanged: `deriveFieldGroupDetailSections` still returns the resolved, enumerated sections (what a reference renders as), with no `visibleWhen` on them. No type member, zod member, registered `inputs` entry, prop or i18n key is added.
28+
29+
**Migration.**
30+
31+
- FROM reading a group's heading or members off a synthesized section (`sections[i].label`, `sections[i].fields`) → TO `deriveFieldGroupDetailSections(def)`, which returns the resolved sections, or the group's own entry in `def.fieldGroups`.
32+
- FROM a stored page section copied from an older seed, `{ name: 'pro', label: 'Pro details', fields: [ … ] }` → TO `{ group: 'pro' }`, with `columns` / `showBorder` / `headerColor` kept if you set them. Only the reference form inherits the group's `visibleWhen`; an enumerated copy renders on every row, as before.

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,21 +2274,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
22742274
// fields stay tucked away.
22752275
const grouped = deriveFieldGroupDetailSections(objectDef as any);
22762276
if (grouped) {
2277-
return grouped.flatMap((sec: any) => {
2277+
return grouped.flatMap<Record<string, unknown>>((sec: any) => {
22782278
if (!sec.name) {
22792279
return splitPrimarySecondary(
22802280
(sec.fields as any[]).map((f: any) => f.name),
22812281
);
22822282
}
2283-
return [{
2284-
...sec,
2285-
// Re-resolve the derived heading through the per-object i18n
2286-
// convention. Reads and writes the same one slot the
2287-
// synthesizer emits and `record:details` consumes —
2288-
// `label` (objectui#6190).
2289-
label: sectionLabel(objectDef.name, sec.name, sec.label),
2290-
showBorder: true as const,
2291-
}];
2283+
// A declared group is written as the spec's own REFERENCE form,
2284+
// `{ group: KEY }` (`RecordDetailsProps.sections[].group`,
2285+
// ADR-0085 §5), never as an enumerated copy of the group
2286+
// (objectui#11630). `record:details` resolves it against the
2287+
// object's `fieldGroups` — members, heading (through the same
2288+
// per-object i18n convention, `sectionLabel`), icon,
2289+
// description, collapse — and evaluates the group's
2290+
// `visibleWhen` against this record, which an enumerated copy
2291+
// cannot carry: the spec refuses `visibleWhen` on one. The
2292+
// Card chrome stays this page's layout choice, a key the spec
2293+
// permits beside `group`.
2294+
return [{ group: sec.name as string, showBorder: true as const }];
22922295
});
22932296
}
22942297

‎packages/plugin-detail/src/index.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ ComponentRegistry.register('details', RecordDetailsRenderer, {
530530
// `record:reference_rail`'s own component-level `hideEmpty` input below.
531531
inputs: [
532532
{ name: 'columns', type: 'enum', enum: ['1', '2', '3', '4'], description: 'Number of columns for field layout (1-4)' },
533-
{ name: 'sections', type: 'array', of: 'object', description: 'Field groups rendered as the detail body, in order. Every entry is an OBJECT — `{ name?, label?, columns?, fields }` — a bare section-id string is NOT accepted (the spec retired that spelling in objectstack#5611, and the renderer reads name/label/fields off each entry, so a string entry renders no fields at all). `fields` are the field names shown in this section, in order — required unless `group` supplies the members instead (the spec refuses a section carrying neither, and refuses one carrying both). `label` is the section heading; omit it for an untitled, borderless section. `name` is a stable snake_case identifier and the i18n anchor — the heading resolves through objects.<object>._sections.<name>.label, so a section without a name shows its authored label in every locale. `columns` (1-4) is THIS section\'s field-grid width; omit it and the renderer derives the width. Authoring `sections` at all makes it the only source of the detail body; omit it and the body falls back to the object\'s highlightFields. @objectstack/spec 17.3.0 declares eight more member keys on an entry, and this renderer honours all eight: `icon` (a Lucide name on the section header), `description` (sub-heading copy under the heading), `collapsible` and `defaultCollapsed` (a foldable section and its initial state), `showBorder` (force the Card wrapper on or off, overriding the heading-derived default) and `headerColor` (a header tint from the shared palette) through DetailSection, plus `group` — the ADR-0085 §5 REFERENCE form, the alternative to enumerating `fields`. `{ group: \'contact_info\' }` inherits the object\'s `fieldGroups` entry with that key: its members (every visible field pointing at it, in declaration order) and its presentation (label, icon, description, collapse) all come from the group, so the section restates none of it and the spec refuses those keys beside `group`; `columns`, `showBorder` and `headerColor` stay yours because they are how THIS page lays the section out. A `group` naming no declared group renders nothing and is reported to the console (`@objectstack/lint` flags it as `page-section-group-unknown`). `hideEmpty` is the eighth, and it decides whether an ALL-empty section exists: it defaults to on, so a section whose fields are every one of them empty renders nothing at all — no heading, no skeleton — and `hideEmpty: false` is the spelling that keeps that heading and its label skeleton on a brand-new record. It decides ONLY the all-empty case; the empty rows of a section that still has a filled one belong to DetailSection\'s auto-hide heuristic plus the reader\'s show-empty toggle, which no authored value overrides in either polarity (objectui#7129 Q2-C), and inline-edit mode renders the section either way so its fields stay reachable. Restored under objectui#8603 (maintainer 2026-09-15) after objectui#7129 retired it on the premise, since falsified upstream, that the spec refused the key. None of the eight has a designer control yet; they are authorable in source mode only, tracked as a deferred feature.' },
533+
{ name: 'sections', type: 'array', of: 'object', description: 'Field groups rendered as the detail body, in order. Every entry is an OBJECT — `{ name?, label?, columns?, fields }` — a bare section-id string is NOT accepted (the spec retired that spelling in objectstack#5611, and the renderer reads name/label/fields off each entry, so a string entry renders no fields at all). `fields` are the field names shown in this section, in order — required unless `group` supplies the members instead (the spec refuses a section carrying neither, and refuses one carrying both). `label` is the section heading; omit it for an untitled, borderless section. `name` is a stable snake_case identifier and the i18n anchor — the heading resolves through objects.<object>._sections.<name>.label, so a section without a name shows its authored label in every locale. `columns` (1-4) is THIS section\'s field-grid width; omit it and the renderer derives the width. Authoring `sections` at all makes it the only source of the detail body; omit it and the body falls back to the object\'s highlightFields. @objectstack/spec 17.3.0 declares eight more member keys on an entry, and this renderer honours all eight: `icon` (a Lucide name on the section header), `description` (sub-heading copy under the heading), `collapsible` and `defaultCollapsed` (a foldable section and its initial state), `showBorder` (force the Card wrapper on or off, overriding the heading-derived default) and `headerColor` (a header tint from the shared palette) through DetailSection, plus `group` — the ADR-0085 §5 REFERENCE form, the alternative to enumerating `fields`. `{ group: \'contact_info\' }` inherits the object\'s `fieldGroups` entry with that key: its members (every visible field pointing at it, in declaration order), its presentation (label, icon, description, collapse) and its `visibleWhen` all come from the group, so the section restates none of it — the predicate is evaluated against the bound record exactly as the entry form evaluates it, and a group it hides renders nothing (a `visibleWhen` written on a section itself is refused by the spec and not read: gate the group and reference it) and the spec refuses those keys beside `group`; `columns`, `showBorder` and `headerColor` stay yours because they are how THIS page lays the section out. A `group` naming no declared group renders nothing and is reported to the console (`@objectstack/lint` flags it as `page-section-group-unknown`). `hideEmpty` is the eighth, and it decides whether an ALL-empty section exists: it defaults to on, so a section whose fields are every one of them empty renders nothing at all — no heading, no skeleton — and `hideEmpty: false` is the spelling that keeps that heading and its label skeleton on a brand-new record. It decides ONLY the all-empty case; the empty rows of a section that still has a filled one belong to DetailSection\'s auto-hide heuristic plus the reader\'s show-empty toggle, which no authored value overrides in either polarity (objectui#7129 Q2-C), and inline-edit mode renders the section either way so its fields stay reachable. Restored under objectui#8603 (maintainer 2026-09-15) after objectui#7129 retired it on the premise, since falsified upstream, that the spec refused the key. None of the eight has a designer control yet; they are authorable in source mode only, tracked as a deferred feature.' },
534534
{ name: 'fields', type: 'array', of: 'string', description: 'Explicit field list (overrides highlightFields)' },
535535
// `hideFields` is DECLARED, not merely honoured (objectui#3808). The spec
536536
// declares it (objectstack#5611) and `RecordDetailsRenderer` has read it

0 commit comments

Comments
 (0)