Repository navigation
fix(plugin-detail): inline edit hands dependsOn fields the staged record (objectui#7190) - #10255
Conversation
…ecord (objectui#7190)
`InlineFieldInput` passed no `dependentValues`, and since objectui#7206 the
fields widgets resolve `dependentValues ?? {}` with no context fallback, so a
`dependsOn` lookup or option list edited inline on the record page was
permanently gated beside a filled parent. `InlineFieldInput` now takes an
optional `dependentValues` prop and forwards it to the lookup, select and
FieldEditWidget branches; `DetailSection` passes the record DetailView already
merges with the inline draft, and `HeaderHighlight` overlays the draft itself.
The real-host pin flips from the gated reading to the enabled one and adds the
staged-draft and select/multiselect cases; the probe changeset's context-tail
sentence is corrected.
Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL — Fixes needed inside the claimed surface:
A |
… staged record (objectui#7190) Contract review on PR #10255 measured an infinite render loop on the highlights strip: a field's own value was read as "the draft unless it is undefined, else the saved value", while the record handed to `dependsOn` widgets was the staged spread in which an own undefined draft key wins. A single select or radio whose stored value the current parent no longer offers pruned it with onChange(undefined), was handed the saved value again, and pruned again forever. The strip now reads each field's value from the same memoised staged record it passes as dependentValues, so a prune settles after exactly one call, as it does in the details body. The pin file gains a bounded prune probe (a circuit breaker turns the loop into a count, not a hang), the strip/body/admissible prune cases and a radio case through the FieldEditWidget forward. The changeset drops two overstatements and discloses that entering inline edit can now stage a cascade prune. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
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
Size Limits
|
… type (objectui#7190) Drops the explicit `any` type argument the previous commit added, which raised HeaderHighlight's no-explicit-any warning count above its base. No behaviour change. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
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
Size Limits
|
Contract review (round 3)Served-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL — Prose only; no code change.
|
…e (objectui#7190) Contract review round 3 on PR #10255: a single select / radio prunes with onChange(undefined), and the record page's save sends the draft through a JSON PATCH that drops undefined keys, so that prune is not written. Only the array prune (multi select / checkboxes) is. The changeset now says so. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…ssue-7190-inline-field-dependent-values-probe
✅ Console Performance Budget
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
Size Limits
|
Contract review (round 4)Served-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
✅ Console Performance Budget
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
Size Limits
|
Fixes #7190
Clause-②: yes
What was wrong
A
dependsOnfield edited inline on a record's detail page could never be filled. A lookup declaringdependsOn: ['region']renderedlookup-trigger-gated, disabled, reading "Select region first", while theregionit asked for was on screen, filled, in the same edit session. Aselect/ multiselectwhose field declaresdependsOn(options carrying per-optionvisibleWhen) was withheld the same way. Both the details body and the highlights strip were affected. This was measured in the real host and pinned on 2026-09-01 (PR #7207), and re-measured on this branch's base before any change (report comment 5814656189 on the card).The widgets in
@object-ui/fieldsread the record they cascade on from one channel, thedependentValuesprop their host passes:LookupFieldanduseCascadingOptionsboth readdependentValues ?? {}, with no context fallback since objectui#7206 (ruling C).InlineFieldInput, the one inline editor both record-page surfaces share, passed none.The change
Route A of the round-2 claim (card comment 5814689653): the call sites pass the record, the shape the objectui#7206 ruling names.
packages/plugin-detail/src/InlineFieldInput.tsxtakes an optionaldependentValuesprop (typed as a Record of string to unknown, the same spelling and shape as the widgets' own prop). It forwards it to theLookupField/UserFieldbranch, to bothSelectFieldbranches, and toFieldEditWidget, which hands it on to the option widgets it renders.packages/plugin-detail/src/DetailSection.tsxpasses itsdata.DetailViewalready hands every section the saved record merged with the inline draft.packages/plugin-detail/src/HeaderHighlight.tsxreceives the saved record, so it overlaysinline.draftwith the same spreadDetailViewuses and passes that.The record is the staged one. This matches the grid inline editor's
pendingRow ?? row(objectui#7188) and the form's live watched record. A parent edited in the session re-scopes the child before anything is saved, and clearing the parent re-gates the child.Clause-②: yes:InlineFieldInputPropsis exported from@object-ui/plugin-detail, and gaining an optional member widens the published surface. The seat owes a review-tier contract review before this is enqueued. Hence theminorchangeset.Also in this PR:
packages/app-shell/src/views/RecordDetailView.lookupDependsOn-7190.test.tsxflips from the gated reading to the enabled one. It adds the staged-draft cases and the select / multiselect cascade, each at both call sites, each with a live control..changeset/7190-detail-dependson-lookup-probe.mdis rewritten. Its sentence claiming a host could populatectx.datahas been false since objectui#7206. The frontmatter stays empty..changeset/7190-detail-inline-dependent-values.mddeclares'@object-ui/plugin-detail': minor.Evidence (all at HEAD
cfd1f8c5, run from the repo root, heavy runs through the shared verify lock)Pins first, on the unfixed tree (test file edited, source unchanged):
Tests 8 failed (8).expect(gated).toBeNull(), because the gated trigger was present.select-trigger-tier_anypresent, thetags_anychipsgold, silver) and failed atselect-empty-tierbeing present.That last reading is the select cascade's render measurement: it was gated the same way, at both call sites, beside live controls. So the same prop is forwarded to
SelectField.After the change:
Tests 8 passed (8), 4 cases × 2 call sites.$filter {region: emea}. The control is enabled.regionin the draft re-gates the lookup ("Select region first"), and the control stays enabled. Stagingapacunlocks it and it queries{region: apac}.updateis never called.gold, the region's option. The controls offer both values.apacre-scopes the multi select tosilver. Clearingregionre-gates both declared selects. The controls are unchanged.Ablation, one leg per call site, on the committed fix. Each mutation goes through
ablation-replacewith the anchor count verified and the blob change verified. Each restore is proven by state: blob equals HEAD,git diff HEADis 0 bytes,git status --porcelainis empty. Package resolution is source-aliased invitest.config.mtsand nodist/was present, so no rebuild legs were needed.DetailSection'sdependentValues={data}4 failed, 4 passed, exactly the BODY fourHeaderHighlight'sdependentValues={stagedRecord}4 failed, 4 passed, exactly the STRIP fourSelectFieldforwards (both branches) set toundefined4 failed, 4 passed, exactly the option fourHeaderHighlightpasses the saveddatainstead of the draft overlay2 failed, 6 passed, exactly the two STRIP staged casesLeg C's first spelling was refused by
ablation-replaceand ran nothing. Its replacement text also occurs at theBooleanFieldbranch, so the planted-count check could not move. It was re-run with a distinct replacement, and that run is the one in the table.Type-check: the dependency closure was built first (
turbo run build --filter=@object-ui/app-shell^..., 28/28 tasks). Thenpnpm --filter @object-ui/plugin-detail run type-checkandpnpm --filter @object-ui/app-shell run type-checkwere both run, eachtsc --noEmit && tsc -p tsconfig.test.json, exit 0, noerror TS.tsc --listFilesnames the pin (1 hit; control: 15 siblingRecordDetailViewtests) and the three edited sources (3).dependentValues={42}was refused withTS2322: Type 'number' is not assignable to type 'Record...', and a record-shaped value was accepted. So app-shell reads the rebuilt declaration. The scratch file was removed and the tree is clean.Suites:
pnpm exec vitest run packages/plugin-detail/:Test Files 197 passed (197),Tests 1982 passed (1982).Test Files 53 passed (53),Tests 597 passed (597). These are all 27RecordDetailView*tests, the fieldsLookupField*/SelectField*/MultiSelectField*/UserField*widget tests, and every test outside plugin-detail that namesInlineFieldInput/HeaderHighlight/DetailSection.Gates, exit captured before any pipe:
pnpm check:control-bytes: OK, 8256 files.pnpm check:new-line-citations:0 new citation(s).node scripts/check-changeset-presence.mjs: OK, 1 changeset declared.node scripts/check-changeset-overwrite.mjs: exit 0, report-only. It reports the intended rewrite of7190-detail-dependson-lookup-probe.md, declared-at-base nothing / declares-now nothing, which is its case 2.pnpm check:changeset-claims: exit 0. It asked for a read of8394-record-page-emptiness-authority.md, which namesDetailSection.tsx. I read it and it is still true:hasCellValuestill comes from./emptiness.pnpm changeset:check: OK.pnpm check:vi-mock-inherit,check:vi-mock-specifiers,check:test-path-rootsandcheck:pending-changeset-literals: all OK.Lint, narrowed and declared as a narrowing:
eslint.config.jsenables no type-aware linting (noprojectService, noparserOptions.project), so this diff cannot move the verdict on any untouched file. On the 4 touched files (count from--format json) there are 0 errors. The three source files have the same warning counts as at the base (39 / 12 / 8). The pin goes from 8 to 11no-explicit-anywarnings in its test harness. The repo-wideturbo run lintis CI's.Round 3 (head
ed7398f1): the strip value readFieldEditWidgetforward at both call sites. A probe countsonChange(undefined)prunes and stops forwarding past 20, so a regression reads as a count, not a hung runner.cfd1f8c5:1 failed | 12 passed,expected 21 to be 1(the loop, measured and bounded). Fixed:13 passed (13).ed7398f1, restore proven after every leg:1 failed | 12 passed, exactly the strip inadmissible case.FieldEditWidgetforward set toundefined:4 failed | 9 passed.DetailSectionprop:6 | 7.3 | 10.7 | 6, observed against a prediction of6 | 7. The extra failure is the admissible control's readiness wait: with the record withheld, the declared select is gated and renders no trigger.stagedRecordis memoised on[data, inline?.draft]as a render-cost hint only. Leg F shows termination does not rest on it.197 passed (197)files /1982tests; the consumer set54 passed (54)/608.type-checkexit 0 for plugin-detail and app-shell. All gates exit 0.Acceptance notes
packages/plugin-detail/src/DetailView.tsxis untouched (fenced by objectui#8941); the route does not need it. Nothing inpackages/fieldsorpackages/reactmoves.undefinedis dropped by a JSON transport, so its stored value survives Save and reappears on refresh (filed as objectui#10291). Cancel discards it (round 4, headcad44d9f). This is pinned as exactly one prune per field at both call sites, with an admissible control.dependentValues, so an own draft key wins even when its value isundefined(commit99d6ae96). The round-2 divergence is gone: atcfd1f8c5it closed into an infinite render loop on the highlights strip (contract review5815969556).InlineFieldInputdirectly and omitsdependentValueskeeps today's gated behaviour. That is the visible failure objectui#7206's ruling chose over a context fallback.Body refreshed by
domain:uiseat #1 with the round-3 corrections and the round-4 Save clause · 2026-09-24T16:39ZGenerated by Claude Code