Skip to content

fix(react,plugin-detail,plugin-grid,plugin-form,docs): one data-source resolution rule for the object-bound family, and a loud report when none resolves - #5447

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5378-datasource-wiring-convergence
Aug 20, 2026
Merged

fix(react,plugin-detail,plugin-grid,plugin-form,docs): one data-source resolution rule for the object-bound family, and a loud report when none resolves#5447
os-support-ai merged 1 commit into
mainfrom
claude/issue-5378-datasource-wiring-convergence

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5378
Fixes#5377

Two independent axes broke the same five snippets in content/docs/guide/building-crud-app.md, and a third turned up while measuring. Landing any one of them alone leaves the page rendering nothing while the PR reports success — which is exactly what #5378 was split out to prevent — so they are folded into one branch.

What was measured first (baseline, origin/main @ 32ef595f4)

Every snippet rendered through the real registry with a fake adapter. find / findOne call counts, and whether the row text appears:

snippet, as the guide publishes itprovider wiringprop wiring
Step 5 / 6 / 7 object-gridfind0, nothingfind0, nothing
Step 6 object-formfindOne0, nothingfindOne0, nothing
Step 8 detail-viewfindOne0, nothingfindOne0, nothing

All five dead in both directions. With the keys corrected, the split #5378 reports appears:

keys corrected, data droppedprovider wiringprop wiring
object-gridfind1, row rendersfind0
detail-viewfindOne0findOne1, row renders

Two mutually exclusive wirings. Neither reported anything: no error, no warning, no empty state that explained itself.

1. Converge the wiring (ruling item 1)

detail-view was registered as the raw DetailView, which reads a React dataSourceprop. Its siblings are registered through wrappers that read SchemaRendererContext. SchemaRenderer itself reads only context, so a prop never becomes context for anything below it.

All three blocks now resolve the adapter through one shared hook, useResolvedDataSource (new, @object-ui/react): an explicit dataSource prop first, the provider context second.

Precedence, stated and pinned: the explicit prop wins. It is the more specific signal — written on one placement, by someone who can see it — while the context adapter is ambient over every descendant. It is also the only precedence under which the ruling's "the prop form stays accepted" is true of the case that matters: a prop-form caller mounted inside an app that happens to have a provider would otherwise have its explicit choice silently overruled by an ancestor it never wrote.

Additive, as ruled — no prop removed this pass. Three wirings are asserted for detail-view: context alone (new), prop alone (must not break), and both at once (prop wins).

The compatibility surface, measured (the triage confidence gap)

Triage flagged that nobody had sized the existing detail-view prop-form caller population. Measured in this repo:

  • packages/plugin-detail/src/renderers/record-details.tsx and packages/plugin-detail/src/RecordDetailDrawer.tsx both render the component directly, never through the registry. The published README documents the same direct form. The new wrapper does not sit between any of them and DetailView.
  • Registry-routed detail-view nodes carrying a dataSource prop: zero, the getting-started guide being the one that tried.

So the additive wrapper changes no measured caller's behaviour and creates no ambiguous precedence. No fork to report — but the precedence is pinned by test rather than left to be discovered.

Two side effects of the convergence, both strictly widening:

  • object-grid used to call useSchemaContext(), which throws without a provider — the || {} beside it could never catch it. So the exact case this card is about surfaced as the error boundary saying Component "object-grid" failed to render over a React hook message naming neither the data source nor the fix. It now reads the context.
  • object-form gains a dataSource prop it did not have. It read context only, so nothing that worked before resolves differently.

2. The silence dies with it (ruling item 2)

A block in the family that resolves no adapter now renders a No data source resolved panel — role="alert", same shape as the existing unresolvable-view panel — naming the block, the object it was about to read, and the ancestor that injects the adapter. An address, not a symptom.

