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 000000000..0c73ad1ac
--- /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 d3635a5ed..d5f3e90cd 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 293aa3537..beefe3a96 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 b0f70ac3c..5fad2198d 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',