From 21ffcb2f70ca5fd60374ee8ac330ca1f6c610bdf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 10:54:21 +0000 Subject: [PATCH] =?UTF-8?q?fix(plugin-list,plugin-form):=20=E5=9C=A8?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=E8=B7=AF=E5=BE=84=E4=B8=8A=E6=8A=8A?= =?UTF-8?q?=20dataSource=20=E6=8E=A5=E5=88=B0=20list-view=20/=20embeddable?= =?UTF-8?q?-form=20(#3144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SchemaRenderer` 把 dataSource 放在 `SchemaRendererContext` 上,**从不**注入组件 props。所以任何从 `props.dataSource` 读取的组件,都需要注册处包一层把二者桥接起来。 `object-form` / `object-kanban` / `object-calendar` 一直都有这层。这两个没有: - `list-view`(以及 `view:list` 别名)直接注册裸 `ListView`,而 `ListView.tsx:585` 是 `const dataSource = props.dataSource` —— 于是取 object schema 的那个 effect 直接 return,永远不取数,渲染出 `data-testid="empty-state"` 的 "Nothing here"。 - `embeddable-form` 的渲染器是 `({ schema }) => `, 把 context 整个丢了 —— 内层 ObjectForm 用来取字段的那个只读源(`formDataSource`) 永远构造不出来,而提交路径 `if (dataSource) await dataSource.create(...)` 也没东西 可调:公开表单会"提交成功"却什么都没写。 两者的 registry `inputs` 都把 `objectName` 标为 **required**。协议逼作者填、而这条 路径上没有任何东西读得到的绑定,就是 objectstack#4413 的形状往上挪了一层。之所以一直 没人报,是因为 console 走的不是这条路:它经 `ObjectView` 的 `renderListView` render-prop,由宿主自己把 dataSource 递进去。应用内是好的,注册表/SDUI 路径是坏的 ——而后者正是 sdui.manifest.json 描述的那份契约、`kind:'react'` 页面走的那条路。 发现自 `public-block-binding-reach.test.tsx`(objectstack#4472),不是人肉读出来的。 那个台账把这两条记为具名欠债;桥接一加上,台账的双向断言**立刻变红,直到条目被删掉** ——机制按设计工作。现在只剩 `record:related_list`,且是合理的。 显式传入的 `dataSource` prop 仍然优先,宿主自带数据源的用法不受影响; `ListViewRenderer` 用 forwardRef,`ListViewHandle` 经注册表仍可用。 验证:plugin-list / plugin-form / plugin-view 三个包 53 files / 662 tests 全绿; 两个包 `turbo type-check`(依赖已构建)clean;console 21 tests 全绿,其中把这两条塞回 台账会立刻变红("now reaches the data layer — delete its NO_DATA_REACH entry")。 Closes #3144 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S3cP1eY1novcNhQEDBrSZD --- ...-view-embeddable-form-datasource-bridge.md | 35 +++++++++++++++++ .../public-block-binding-reach.test.tsx | 38 +++++++------------ packages/plugin-form/src/index.tsx | 11 +++++- packages/plugin-list/src/index.tsx | 38 +++++++++++++++++-- 4 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 .changeset/list-view-embeddable-form-datasource-bridge.md diff --git a/.changeset/list-view-embeddable-form-datasource-bridge.md b/.changeset/list-view-embeddable-form-datasource-bridge.md new file mode 100644 index 0000000000..0c73ad1ac6 --- /dev/null +++ b/.changeset/list-view-embeddable-form-datasource-bridge.md @@ -0,0 +1,35 @@ +--- +"@object-ui/plugin-list": patch +"@object-ui/plugin-form": patch +--- + +`list-view` and `embeddable-form` get a data source on the registry path — their required `objectName` was binding to nothing (#3144). + +`SchemaRenderer` puts the data source on `SchemaRendererContext` and **never** injects it into +component props. A component that reads `props.dataSource` therefore needs its registration to +bridge the two. `object-form`, `object-kanban` and `object-calendar` each register a small +renderer that does exactly that. These two did not: + +- `list-view` (and its `view:list` alias) registered the bare `ListView`, which reads + `props.dataSource` — so its `getObjectSchema` effect returned immediately, nothing was ever + fetched, and it rendered the `empty-state` "Nothing here". +- `embeddable-form`'s renderer was `({ schema }) => `, dropping + the context entirely — so the read-only source it derives for its inner `ObjectForm` was never + built, and its submit path (`if (dataSource) await dataSource.create(...)`) had nothing to call. + +Both declare `objectName` **required** in their registry `inputs`. A binding the protocol obliges +an author to supply, that nothing on that path can consume, is objectstack#4413's shape one layer +up — and the reason it went unnoticed is that the console never takes this path: it reaches +ListView through `ObjectView`'s `renderListView` render-prop, which passes a data source itself. +Broken on the registry/SDUI path, which is the path `sdui.manifest.json` describes and a +`kind:'react'` page walks. + +Found by `apps/console/src/__tests__/public-block-binding-reach.test.tsx` (objectstack#4472), not +by hand — that suite mounts every public block declaring an `objectName` under a recording +`dataSource` and asserts the binding arrives. Its ledger carried these two as named debt; with the +bridge in place the ledger's both-directions assertion **failed until the entries were deleted**, +which is the mechanism working as designed. Only `record:related_list` remains, and legitimately +(it needs a parent record id from `RecordContext` before it may fetch). + +An explicit `dataSource` prop still wins, so hosts passing their own are unaffected, and +`ListViewRenderer` forwards refs so `ListViewHandle` still works through the registry. 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 d3635a5ed1..d5f3e90cd6 100644 --- a/apps/console/src/__tests__/public-block-binding-reach.test.tsx +++ b/apps/console/src/__tests__/public-block-binding-reach.test.tsx @@ -105,31 +105,21 @@ const NO_DATA_REACH: Readonly> = { 'record:related_list': 'needs the parent record id from RecordContext before it may fetch; declines to fetch without one (objectstack#4413 ledger)', - // Both of these are the SAME defect, and it is a real one — debt recorded - // here, not divergence accepted. Neither registration bridges the - // schema-renderer context onto the component's `dataSource` PROP: - // `object-form`, `object-kanban` and `object-calendar` each register a small - // renderer that does exactly that, `list-view` is registered as the bare - // `ListView` (which reads `props.dataSource`), and `embeddable-form`'s - // renderer is `({schema}) => `, which drops - // it. `SchemaRenderer` never injects `dataSource` into props, so on the - // registry/SDUI path both render an empty shell while declaring `objectName` - // **required** — the objectstack#4413 shape, one layer up. + // `list-view` and `embeddable-form` were the other two entries here for + // exactly one release of this file. Neither registration bridged the + // schema-renderer context onto the component's `dataSource` PROP — the bridge + // `object-form`, `object-kanban` and `object-calendar` always had — and + // `SchemaRenderer` never injects it, so on the registry/SDUI path both + // rendered an empty shell while declaring `objectName` **required**: the + // objectstack#4413 shape, one layer up. // - // Verified to be the wiring and not this probe's reach: `embeddable-form` - // fetches the moment the bridge exists (its inner `ObjectForm` calls - // `getObjectSchema` through the read-only source it derives), and does not - // without it, on the identical mount. - // - // Filed rather than fixed alongside this suite: giving these two a data source - // changes what they render everywhere they are mounted bare, which wants its - // own review — objectui#3144. When it lands, the assertions below FORCE these - // two entries deleted; a ledger nobody must update is how an accepted baseline - // starts. - 'list-view': - 'registered bare; ListView reads props.dataSource and SchemaRenderer never injects it — objectui#3144', - 'embeddable-form': - 'renderer drops the context dataSource (``) — objectui#3144', + // They are gone because #3144 fixed the wiring, and they are gone the only way + // an entry here can go: the assertions below stopped passing the moment those + // two blocks started reaching the data layer, and deleting the entries was the + // way to get green again. That is the whole point of the both-directions + // check — a ledger nobody is FORCED to update is how an accepted baseline + // starts, and an accepted baseline reporting zero divergence is what let + // objectstack#4413 ship. }; /** Does this config declare an `objectName` input? */ diff --git a/packages/plugin-form/src/index.tsx b/packages/plugin-form/src/index.tsx index 293aa3537e..beefe3a968 100644 --- a/packages/plugin-form/src/index.tsx +++ b/packages/plugin-form/src/index.tsx @@ -118,7 +118,16 @@ ComponentRegistry.register('form', ObjectFormRenderer, { import { EmbeddableForm } from './EmbeddableForm'; const EmbeddableFormRenderer: React.FC<{ schema: any }> = ({ schema }) => { - return ; + // Same bridge `object-form` above does, and for the same reason (#3144): + // `EmbeddableForm` needs a dataSource to fetch the object schema — its own + // comment says so — and `SchemaRenderer` only ever puts one on the context, + // never on props. Dropping it here meant an `embeddable-form` rendered + // through the registry declared `objectName` **required** and then had + // nothing to bind it to, rendering a field-less shell. That is the + // objectstack#4413 shape; `public-block-binding-reach.test.tsx` is what + // catches it now. + const ctx = useContext(SchemaRendererContext as React.Context); + return ; }; ComponentRegistry.register('embeddable-form', EmbeddableFormRenderer, { diff --git a/packages/plugin-list/src/index.tsx b/packages/plugin-list/src/index.tsx index b0f70ac3c3..5fad2198d2 100644 --- a/packages/plugin-list/src/index.tsx +++ b/packages/plugin-list/src/index.tsx @@ -6,8 +6,10 @@ * LICENSE file in the root directory of this source tree. */ +import React, { useContext } from 'react'; import { ComponentRegistry } from '@object-ui/core'; -import { ListView } from './ListView'; +import { SchemaRendererContext } from '@object-ui/react'; +import { ListView, type ListViewHandle, type ListViewProps } from './ListView'; import { ViewSwitcher } from './ViewSwitcher'; import { ObjectGallery } from './ObjectGallery'; @@ -22,8 +24,38 @@ export type { ListViewProps, ListViewHandle } from './ListView'; export type { ObjectGalleryProps } from './ObjectGallery'; export type { ViewSwitcherProps, ViewType } from './ViewSwitcher'; +/** + * Registry entry point for `` — bridges the schema-renderer context + * onto the component's `dataSource` PROP (#3144). + * + * `ListView` reads `props.dataSource`, and `SchemaRenderer` never injects it — + * it only puts the data source on `SchemaRendererContext`. So registering the + * component bare meant that on the registry/SDUI path (a metadata page, a + * `kind:'react'` page, the designer preview) it received no data source at all: + * its `getObjectSchema` effect returned immediately, nothing was ever fetched, + * and it rendered the "Nothing here" empty state — while its registry `inputs` + * declared `objectName` **required**. The app never showed this because the + * console reaches ListView through `ObjectView`'s `renderListView` render-prop, + * which passes a data source itself. + * + * That is the objectstack#4413 shape (a declared binding nothing can consume), + * caught this time by `apps/console/src/__tests__/public-block-binding-reach.test.tsx` + * rather than by hand. `object-form`, `object-kanban` and `object-calendar` have + * carried this same one-line bridge all along. + * + * An explicit `dataSource` prop still wins — hosts that pass their own (as + * `ObjectView` does) are unaffected. `useContext` rather than + * `useSchemaContext()` because the latter throws outside a provider, and a bare + * `` with a host-supplied data source must keep working. + */ +const ListViewRenderer = React.forwardRef((props, ref) => { + const context = useContext(SchemaRendererContext as React.Context); + return ; +}); +ListViewRenderer.displayName = 'ListViewRenderer'; + // Register ListView component -ComponentRegistry.register('list-view', ListView, { +ComponentRegistry.register('list-view', ListViewRenderer, { namespace: 'plugin-list', label: 'List View', category: 'Views', @@ -63,7 +95,7 @@ ComponentRegistry.register('list-view', ListView, { // data-bound ListView (which requires `objectName`) instead. Object list VIEWS // are rendered via `type: 'list-view'`, never the bare `list` lookup, so the // data view loses nothing by yielding the bare key. -ComponentRegistry.register('list', ListView, { +ComponentRegistry.register('list', ListViewRenderer, { namespace: 'view', skipFallback: true, category: 'view',