Skip to content

fix(plugin-form): a detail that loaded but could not DERIVE gets a config hint naming relationshipField - #6407

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6394-master-detail-derive-config-hint
Aug 25, 2026
Merged

os-support-ai merged 1 commit into
mainfrom
claude/issue-6394-master-detail-derive-config-hint

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #6394

Per triage's ruling on that card: option 2 — a failed derive gets the config-hint placeholder naming relationshipField as the key to set, the objectui#5940 / objectui#6360 shape.

What changed

MasterDetailForm's detail resolver had a third arm still sitting on a permanent Loading columns…: the schema loaded fine and deriveDetail then threw, because no lookup/master_detail field on the child object references the parent. The catch returned the entry unresolved (return entry;), so it fell through to !d.columns?.length ? <p>Loading columns…</p> — and the derive is not retried, so those columns could never arrive.

The entry now carries status: 'underivable' and gets a render branch of its own:

Could not work out how po_line links to purchase_order: no lookup or master_detail field on it references the parent. Set relationshipField on this collection to the field that holds the parent record.

⛔ Deliberately not objectui#6372's refusal placeholder, which states the schema could not be loaded — triage's words, "dishonest copy for a schema that loaded fine." The thrown error is still logged with its stack (objectui#6372); the placeholder shows the author the key rather than the raw message.

MasterDetailLines gains one prop, parentObjectName, so the hint can name both ends of the relationship it could not find. Everything else in the file is untouched.

Three arms, three distinct outcomes

Arm Fixture Renders Card
no childObject [{ title: 'Unconfigured' }] md-detail-no-child-object objectui#5940 / objectui#6360
fetch threw getObjectSchema rejects md-detail-schema-unavailable objectui#6372
fetch resolved, derive threw child schema with no FK to the parent md-detail-no-relationship-field (new) this PR
still in flight never-settling promise Loading columns… (true here) control
resolves child schema with the FK the grid control

All five are asserted, so the new behaviour is attributable to the failed derive rather than to "this renderer shows a hint for every unresolved detail".

⭐ Pinned-test rewrite 1 of 2 — the one triage authorized

The ruling clause that moves it, quoted verbatim from the triage comment:

This ruling explicitly authorizes rewriting the one #6360 assertion in MasterDetailForm.detailChildObjectDecline.test.tsx whose premise the body measured as inaccurate (it pins a failed derive while titled as a pending fetch) — the pending case now has an honest control in MasterDetailForm.detailSchemaFetchFailure.test.tsx, so no coverage is lost. No other assertion in that file may be weakened; #6372's dispatch-order protection stands for everything else.

OLD, verbatim (packages/plugin-form/src/MasterDetailForm.detailChildObjectDecline.test.tsx):

  it('does NOT show the hint for a detail that names its child object, and still says "Loading columns…" while it resolves', async () => {
    // ⭐ The other direction. Without this, deleting the loading branch
    // altogether would pass the test above.
    const { view } = await renderDetails([{ childObject: 'invoice_line', title: 'Invoice lines' }]);

    expect(view.queryByTestId('md-detail-no-child-object')).toBeNull();
    expect(view.container.textContent).toContain('Loading columns…');

    view.unmount();
  });

NEW, verbatim:

  it('does NOT show the `childObject` hint for a detail that names its child object — whose fixture reaches the FAILED DERIVE, hinted at `relationshipField` (objectui#6394)', async () => {
    // ⭐ The other direction, unchanged in purpose: without this, a renderer
    // that showed the `childObject` hint for every unresolved detail would pass
    // the test above.
    //
    // ⚠️ This assertion's PREMISE was rewritten under objectui#6394's triage
    // ruling, which explicitly authorized moving this ONE objectui#6360
    // assertion (and no other in this file). Its title used to claim the detail
    // "still says `Loading columns…` while it resolves" — but this file's
    // recording data source resolves `getObjectSchema` to `[]`, so for
    // `invoice_line` the fetch SUCCEEDS and `deriveDetail` then throws: the
    // entry is not resolving at all, it is permanently underivable, and the
    // message it pinned could never end. The loading branch this believed it
    // was holding alive now has an honest control that drives a never-settling
    // promise (`MasterDetailForm.detailSchemaFetchFailure.test.tsx`, "DEGENERATE
    // CONTROL: a detail that is still IN FLIGHT keeps "Loading columns…""),
    // so naming what this fixture really hits loses no coverage.
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const { view } = await renderDetails([{ childObject: 'invoice_line', title: 'Invoice lines' }]);

    // The original claim, kept verbatim: the `!childObject` hint belongs to the
    // OTHER arm and must not appear for a detail that named its child object.
    expect(view.queryByTestId('md-detail-no-child-object')).toBeNull();

    // What this fixture actually reaches, now that the arm renders it: the
    // config hint naming the key to set, in place of a "Loading columns…" that
    // could never end (objectui#6394).
    const hint = view.getByTestId('md-detail-no-relationship-field');
    expect(hint.textContent).toContain('relationshipField');
    expect(hint.textContent).toContain('invoice_line');
    expect(view.container.textContent).not.toContain('Loading columns…');

    warn.mockRestore();
    view.unmount();
  });

