Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/list-view-embeddable-form-datasource-bridge.md
Original file line number Diff line number Diff line change
@@ -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 }) => <EmbeddableForm config={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.
38 changes: 14 additions & 24 deletions apps/console/src/__tests__/public-block-binding-reach.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,31 +105,21 @@ const NO_DATA_REACH: Readonly<Record<string, string>> = {
'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}) => <EmbeddableForm config={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 (`<EmbeddableForm config={schema} />`) — 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? */
Expand Down
11 changes: 10 additions & 1 deletion packages/plugin-form/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,16 @@ ComponentRegistry.register('form', ObjectFormRenderer, {
import { EmbeddableForm } from './EmbeddableForm';

const EmbeddableFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
return <EmbeddableForm config={schema} />;
// 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<any>);
return <EmbeddableForm config={schema} dataSource={ctx?.dataSource} />;
};

ComponentRegistry.register('embeddable-form', EmbeddableFormRenderer, {
Expand Down
38 changes: 35 additions & 3 deletions packages/plugin-list/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 `<ListView>` — 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
* `<ListView>` with a host-supplied data source must keep working.
*/
const ListViewRenderer = React.forwardRef<ListViewHandle, ListViewProps>((props, ref) => {
const context = useContext(SchemaRendererContext as React.Context<any>);
return <ListView ref={ref} {...props} dataSource={props.dataSource ?? context?.dataSource} />;
});
ListViewRenderer.displayName = 'ListViewRenderer';

// Register ListView component
ComponentRegistry.register('list-view', ListView, {
ComponentRegistry.register('list-view', ListViewRenderer, {
namespace: 'plugin-list',
label: 'List View',
category: 'Views',
Expand Down Expand Up @@ -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',
Expand Down
Loading