Uh oh!
There was an error while loading. Please reload this page.
fix(detail): inline edit no longer destroys array values or flattens types on the record page - #4244
Merged
Merged
Conversation
…idget InlineFieldInput's type switch ends in a raw text input, and everything it has no branch for landed there: displayed through coerceToSafeValue and written back as whatever was typed, a bare string. Two damage classes survived #4216 (structured objects) and #4228 (containers + credentials, closed at the host gate). Array-valued fields (tags, checkboxes, an options-less multi picklist) were offered as "a, b" — coerceToSafeValue joins arrays — and saved as that string. Type-lossy scalars (toggle, slider, progress, rating, radio) round-tripped through String(), so a boolean column received "true", a numeric one "42", and radio accepted any free-typed value its option list never offered. Types the switch already routes keep their editors, so no working path churns. Everything else the fields package can edit inline now falls back to FieldEditWidget — the form's own control, json to the code editor included — and only genuinely string-valued types keep the plain input. The design calls inside the array class resolve inside that delegation, which is the point of delegating rather than routing type by type. The lasting deliverable is the drift guard: every member of the form widget map UNION the spec FieldType enum must be exactly one of routed / excluded / delegated / benign, and the declared bucket is checked against what the component really renders. A new type in none of the four is red, instead of inheriting the value-destroying default in silence — this family has produced six cards from that one cause. @object-ui/fields: the four fixed-option widgets no longer clear the stored value when no options are authored. An empty offered set had two opposite causes — cascaded to zero (clear, ADR-0058) and never configured (nothing to decide) — and the second deleted the value on MOUNT, which the delegation would have staged straight into the record draft. FieldEditWidget also forwards autoFocus so an inline host's caret lands on the widget's own control. Fixes#4220 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 11, 2026 07:24
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#4220
InlineFieldInput's type switch ends in a raw text input, and every type it has no branch for landed there: displayed throughcoerceToSafeValueand written back as whatever the user typed — a bare string. #4216/#4222 closed the structured composites, #4228 closed the containers and credentials at the host gate. This is the rest of the sweep.Premise, re-measured on this tip (all confirmed)
A throwaway probe asserted the CURRENT behaviour before a line was changed; every assertion passed on
origin/main(17/17):tags/checkboxes/multiselect/select+multiple, no options['alpha','beta']"alpha, beta"(coerceToSafeValuejoins arrays)"alpha, beta, gamma"— a string over the arraytoggletrue"true""whatever-i-type"— a string over the booleanslider/progress/rating40/3"40"/"3"radio'alpha'"alpha"json,code,color,time,qrcodeeach reachedinput[type=text]Class B is already closed and was verified, not redone:
object/composite/record/grid/repeater/vectorare all gated by #4228's shared-exclusion consultation.The four-way classification, as landed
One fallback branch, not per-type routing. Every field type in
FORM_FIELD_TYPESUNION the specFieldTypeenum (57 types) now sits in exactly one bucket:auto_number,autonumber,composite,filter-condition,formula,grid,html,markdown,object,object-ref,password,recipient-picker,record,repeater,richtext,secret,summary,vectorInlineFieldInput's own switch — unchanged, no UX churnaddress,audio,avatar,boolean,currency,date,datetime,file,geolocation,image,location,lookup,master_detail,multiselect,number,owner,percent,select,signature,tree,user,videoFieldEditWidget— the same control the form renderscheckboxes,code,color,json,progress,qrcode,radio,rating,slider,tags,time,toggletext,textarea,email,phone,urlTwo conditional members are pinned by name rather than by type: an options-less single
selectstays on the plain input (its value is a bare string, and the widget's own empty state would leave the user unable to enter anything), while themultiplespelling of that same degenerate field delegates and keeps its array.selectis deliberately absent from the benign set;markdown/html/richtextare excluded rather than benign because the hosts stopped opening an editor for them in #4228.The drift guard is the deliverable that outlives the fix
packages/plugin-detail/src/__tests__/inlineEditTypeCoverage.test.tsx— 67 assertions. Every type must be in exactly one bucket: none is red (a new type would otherwise inherit the value-destroying default), two is red (overlapping claims). The benign list is enumerated explicitly, never "everything else" — the open tail is the drift itself. This family has produced six cards from one cause (#2942, #3355, #4216, #4219, #4221/#4228, #4220).The declarative half alone would be a tautology over four hand-written sets, so each declared bucket is also checked against what the component really RENDERS, via a
data-testidon the terminal input. Sniffinginput[type=text]cannot do this:TextField,ColorFieldandQRCodeFieldrender one too, so the heuristic reads a benign type's correct fallback and a delegated type's correct widget as the same thing.FieldEditWidget under the inline hosts — measured
{ field, value, onChange }. The hosts' draft ({ ...data, ...editedValues }→ one atomicdataSource.update) is value-shape agnostic, so an array / boolean / number reaches the write unchanged. Pinned per class.autoFocus:FieldEditWidgetnow forwards it, and each widget's owntoDomPropswhitelist lands it on the real focusable control (pinned ontags' chip input andcode's editor). NodataSourceis needed — every record-querying type is routed above.compact(that is set only for the relational pickers, all routed). The tallest surfaces arecheckboxes/radiooption lists, which grow the row the same way the routed multi-select chip row already does.optionsat all (no decision — the widget renders its "nothing to pick" state). In the second case it deleted the stored value on mount, and the detail hosts render every editable field in a section at once, so an options-lesscheckboxeswould have staged an empty array into the record draft the moment the user started editing any other field in that section, and the save bar would have written it back. The prune now requires an authored list to prune against; the cascade itself is untouched, and four controls pin that a configured list still clears what it no longer offers. This also closes the same path in the grid's inline cell editor, which has always delegated here.No type turned out to be genuinely unusable inline, so no new host-gate exclusion was needed.
Reverse verification
Predicted before running, both times, and taken out with
git checkout(nevergit stash— shared stack):getByRole('switch')/('slider')unable to find, stars absent,radioback on a text box), the five never-branched types, and the 12 delegated entries of the coverage guard. Every class D pin, every routed control (select+options,boolean,number,date,address,lookup,image, thepermission-facet-linkhint) and the whole partition half stayed green — 81 passed.One honest deviation from the naive prediction:
tags'autoFocuspin stays GREEN without the fix, because the terminal input honoursautoFocustoo. That assertion only discriminates for a widget whose control is not a bareinput(code's textarea), which is where it went red.onChangecalled once, with[]/undefined) plus the three detail-page pins for the options-less picklists. All four cascade controls stayed green, which is the assertion that the ADR-0058 behaviour was not weakened..d.tsrejects (autoFocusTypo) at the delegation call site fails withTS2322 … Did you mean 'autoFocus'?, so the typecheck read the freshly built declarations rather than a cached copy.#4219 ride-along verdict — still worth landing
Measured (and now pinned): a field typed
autonumberrenders no inline affordance and no editor on either host, in the details body and inside an active session on the highlights strip. #4228 changed the reachability underneath that report —autonumberresolves through the alias table onto the excludedauto_number, so the shared exclusion catches it, not the computed set the card names.The spelling fix is still worth landing, and not only as defence in depth, on two measured paths:
TEXTUAL_REF_FALLBACK_TYPESis read directly byInlineFieldInput's reference fallback —!!field.reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has(type)— with no host gate in front of it.InlineFieldInputis exported public API, so anautonumbercarrying areference_tostill resolves into the record picker there today. That is the input-side half of A field typedautonumber(the spec spelling) is inline-editable on the detail page, so a user can overwrite a machine-generated value #4219's own report, and no exclusion-set consultation touches it.EDIT_WIDGETS"). The two gates are a union precisely so that one of them going away is survivable; todayauto_numberwould still be caught by the computed gate andautonumberwould not, so the protection of a machine-generated identity depends on which spelling the metadata happens to use.Nothing about #4219 is changed here — it is a separate card, and this PR deliberately does not pin today's asymmetry in either direction, since a pin asserting it would go red on the very fix that card proposes.
Verification
pnpm exec vitest run packages/plugin-detail/ packages/fields/→ 148 files, 1929 tests, all passed (repo-root cwd, path-filtered per AGENTS.md §测试纪律).pnpm --filter '@object-ui/plugin-detail^...' --filter '@object-ui/plugin-grid^...' build→ Done;type-checkonplugin-detail/fields/plugin-grid(the otherFieldEditWidgetconsumer) → Done.pnpm exec eslinton all nine changed files → 0 errors (66 pre-existingno-explicit-anywarnings;--max-warningsis deliberately unset in this repo).pnpm run check:control-bytes→ OK, 3946 files.node scripts/check-changeset-presence.mjs→ OK, 1 changeset for 2 released packages.fields.options.empty,fields.tags.placeholder, …).Generated by Claude Code