Skip to content

fix(plugin-form): decline to fetch a line-items child schema with no childObject - #6196

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-6188-line-items-decline-to-fetch
Aug 25, 2026
Merged

fix(plugin-form): decline to fetch a line-items child schema with no childObject#6196
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-6188-line-items-decline-to-fetch

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Fixes#6188

The measurement this card was for, first

The card explicitly did not verify reachability. Both halves are now measured, and the answer is the stronger reading, not the weaker one:

Is childObject declared a required input for record:line_items? Yes — packages/plugin-form/src/index.tsx:404, { name: 'childObject', type: 'string', label: 'Child Object', required: true }.

Is that declaration enforced anywhere? No. inputs[].required is designer metadata: WidgetRegistry.ts:180 copies it onto the ComponentRegistry entry and nothing parses an authored node against it. Grepped for every consumer of .required under packages/core/src — the hits are field-level rules (evaluator/fieldRules.ts), the record-level validators, and the schema builder; none of them sees a block's declared inputs.

Does the spec's RecordLineItemsProps make it required? There is no RecordLineItemsProps. Grep over @objectstack/spec@17.2.0 (src and dist) and over this whole repo returns zero hits. The spec names this block exactly once, as an example of the opposite: "type is an open union (unregistered types like record:line_items are authored in the wild)" (src/ui/component.zod.ts:105). So the block has no spec schema at all, and the registry's required: true is the only place the requirement is written down.

Does a bare mount reproduce the call? Yes — through the registered authoring surface, not only through direct component use. packages/plugin-form/src/LineItemsPanel.childObjectDecline.test.tsx renders a plain authored node through SchemaRenderer (so it goes through ComponentRegistry and the ElementDataSourceGate wrapper, the real path), with childObject simply absent. Recorded calls, by first argument, against unmodified origin/main @ b04646b:

['getObjectSchema(undefined)', 'find(undefined)']

So the severity claim stands at its stronger reading: an author who omits childObject triggers this. Nothing between the authored JSON and the effect rejects the node — the key is declared required in a place that only a designer UI reads.

The fix

LineItemsPanel's child-schema effect guarded the data source (ds, and that getObjectSchema is callable) but never the argument. It now declines and warns.

Following PR #6191 (card #5940), which landed the same decision on the same key in this package, with both of its deliberate choices checked against this component rather than copied:

  1. fix(plugin-form): decline to fetch a detail collection with no child object #6191 returns the entry rather than dropping it, because the surrounding array is index-matched against row state. That coupling does not exist here — there is no array. LineItemsPanel is one panel with one childSchema state, so the analogous question is what that state should hold when the fetch is declined, and the answer is null: an unresolvable panel has no child schema, and leaving a previous object's schema cached would sanitize a later save against the wrong object's fields. null is also what the effect's own .catch already produces, so the sanitize path needs no new case.
  2. The warning names the key and says what to set it to"a line-items panel has no childObject — refusing to fetch its child schema. Set childObject to the child object the panel lists." — rather than reporting that something was undefined.

The precedent it joins: RelatedList declines when it cannot scope ("has no referenceField/parentId — refusing to fetch all rows", RelatedList.tsx:498), MasterDetailForm's child-schema cache has always spelled it .filter(Boolean), and after #6191 its detail-resolve effect declines on this exact key.

Why the test asserts the call list

The symptom here is quieter than #5940's, and that shaped the test. The .catch turns whatever a real backend returns for an object named undefined into a null child schema — and a null child schema is exactly what the panel holds before any fetch resolves. So "it did not crash" and "the schema is null" were both true while the defect was live. No assertion on resulting state could have caught this. Only the calls distinguish the two worlds.

Both directions are pinned, because a guard that declined everything would also make the bad call vanish and would pass an absence-only assertion:

fixturerecorded calls (by first argument)
childObject unset — before['getObjectSchema(undefined)', 'find(undefined)']
childObject unset — after['find(undefined)'] (see below)
childObject: 'invoice_line' — after['getObjectSchema("invoice_line")', 'find("invoice_line")']

The recorder deliberately captures method(firstArgument) rather than the full argument list: the child fetch also carries $filter / $top, and pinning those would make the file fail for changes that have nothing to do with the object name.

The entry still in that list — a further site, filed not fixed