The original direction it existed to pin — the !childObject hint must not appear for a detail that named its child object — is carried over unchanged and unweakened. What moved is only the outcome the fixture actually reaches. No other assertion in that file was touched; the file's describe-block prose that described this test as "holding the loading branch alive" was corrected in place, because it stated the same measured-false premise.

⭐ Pinned-test rewrite 2 of 2 — forced by the same ruling, disclosed under the same rule

Not named in the dispatch order, and disclosed here because the standing rule is about pinned tests, not about which file they live in. objectui#6372's own control in MasterDetailForm.detailSchemaFetchFailure.test.tsx — "DEGENERATE CONTROL: a fetch that SUCCEEDS but fails to DERIVE is not called a load failure" — also waited on Loading columns… for this arm. That wait is unfixably tied to the behaviour triage has just ruled out; the control's subject is unchanged (this is not a load failure) and is still asserted.

OLD, verbatim:

    await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalledWith('po_line'));
    await waitFor(() => expect(view.container.textContent).toContain('Loading columns…'));

    // NOT the load-failure placeholder: the load succeeded.
    expect(view.queryByTestId('md-detail-schema-unavailable')).toBeNull();

NEW, verbatim:

    await waitFor(() => expect(ds.getObjectSchema).toHaveBeenCalledWith('po_line'));
    // The derive failure's OWN placeholder (objectui#6394), naming
    // `relationshipField` — not the "Loading columns…" this waited on before
    // that card, and not the refusal below.
    await waitFor(() => expect(view.queryByTestId('md-detail-no-relationship-field')).not.toBeNull());
    expect(view.container.textContent).not.toContain('Loading columns…');

    // NOT the load-failure placeholder: the load succeeded.
    expect(view.queryByTestId('md-detail-schema-unavailable')).toBeNull();

The assertion is strengthened, not weakened: it went from "some spinner text is on screen" to "this exact placeholder is on screen, and the load-failure one is not". Its comment block was updated to say which card moved it. Everything else in that file — including objectui#6372's refusal assertions, its log assertion, and the never-settling-promise control — is untouched.

Ghost-assertion guard

