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',