diff --git a/.changeset/related-list-add-picker-guard-3838.md b/.changeset/related-list-add-picker-guard-3838.md new file mode 100644 index 000000000..d450a8e6c --- /dev/null +++ b/.changeset/related-list-add-picker-guard-3838.md @@ -0,0 +1,7 @@ +--- +'@object-ui/plugin-detail': patch +--- + +`record:related_list`: an `add` without `add.picker` no longer takes the whole related list down. + +The Add-picker gate compared only `add` for truthiness and then read `add.picker.object` bare, so page metadata declaring `add` but omitting the (spec-required) `picker` threw during render and `SchemaRenderer` replaced the entire related list with a "Component failed to render" card whose message never mentioned `picker`. Both the Add button and the picker dialog now gate on the resolved `add.picker.object` — the list body renders as usual, only the unconfigured Add affordance is withheld, and a console hint names the missing key. Off-spec `add` still does nothing, so no lenient second dialect is introduced; producing-side validation of page metadata is tracked separately. diff --git a/apps/console/src/__tests__/public-block-binding-reach.test.tsx b/apps/console/src/__tests__/public-block-binding-reach.test.tsx index 28f452f7e..be94fd411 100644 --- a/apps/console/src/__tests__/public-block-binding-reach.test.tsx +++ b/apps/console/src/__tests__/public-block-binding-reach.test.tsx @@ -235,8 +235,9 @@ const sampleFor = (input: any): unknown => { if (input.name === 'objectName') return PROBE_OBJECT; // `record:related_list.add` — the generic `object` sample below is `{}`, and // `{}` is not a valid `add`: the spec makes `picker` required. An invalid one - // does not merely under-configure this block, it CRASHES it - // (`RelatedList.tsx:1299` dereferences `add.picker.object`, objectui#3838) — + // did not merely under-configure this block, it CRASHED it + // (`RelatedList.tsx:1299` dereferenced `add.picker.object`, objectui#3838, + // whose fix now gates the Add affordance on the resolved picker target) — // and a crashed block makes no data calls, which is indistinguishable from the // "declines to fetch" verdict this block is ledgered for below. That is a green // for the wrong reason, so the sample is spec-valid at the source instead. @@ -381,11 +382,12 @@ describe('public blocks — a declared objectName reaches the data layer (object // // Added with objectui#3808, and DEFENSIVE rather than load-bearing today: // that change made an invalid `add` sample crash `record:related_list` - // (objectui#3838), which is what it does in the sibling probe, but not + // (objectui#3838), which is what it did in the sibling probe, but not // here — `renderers/record-related-list.tsx:185` passes // `dataSource={ctx?.dataSource}`, this probe mounts with no RecordContext, - // so `RelatedList`'s `add && dataSource` guard short-circuits before the - // unguarded read. Checked, not assumed: reverting the sample to `{}` keeps + // so `RelatedList`'s picker gate (`add && pickerObject && dataSource`, + // truthiness-only on `add` before #3838) short-circuits before the read + // either way. Checked, not assumed: reverting the sample to `{}` keeps // all 16 green. The predicate itself is known to work — applied to both // branches it reports the two crashes in objectui#3840 — so this is a // cheap standing guard on the one branch where a crash IS the pass diff --git a/apps/console/src/__tests__/record-block-record-reach.test.tsx b/apps/console/src/__tests__/record-block-record-reach.test.tsx index d2e0fe77e..a4ccc9ab2 100644 --- a/apps/console/src/__tests__/record-block-record-reach.test.tsx +++ b/apps/console/src/__tests__/record-block-record-reach.test.tsx @@ -205,10 +205,14 @@ const DATA_SOURCE_METHODS = [ * sample, which would put an unspecified bag on every future `object` input. * * That `{}` did not merely under-exercise the block, it CRASHED it — - * `RelatedList.tsx:1299` dereferences `add.picker.object` where `:378` / `:390` - * optional-chain the same path — and the crash is filed as objectui#3838 rather + * `RelatedList.tsx:1299` dereferenced `add.picker.object` where `:378` / `:390` + * optional-chain the same path — and the crash was filed as objectui#3838 rather * than papered over here: this fixture's job is to be spec-valid, not to steer - * clear of the renderer's unguarded reads. + * clear of the renderer's unguarded reads. #3838 has since tightened that gate + * to require the resolved `add.picker.object`, so the same `{}` now withholds + * the Add affordance behind a named console hint instead of taking the block + * down; the sample below stays spec-valid on its own merit, not as crash + * avoidance. */ const SAMPLE_BY_INPUT: Readonly> = { // On `record:*` this names the RELATED object, not the page's object — diff --git a/packages/plugin-detail/src/RelatedList.tsx b/packages/plugin-detail/src/RelatedList.tsx index 1cb2e9610..3a9ac11a5 100644 --- a/packages/plugin-detail/src/RelatedList.tsx +++ b/packages/plugin-detail/src/RelatedList.tsx @@ -393,6 +393,27 @@ export const RelatedList: React.FC = ({ return derived.length > 0 ? derived : undefined; }, [pickerSchema, pickerDisplayField]); + // Developer hint for an `add` that cannot be honoured (#3838). `picker` is + // REQUIRED on `add` by the spec (`RecordRelatedListProps.add`), so getting + // here means the page metadata is off-spec — but nothing on the render path + // parses it (the sdui-parser manifest gate compares top-level key names and + // coarse types only), so the renderer is the first place able to say so. It + // says WHICH key is missing, because the failure it replaces — a bare + // `add.picker.object` read that threw and made SchemaRenderer swap the whole + // list for a "failed to render" card — never mentioned `picker` at all. + // Console-only, matching the in-file hint for the other partial + // misconfiguration (`no referenceField/parentId` below): the block-level + // dashed placeholder precedent in `renderers/record-related-list.tsx` is for + // blocks that can render NOTHING (missing objectName), whereas here only the + // Add affordance is unconfigured and the list body is perfectly fine. + React.useEffect(() => { + if (!add || pickerObject) return; + // eslint-disable-next-line no-console + console.warn( + `[RelatedList] "${api || objectName || 'related list'}" declares add without add.picker.object — the Add affordance is not rendered. add.picker is required by the spec (RecordRelatedListProps.add): set add.picker.object to the object the picker should list.`, + ); + }, [add, pickerObject, api, objectName]); + React.useEffect(() => { // Stale-response guard: page flips re-run this effect while an earlier // window may still be in flight — a slow page-2 response must not @@ -1118,7 +1139,12 @@ export const RelatedList: React.FC = ({ onToolbarAction={onToolbarAction} /> ))} - {add && ( + {/* Gated on the RESOLVED picker target, not merely on `add` being + truthy: an `add` without `picker` is metadata the spec rejects, + and offering a button that could never open a picker is worse + than withholding it (#3838 — the console hint above names the + missing key). */} + {add && pickerObject && (