Method: the fix was committed first, then only packages/plugin-form/src/MasterDetailForm.tsx was reverted to the pinned base commit faa863dce (the merge of PR #6393), leaving the new tests in place. The mutation was confirmed on disk by blob identity, not by an exit code:

hint markers now: 0        underivable now: 0        unresolved 'return entry;' back: 1
disk blob : b5358609f696724be56ed3c95d76c301b943dd6e
BASE blob : b5358609f696724be56ed3c95d76c301b943dd6e   ← identical: the tree really is base source

Restore was proved the same way (git checkout HEAD -- <file>, never a bare git checkout --): git diff HEAD empty, disk blob a9c54d1a… == HEAD blob a9c54d1a…. The runner carried a trap … EXIT INT TERM restore.

Against unmodified base source — Test Files 3 failed (3) / Tests 4 failed | 12 passed (16), os-verify-lock: VERDICT command-exit 1:

Test On base faa863dce On this branch
new: renders a config hint naming relationshipField … ❌ Unable to find an element by: [data-testid="md-detail-no-relationship-field"] ✅
new: still logs the derive error, carrying the thrown Error … ❌ expected null not to be null (anchored on the new placeholder, so it cannot read green without it) ✅
rewrite 1: decline file's authorized pin ❌ Unable to find an element by: [data-testid="md-detail-no-relationship-field"] ✅
rewrite 2: fetch-failure file's derive control ❌ expected null not to be null ✅
the 4 degenerate controls + objectui#6372/objectui#6360's other arms (12 tests) ✅ ✅

⭐ The 12 that pass on both are the controls, and passing on both is what makes them controls: they pin behaviour this PR does not change. Every assertion that states the new behaviour fails on base source.

On this branch — Test Files 3 passed (3) / Tests 16 passed (16), os-verify-lock: VERDICT command-exit 0.

Verification (all at final commit 028664357, clean tree)

Command Verdict line
pnpm exec vitest run packages/plugin-form/ Test Files 71 passed (71) / Tests 722 passed (722)
pnpm --filter @object-ui/plugin-form type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0, script echoed — the second project compiles this package's tests, so the new test file is type-checked
pnpm --filter @object-ui/plugin-form lint (eslint ., 101 files) ✖ 713 problems (0 errors, 713 warnings), exit 0 — warnings are the package's pre-existing no-explicit-any style, 0 errors
pnpm check:control-bytes ✅ OK (scanned 5286 tracked text file(s))
pnpm check:shell-escape-residue ✅ OK
node scripts/check-changeset-no-major.mjs ✅ No changeset declares a major bump.

Dependency closure was built first (pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build) — without it type-check reports Cannot find module '@object-ui/*' for missing dist/*.d.ts, which reads exactly like a broken import.

Declared narrowing: the repo-wide pnpm lint (turbo → each package's own eslint .) was not run locally; CI owns the farm. The narrowing is measurable, not assumed: (1) the linted population came from eslint's own resolution, not a guess — eslint --format json packages/plugin-form/ reports 101 files, i.e. the whole affected package, a superset of the 4 files this PR touches; (2) eslint.config.js configures no type-aware linting (no parserOptions.project / projectService), so this diff cannot move the verdict on any file it did not touch; (3) the diff changes no exported API, so no other package's eslint . sees different input. Repo-wide pnpm lint and pnpm check run in CI regardless.

Not touched

LineItemsPanel.tsx, deriveMasterDetail.ts, packages/spec, content/docs/releases/. objectui#6372's and objectui#6360's landed arms are untouched beyond the two disclosed assertions. Changeset: patch, @object-ui/plugin-form.

⛔ Draft on purpose — the PM lands it.


Generated by Claude Code

…nfig hint

The third and last arm of MasterDetailForm's detail resolver to sit on a
permanent "Loading columns…". deriveDetail throws when no lookup/master_detail
field on the child object references the parent; the catch returned the entry
unresolved, so it fell through to the loading branch — and the derive is not
retried, so those columns could never arrive.

The entry now carries status: 'underivable', and the renderer gives it a branch
of its own: the config-hint shape (objectui#5940 / objectui#6360) naming both
ends of the relationship it could not find and `relationshipField` as the key to
set. Deliberately NOT objectui#6372's refusal placeholder, which states the
schema could not be LOADED — false for a schema that loaded fine. The thrown
error is still logged with its stack.

Per triage's ruling on objectui#6394 (option 2), which also authorized rewriting
the one objectui#6360 assertion whose premise was measured inaccurate: it was
titled as a pending fetch while its fixture reached the failed derive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3230.8 KB 3266.6 KB
Main entry chunk (gzip) 156.1 KB 350 KB
Entry file index-CAByiEUF.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.30KB 4.28KB
app-shell (runtime-config.js) 18.10KB 6.51KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 505.84KB 114.57KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.18KB 47.97KB
fields (index.js) 238.89KB 60.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.66KB 12.84KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.60KB 44.82KB
plugin-dashboard (index.js) 133.46KB 34.48KB
plugin-designer (index.js) 211.95KB 42.75KB
plugin-detail (index.js) 245.10KB 62.31KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 131.83KB 32.21KB
plugin-gantt (index.js) 164.14KB 39.87KB
plugin-grid (index.js) 201.79KB 54.60KB
plugin-kanban (index.js) 52.87KB 14.57KB
plugin-list (index.js) 112.63KB 27.45KB
plugin-map (index.js) 20.09KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.70KB 7.69KB
plugin-tree (index.js) 9.26KB 3.13KB
plugin-view (index.js) 84.55KB 20.74KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 54.84KB 18.43KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.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 25, 2026 20:52
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 25, 2026
Merged via the queue into main with commit fd814d6 Aug 25, 2026
28 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6394-master-detail-derive-config-hint branch August 25, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants