Skip to content

Commit 5eaa861

Browse files
os-zhuangclaude
andauthored
fix(plugin-list,plugin-form): 在注册表路径上把 dataSource 接到 list-view / embeddable-form (#3144) (#3147)
`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 }) => <EmbeddableForm config={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 Claude-Session: https://claude.ai/code/session_01S3cP1eY1novcNhQEDBrSZD Co-authored-by: Claude <noreply@anthropic.com>
1 parent cc70b8f commit 5eaa861

4 files changed

Lines changed: 94 additions & 28 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@object-ui/plugin-list": patch
3+
"@object-ui/plugin-form": patch
4+
---
5+
6+
`list-view` and `embeddable-form` get a data source on the registry path — their required `objectName` was binding to nothing (#3144).
7+
8+
`SchemaRenderer` puts the data source on `SchemaRendererContext` and **never** injects it into
9+
component props. A component that reads `props.dataSource` therefore needs its registration to
10+
bridge the two. `object-form`, `object-kanban` and `object-calendar` each register a small
11+
renderer that does exactly that. These two did not:
12+
13+
- `list-view` (and its `view:list` alias) registered the bare `ListView`, which reads
14+
`props.dataSource` — so its `getObjectSchema` effect returned immediately, nothing was ever
15+
fetched, and it rendered the `empty-state` "Nothing here".
16+
- `embeddable-form`'s renderer was `({ schema }) => <EmbeddableForm config={schema} />`, dropping
17+
the context entirely — so the read-only source it derives for its inner `ObjectForm` was never
18+
built, and its submit path (`if (dataSource) await dataSource.create(...)`) had nothing to call.
19+
20+
Both declare `objectName` **required** in their registry `inputs`. A binding the protocol obliges
21+
an author to supply, that nothing on that path can consume, is objectstack#4413's shape one layer
22+
up — and the reason it went unnoticed is that the console never takes this path: it reaches
23+
ListView through `ObjectView`'s `renderListView` render-prop, which passes a data source itself.
24+
Broken on the registry/SDUI path, which is the path `sdui.manifest.json` describes and a
25+
`kind:'react'` page walks.
26+
27+
Found by `apps/console/src/__tests__/public-block-binding-reach.test.tsx` (objectstack#4472), not
28+
by hand — that suite mounts every public block declaring an `objectName` under a recording
29+
`dataSource` and asserts the binding arrives. Its ledger carried these two as named debt; with the
30+
bridge in place the ledger's both-directions assertion **failed until the entries were deleted**,
31+
which is the mechanism working as designed. Only `record:related_list` remains, and legitimately
32+
(it needs a parent record id from `RecordContext` before it may fetch).
33+
34+
An explicit `dataSource` prop still wins, so hosts passing their own are unaffected, and
35+
`ListViewRenderer` forwards refs so `ListViewHandle` still works through the registry.

apps/console/src/__tests__/public-block-binding-reach.test.tsx

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -105,31 +105,21 @@ const NO_DATA_REACH: Readonly<Record<string, string>> = {
105105
'record:related_list':
106106
'needs the parent record id from RecordContext before it may fetch; declines to fetch without one (objectstack#4413 ledger)',
107107

108-
// Both of these are the SAME defect, and it is a real one — debt recorded
109-
// here, not divergence accepted. Neither registration bridges the
110-
// schema-renderer context onto the component's `dataSource` PROP:
111-
// `object-form`, `object-kanban` and `object-calendar` each register a small
112-
// renderer that does exactly that, `list-view` is registered as the bare
113-
// `ListView` (which reads `props.dataSource`), and `embeddable-form`'s
114-
// renderer is `({schema}) => <EmbeddableForm config={schema} />`, which drops
115-
// it. `SchemaRenderer` never injects `dataSource` into props, so on the
116-
// registry/SDUI path both render an empty shell while declaring `objectName`
117-
// **required** — the objectstack#4413 shape, one layer up.
108+
// `list-view` and `embeddable-form` were the other two entries here for
109+
// exactly one release of this file. Neither registration bridged the
110+
// schema-renderer context onto the component's `dataSource` PROP — the bridge
111+
// `object-form`, `object-kanban` and `object-calendar` always had — and
112+
// `SchemaRenderer` never injects it, so on the registry/SDUI path both
113+
// rendered an empty shell while declaring `objectName` **required**: the
114+
// objectstack#4413 shape, one layer up.
118115
//
119-
// Verified to be the wiring and not this probe's reach: `embeddable-form`
120-
// fetches the moment the bridge exists (its inner `ObjectForm` calls
121-
// `getObjectSchema` through the read-only source it derives), and does not
122-
// without it, on the identical mount.
123-
//
124-
// Filed rather than fixed alongside this suite: giving these two a data source
125-
// changes what they render everywhere they are mounted bare, which wants its
126-
// own review — objectui#3144. When it lands, the assertions below FORCE these
127-
// two entries deleted; a ledger nobody must update is how an accepted baseline
128-
// starts.
129-
'list-view':
130-
'registered bare; ListView reads props.dataSource and SchemaRenderer never injects it — objectui#3144',
131-
'embeddable-form':
132-
'renderer drops the context dataSource (`<EmbeddableForm config={schema} />`) — objectui#3144',
116+
// They are gone because #3144 fixed the wiring, and they are gone the only way
117+
// an entry here can go: the assertions below stopped passing the moment those
118+
// two blocks started reaching the data layer, and deleting the entries was the
119+
// way to get green again. That is the whole point of the both-directions
120+
// check — a ledger nobody is FORCED to update is how an accepted baseline
121+
// starts, and an accepted baseline reporting zero divergence is what let
122+
// objectstack#4413 ship.
133123
};
134124

135125
/** Does this config declare an `objectName` input? */

packages/plugin-form/src/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,16 @@ ComponentRegistry.register('form', ObjectFormRenderer, {
118118
import { EmbeddableForm } from './EmbeddableForm';
119119

120120
const EmbeddableFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
121-
return <EmbeddableForm config={schema} />;
121+
// Same bridge `object-form` above does, and for the same reason (#3144):
122+
// `EmbeddableForm` needs a dataSource to fetch the object schema — its own
123+
// comment says so — and `SchemaRenderer` only ever puts one on the context,
124+
// never on props. Dropping it here meant an `embeddable-form` rendered
125+
// through the registry declared `objectName` **required** and then had
126+
// nothing to bind it to, rendering a field-less shell. That is the
127+
// objectstack#4413 shape; `public-block-binding-reach.test.tsx` is what
128+
// catches it now.
129+
const ctx = useContext(SchemaRendererContext as React.Context<any>);
130+
return <EmbeddableForm config={schema} dataSource={ctx?.dataSource} />;
122131
};
123132

124133
ComponentRegistry.register('embeddable-form', EmbeddableFormRenderer, {

packages/plugin-list/src/index.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
import React, { useContext } from 'react';
910
import { ComponentRegistry } from '@object-ui/core';
10-
import { ListView } from './ListView';
11+
import { SchemaRendererContext } from '@object-ui/react';
12+
import { ListView, type ListViewHandle, type ListViewProps } from './ListView';
1113
import { ViewSwitcher } from './ViewSwitcher';
1214
import { ObjectGallery } from './ObjectGallery';
1315

@@ -22,8 +24,38 @@ export type { ListViewProps, ListViewHandle } from './ListView';
2224
export type { ObjectGalleryProps } from './ObjectGallery';
2325
export type { ViewSwitcherProps, ViewType } from './ViewSwitcher';
2426

27+
/**
28+
* Registry entry point for `<ListView>` — bridges the schema-renderer context
29+
* onto the component's `dataSource` PROP (#3144).
30+
*
31+
* `ListView` reads `props.dataSource`, and `SchemaRenderer` never injects it —
32+
* it only puts the data source on `SchemaRendererContext`. So registering the
33+
* component bare meant that on the registry/SDUI path (a metadata page, a
34+
* `kind:'react'` page, the designer preview) it received no data source at all:
35+
* its `getObjectSchema` effect returned immediately, nothing was ever fetched,
36+
* and it rendered the "Nothing here" empty state — while its registry `inputs`
37+
* declared `objectName` **required**. The app never showed this because the
38+
* console reaches ListView through `ObjectView`'s `renderListView` render-prop,
39+
* which passes a data source itself.
40+
*
41+
* That is the objectstack#4413 shape (a declared binding nothing can consume),
42+
* caught this time by `apps/console/src/__tests__/public-block-binding-reach.test.tsx`
43+
* rather than by hand. `object-form`, `object-kanban` and `object-calendar` have
44+
* carried this same one-line bridge all along.
45+
*
46+
* An explicit `dataSource` prop still wins — hosts that pass their own (as
47+
* `ObjectView` does) are unaffected. `useContext` rather than
48+
* `useSchemaContext()` because the latter throws outside a provider, and a bare
49+
* `<ListView>` with a host-supplied data source must keep working.
50+
*/
51+
const ListViewRenderer = React.forwardRef<ListViewHandle, ListViewProps>((props, ref) => {
52+
const context = useContext(SchemaRendererContext as React.Context<any>);
53+
return <ListView ref={ref} {...props} dataSource={props.dataSource ?? context?.dataSource} />;
54+
});
55+
ListViewRenderer.displayName = 'ListViewRenderer';
56+
2557
// Register ListView component
26-
ComponentRegistry.register('list-view', ListView, {
58+
ComponentRegistry.register('list-view', ListViewRenderer, {
2759
namespace: 'plugin-list',
2860
label: 'List View',
2961
category: 'Views',
@@ -63,7 +95,7 @@ ComponentRegistry.register('list-view', ListView, {
6395
// data-bound ListView (which requires `objectName`) instead. Object list VIEWS
6496
// are rendered via `type: 'list-view'`, never the bare `list` lookup, so the
6597
// data view loses nothing by yielding the bare key.
66-
ComponentRegistry.register('list', ListView, {
98+
ComponentRegistry.register('list', ListViewRenderer, {
6799
namespace: 'view',
68100
skipFallback: true,
69101
category: 'view',

0 commit comments

Comments
 (0)