The check is opt-in per block (requiresDataSource on ElementDataSourceGate). The gate cannot compute "needs an adapter" and does not try: every block has different escape hatches, and a predicate guessed in the shared gate would paint a configuration error over a block that is working — a worse failure than the silence it replaces. Each call site states it, enumerating its own fallbacks:

  • object-grid: inline rows (data array, provider: 'value', legacy staticData), bind, or a host that owns the fetch and passes the window as a data React prop (plugin-list's ListView). Tested with Array.isArray, which is the exact test ObjectGrid applies — a bare 'data' in props would also be true of the schema's own data object, because SchemaRenderer spreads every unstripped schema key as a prop.
  • object-form: inline customFields, which is what hasInlineFields gates on inside the component.
  • detail-view: an inline data record, or an api endpoint it fetches without an adapter.

The no-adapter answer comes before the unresolvable-view answer: with no adapter the view cannot resolve either, so the block would otherwise report "this data source cannot list the saved views" — true, and pointing at the view name instead of at the missing injection.

The other eight blocks using this gate are untouched; the flag defaults off.

3. The guide (ruling item 3, plus #5377's key axis)

  • Wiring — one SchemaRendererProvider at the top of the App, per AGENTS.md commandment Implement visual designer for Object UI schemas #1. No SchemaRenderer in the page carries a dataSource prop any more; a test asserts that over the file's text.
  • Keysobject becomes objectName in all five snippets.
  • recordId becomes resourceId on detail-view ONLY.object-form genuinely reads recordId (ObjectForm.tsx calls findOne(schema.objectName, schema.recordId)), so the blanket rename that "fix the keys" reads like from a distance would have taken a working block to broken. Both directions are pinned: the form probe asserts the resourceId spelling loads no record there, the detail probe asserts recordId loads none there.
  • data removed from the detail-view snippet only. Per-snippet triage, not a sweep: on detail-view, data means "here is the record already, do not fetch"DetailView returns early on any schema.data, so the object's metadata was being installed as the record and findOne never ran (measured findOne0 with it, 1 without). On object-grid and object-form the same key is measured inert (find 1 either way), so it stays and is filed instead.

A third axis, found while measuring — and fixed here with evidence

The guide's setup.ts loaded @object-ui/components and @object-ui/fields only. initializeComponents() registers neither the plugins nor anything else beyond its own package, and Step 1 never installed @object-ui/plugin-detail at all. Measured by rendering the three keys against exactly the guide's registration set:

REG|object-grid|text="Unknown component type: object-grid…"
REG|object-form|text="Unknown component type: object-form…"
REG|detail-view|text="Unknown component type: detail-view…"

All five snippets resolved to the OBJUI-001 panel for anyone who followed the page literally, so no amount of wiring or key fixing could have made it render. Fixed in place rather than filed because the correct form is pinned by the page's own idiom — Step 2 already writes import '@object-ui/fields'; and explains why — and because a render probe over "the guide's snippets" that registered the plugins itself while the guide did not would have been measuring something the reader cannot reproduce.

Acceptance: a render probe, not a diff review

Three probe files (packages/plugin-{grid,form,detail}/src/__tests__/guideCrudAppRenders.test.tsx) evaluate the guide's own schema literals off disk — balanced-brace extraction plus Function, with the identifiers the page closes over bound — and render them through the real registry. A transcription kept in the tests would drift from the page silently, which is this defect's whole failure mode. Each file asserts its snippet count first, so a lost snippet cannot make the file vacuous.

Per snippet, after both axes: adapter call count 0 → 1 and the row text appears.

snippetbeforeafter
Step 5 object-gridfind 0find 1, ["task", …], row painted
Step 6 object-gridfind 0find 1, row painted
Step 7 object-gridfind 0find 1, row painted
Step 6 object-formgetObjectSchema 0 / findOne 0getObjectSchema('task'), findOne('task','42'), fields painted
Step 8 detail-viewfindOne 0findOne('task','42'), record painted

Each axis is also pinned as a standing contrast, so reintroducing one fails here rather than on a reader's screen: re-spell objectName back to object and the block fetches nothing; put recordId on the detail view and it loads nothing; hand the detail view a data and findOne never runs.

Reverse verification — predicted, then observed

No build artifact sits between any edit and the thing under test: the root vitest config aliases every @object-ui/* specifier to packages/*/src, and each probe imports its own plugin as ../index. Leg A proves it rather than asserting it — it edits a registration that a different file's assertions depend on, and a stale dist/ in the path would have left it green.

legpredictedobserved
A — register the raw DetailView again4 red2 red: the provider-wiring test and the panel-presence test
B — drop requiresDataSource from the grid1 red1 red, exactly the panel test
C — put object: back in the guide's Step 51 red2 red

Leg A is the interesting miss, and it is about my own tests rather than the code: two of the three no-adapter tests are absence assertions (a placement with inline rows must NOT get the panel), and a build with no wrapper satisfies those trivially. They are false-positive guards, not feature proofs, and only the presence assertion can fail when the feature is removed. Leg C under-counted for the same kind of reason — the both-wirings test reads the same snippet, so it dies with it.

A cross-package type check was reverse-verified too: requiresDataSource="yes-please" in packages/plugin-grid draws TS2322: Type 'string' is not assignable to type 'boolean | undefined', which proves the new prop reached packages/react/dist/index.d.ts and that plugin-grid's tsc is reading the rebuilt declarations, not a cached copy. (It first failed for the opposite reason — a stale dist gave TS2558: Expected 0 type arguments after useResolvedDataSource became generic.)

Out of scope, filed

Filed as #5446: Step 7 documents a view switcher and a search box that object-grid never reads — top-level view has zero read points, and data: { objectSchema, queryParams } is none of ViewData's four provider arms. Measured inert (identical find params with and without both keys). Left alone here because two legitimate correct forms exist — move it into the spec dataSource: { object, view } binding, or spell the view's declared filter / sort on the block — and choosing between them changes what the page teaches and, for the first option, what a reader sees when their backend has no such saved view. That is a decision, not a mechanical fix.

Verification

Gate union re-derived from the actual three-dot changed paths after the final commit, and run at 55c49cbb1:

  • pnpm exec vitest run packages/plugin-detail/ packages/plugin-form/ packages/react/ — 193 files, 2074 passed
  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/ — 124 files, 1379 passed (plugin-list included deliberately: it is the host that passes the grid's rows as a React prop)
  • pnpm exec vitest run packages/app-shell/ — 456 files, 4410 passed, 1 skipped (the heaviest in-repo consumer of all three blocks)
  • turbo run type-check across the workspace — 81/81, the downstream-consumer direction for the new @object-ui/react exports
  • turbo run lint on the four changed packages — 0 errors
  • check:doc-types, check:doc-snippets (with the packages its --build-filter names built), check-doc-links, check:control-bytes, check:phantom-deps, check:self-import, check-lint-coverage, check-type-check-coverage, check:skills-paths, check:spec-symbols, check:action-forward-parity, check:i18n-keys, check:i18n-drift, check-changeset-no-major, check-changeset-fixed, check-changeset-presence — all green

Changeset: minor on the four packages (never major, per AGENTS.md §版本号策略).

Generated by Claude Code


Generated by Claude Code

…e resolution rule for the object-bound family, and a loud report when none resolves (#5378, #5377)
`object-grid` / `object-form` read the adapter from `SchemaRendererProvider`
context; `detail-view` was registered as the raw component and read a React
`dataSource` prop. `SchemaRenderer` reads only context, so the two wirings were
mutually exclusive — a page could satisfy one half of itself or the other.
All three now resolve through `useResolvedDataSource` (explicit prop first,
context second). Additive: `detail-view` keeps its prop form, direct
`<DetailView dataSource={…} />` callers are untouched, and `object-grid` no
longer throws out of `useSchemaContext()` when a page has no provider.
A block that resolves no adapter renders a `No data source resolved` panel
naming the block, the object and the ancestor that injects the adapter, instead
of an empty shell. Opt-in per block so inline rows / `customFields` / inline
records / `api` placements are untouched.
`content/docs/guide/building-crud-app.md` is rewritten to the provider wiring
with the declared keys, and its `setup.ts` now registers the three plugins it
renders — without which every snippet resolved to `Unknown component type`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation package: react plugin tests labels Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-fsURae6P.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.94KB113.61KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.21KB59.50KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.43KB32.92KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.04KB60.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.62KB53.21KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.64KB27.13KB
plugin-map (index.js)20.08KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)36.10KB12.26KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-ai
os-support-ai marked this pull request as ready for review August 20, 2026 21:17
@os-support-ai
os-support-ai added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit ebce5a3Aug 20, 2026
23 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5378-datasource-wiring-convergence branch August 20, 2026 21:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment