From 054b6b112c4ff9f055d11fbcc81dc4c5e72e5e2e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 00:13:11 +0000 Subject: [PATCH] =?UTF-8?q?fix(plugin-detail):=20=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=20Add=20=E9=97=A8=E6=94=B9=E9=97=AE?= =?UTF-8?q?=20add.picker.object,=E6=BC=8F=E5=86=99=20picker=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8A=8A=E6=95=B4=E5=9D=97=E6=8D=A2=E6=88=90=E7=BA=A2?= =?UTF-8?q?=E5=8D=A1=20(#3838)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `record:related_list` 写了 `add` 却漏了 `add.picker` 时,`RelatedList.tsx:1293` 的门只判 `add` 真值,紧接着 `:1299` 裸取 `add.picker.object` 抛 TypeError,`SchemaRenderer` 捕获后把**整个相关列表**换成 「Component "record:related_list" failed to render」错误卡 —— 报错文案里 没有 `picker` 三个字。`picker` 在 spec(`RecordRelatedListProps.add`)里是必填, `RelatedListProps` 也把它声明为必填(这正是裸取能通过 TS 的原因),但渲染路径上 没有任何一处解析 schema(sdui-parser 的 manifest 门只比顶层键名与粗类型), 所以不合规元数据一路抵达裸取。 - Add 按钮(`:1121`)与 picker 对话框(`:1293`)的门统一收紧为 `add && pickerObject`,`:1299` 改用文件里已有的 `pickerObject` (即 `add?.picker?.object`)—— 渲染路径上最后一处 `add.picker` 裸取消失, 而不是给它加可选链:缺 `picker` 的 `add` 仍旧什么都不做,不产生第二方言 (AGENTS.md #0.1)。与 `:378`/`:390` 既有可选链同向。 - 具名诊断:缺 `add.picker.object` 时按文件内既有的「no referenceField/parentId」 开发者提示先例,console.warn 点名缺失的键。`renderers/record-related-list.tsx` 的整块 placeholder 先例适用于「整块无可渲染」(缺 objectName),这里列表主体 完好、只有 Add 未配置,故不套用。 - 钉子测试三例:缺 picker → 主体照常渲染 + 无 Add + 诊断点名;合法 add → Add 照常且打开 picker 目标;无 add → 现状不变且不告警。 - apps/console 两个 reach 探针里描述该崩溃与旧门形状的注释同步更正(仅注释, 样本与断言未动)。 (c) 产出端(save-gate / `os build`)真正跑一次 spec 解析仍是最正的方向,不在本单。 --- .../related-list-add-picker-guard-3838.md | 7 + .../public-block-binding-reach.test.tsx | 12 +- .../record-block-record-reach.test.tsx | 10 +- packages/plugin-detail/src/RelatedList.tsx | 38 ++++- .../RelatedList.addPickerGuard.test.tsx | 159 ++++++++++++++++++ 5 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 .changeset/related-list-add-picker-guard-3838.md create mode 100644 packages/plugin-detail/src/__tests__/RelatedList.addPickerGuard.test.tsx 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 && (