find(undefined) is the sibling site in this same component: load() guards dataSource and parentId but not schema.childObject, so the row fetch still queries an object literally named undefined. It is out of this card's scope by dispatch order, which scoped this to the getObjectSchema call and said to file any further unguarded sub-key site. Filed as #6194, which stays open — it is not addressed here.

It is pinned in the test as current behaviour, with a comment pointing at #6194 and saying what to change the line to, rather than hidden behind a narrower fixture that omits parentId. Hiding it would have made this PR's test read as if the component were clean.

That card is not a one-line rider, which is the other reason it is separate: declining a schema fetch has no visible consequence, but declining the row fetch does — load owns loading, and the render branches loading ? … : !parentId ? … : grid, so an unresolvable panel with a parent id bound would land on the grid branch and show an empty editable grid over an object that does not exist. Whether that wants its own config-hint branch is a question, not a mechanical fix.

Lint note

The new setChildSchema(null) raises one react-hooks/set-state-in-effectwarning. It is the same warning the effect below it already carries (void load(), line 203), the rule is configured 'warn' repo-wide (eslint.config.js:95), and lint.yml deliberately does not set --max-warnings ("warnings repo-wide run into the…", line 20). Package lint is 0 errors.

Verification — all at fd7525d9b, the commit this PR ships

Exit codes captured by redirect before any pipe. Heavy runs went through the shared verify lock.

gatecommandresult
targeted test, BEFORE the guardpnpm exec vitest run packages/plugin-form/src/LineItemsPanel.childObjectDecline.test.tsxTests 1 failed | 1 passed (2) — reproduced, exactly as predicted
testspnpm exec vitest run packages/plugin-form/ apps/console/src/__tests__/record-block-record-reach.test.tsx apps/console/src/__tests__/public-block-binding-reach.test.tsx apps/console/src/__tests__/public-contract.test.tsTest Files 64 passed (64) / Tests 655 passed (655)
type-checkpnpm exec turbo run type-check --filter=@object-ui/plugin-form --concurrency=2Tasks: 13 successful, 13 total (includes the dependency build and tsconfig.test.json)
linteslint . in packages/plugin-form641 problems (0 errors, 641 warnings)
control bytesnode scripts/check-control-bytes.mjsOK (scanned 5112 tracked text file(s))
changesetpnpm run changeset:checkboth checks OK

How the gate scope was derived — from each workflow's own configuration, not assumed. ci.yml and lint.yml carry paths-ignore for **/*.md / content / docs / .changeset only, so a packages/** diff runs both in full; control-bytes.yml and changeset-presence.yml deliberately carry no path filter at all; changeset-guard.yml filters to .changeset/**, which this PR touches. performance-budget.yml (Bundle Analysis) filters on packages/** and so applies — it is a full turbo build plus a console build, a repo-level run CI owns; not reproduced locally.

Why the lint narrowing is a measurement, not an omission: the universe comes from eslint's own resolution (eslint . from the package directory, which is what turbo run lint invokes for this package — packages/plugin-form/package.json"lint": "eslint ."), and the root eslint.config.js declares no project / projectService, so type-aware linting is off and this diff cannot move the verdict on any file it does not touch. Files outside packages/plugin-form are therefore invariant under this change.

Docs are untouched, matching #6191: this is a guard on an existing read path, not a change to the authorable surface any document describes.

Generated by Claude Code


Generated by Claude Code

…childObject
`record:line_items` read `schema.childObject` and handed it to
`getObjectSchema` with a guard on the data source but none on the argument, so
a panel authored without the key asked the backend for an object literally
named `undefined`. The effect's `.catch` turned the answer into a null child
schema, so the visible outcome was a silently unsanitized child grid rather
than an error — which is why no assertion on resulting state could have caught
it and the new test asserts the call list instead.
The effect now declines and warns, naming the key and what to set it to, and
clears the cached child schema so a later save is never sanitized against a
previous object's fields. Matches the choice `RelatedList` makes for the same
class of missing key and the one `object-master-detail-form` makes on this
exact key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.3 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-BjrAvkRG.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
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)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
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)505.15KB114.53KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)168.48KB46.47KB
fields (index.js)238.40KB59.89KB
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.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
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.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.14KB61.94KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.87KB30.71KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.05KB54.38KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
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)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)2.74KB1.41KB
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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] record:line_items issues getObjectSchema(undefined) for a panel whose childObject is unset, instead of declining to fetch

2 participants

@yinlianghui-tw@yinlianghui