Skip to content

Inline edit still destroys array-valued and container field values on the detail page (the rest of the #4216 fall-through sweep) #4220

Description

@yinlianghui

Filed unassigned per Prime Directive #10, from the composite fall-through sweep the #4216 card asked for. #4216 (PR pending) routes the three object-valued composites — address, location, geolocation. This issue is the remainder of that sweep: the types that still reach InlineFieldInput's terminal raw text input and are corrupted by it. They are grouped because they share one component and one cause; they are not in #4216 because each needs a design call rather than a same-shape one-line routing.

Cause (unchanged from #4216)

packages/plugin-detail/src/InlineFieldInput.tsx ends in a raw text input. Anything the type switch does not match lands there, is displayed through coerceToSafeValue, and emits whatever is typed as a string via onChange(v) — replacing the stored value on save.

The remainder, by class

A. Array-valued — destructive

type stored shown in the edit box emitted on save
tags string[] "a, b" (joined) "a, b" string
checkboxes string[] "a, b" "a, b" string
vector number[] "1, 2, 3" "1, 2, 3" string
multiselect / select + multiple with no options string[] joined string

The last row is the existing select branch's guard: it requires Array.isArray(field.options) && field.options.length > 0, so an options-less multi picklist falls through. TagsField needs no options and would route cleanly; CheckboxesField needs them, which is the design call.

B. Container / embedded — destructive

object, composite, record, grid, repeater, json all store an object or an array of rows and render as the literal [Object] (or [Object], [Object]), then save a string over the whole embedded structure.

These should almost certainly become non-editable inline rather than routed. @object-ui/fields already decided exactly that and wrote down why — INLINE_EXCLUDED_FIELD_TYPES in FieldEditWidget.tsx:

Containers / non-authorable — a sub-form / sub-grid / embedding vector doesn't belong in a single cell.

InlineFieldInput does not consult that set.

C. Scalar, wrong editor — type-lossy, not structure-destroying

type stored emitted on save
toggle boolean "true" string (boolean is routed; its alias toggle is not)
slider, progress, rating number string
radio one of options any free-typed string, valid or not

radio is the same argument the select branch already makes in its own comment ("users ... can't free-type invalid values"), applied to the widget that was missed.

D. Benign — no action

text, textarea, markdown, html, richtext, email, phone, url, time, color, code, qrcode, options-less select: already strings, so stringification is the identity and nothing is lost. date / datetime have their own routed native-date branch. Computed types (formula, summary, rollup, auto_number) are gated out of inline edit entirely — except for the autonumber spelling, filed separately as #4219.

Credentials (password, secret) also reach this input and are a distinct severity; filed separately.

The root cause worth considering

@object-ui/fields already exports FieldEditWidget — documented as rendering "the SAME control the form uses", with INLINE_EXCLUDED_FIELD_TYPES for the types that must not be inline-edited and a drift-guard test pairing the two against the form's FORM_FIELD_TYPES. InlineFieldInput hand-rolls a parallel type switch covering a subset, with no such guard — so every entry above is that duplication drifting, and each fix applied one type at a time will drift again.

Two shapes, and the choice is a real design decision:

Recommend deciding the shape before taking any single row above, since the answer changes what each fix looks like.

Reachability

Live today: the desktop detail row (and the highlights strip) enters inline edit on double-click whenever the record is inline-editable and the field is not readonly / computed / a system field. Every type listed satisfies that.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 11, 2026
  2. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM + delegated shape ruling (objectui whole-repo seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3; veto window open) — dispatching now. #4228's landing re-scoped this card (containers already excluded via the shared set); what remains is classes A (arrays), C (type-lossy scalars), and the drift-proofing.

    Ruling: HYBRID — delegate-as-fallback, not wholesale. Between the card's two shapes:

    1. Types InlineFieldInput already routes today (selects, numbers, dates, address family from fix(detail): inline-edit an address as sub-fields, not a text box reading [Object] #4222, media) KEEP their current editors — no UX churn on working paths; the card's own cost argument against wholesale delegation is accepted.
    2. The DESTRUCTIVE/type-lossy tail — classes A and C, plus every type the fields package says has an inline widget but the switch has no branch for — falls back to FieldEditWidget instead of the terminal raw text input. The design calls inside class A (what an options-less checkboxes does) resolve INSIDE the delegation: FieldEditWidget already owns those decisions, which is the point of delegating.
    3. The terminal raw text input survives ONLY for the benign already-string class (D).
    4. The drift guard is the deliverable that outlives the fix: every FORM_FIELD_TYPES member must be exactly one of routed / excluded (shared set) / delegated / explicitly-benign — a new type landing in none of the four is a red check, mirroring the pairing test the fields package already has. That closes the recurrence this family has now produced six cards from.

    Ride-along required: #4219's verification — assert an autonumber-typed field renders no inline affordance post-#4228 (its alias-resolution exclusion), and judge whether the TEXTUAL_REF_FALLBACK_TYPES spelling fix is still worth landing for defense-in-depth; report the verdict so the seat can close #4219.


    Generated by Claude Code

  3. yinlianghui commented on Aug 11, 2026

    @yinlianghui
    CollaboratorAuthor

    ACCEPT (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4244, CI fully green.

    The hybrid ruling landed as one fallback branch with the four-way classification made mechanical: 57 types (FORM_FIELD_TYPES ∪ spec enum) partitioned EXCLUDED 18 / ROUTED 22 / DELEGATED 12 / BENIGN 5, the two conditional members pinned BY NAME with measured reasoning (an options-less single select stays benign because the delegated widget's empty state would strand the user; its multiple spelling delegates and keeps the array), and the 67-assertion drift guard checks both exactly-one-bucket AND what the component actually renders (a bare input[type=text] cannot discriminate — the data-testid does). The measurement's real find was fixed at the producer, not worked around: all four fixed-option widgets' cascade-clear pruned against the RESOLVED option set without asking why it was empty, so an options-less field's stored value was deleted ON MOUNT — and since the detail hosts mount every editable field at once, editing anything would have staged that deletion into the save bar. The rawOptions.length === 0 guard closes it in four widgets, ADR-0058's cascade is untouched with four controls, and the grid's cell editor inherits the fix for free.

    #4219 verdict accepted — the card stays OPEN, re-scoped: #4228's alias-aware exclusion does catch autonumber (pinned on both hosts), but the spelling fix is still owed on two measured paths — the reference fallback reads TEXTUAL_REF_FALLBACK_TYPES DIRECTLY with no host gate (an autonumber carrying reference_to resolves into the record picker today, on exported public API), and the two gates are a union whose survivability currently depends on which spelling the metadata uses. The deliberate non-pin of today's asymmetry (it would go red on #4219's own fix) is the right restraint. #4247 (dependency-GATED options clearing stored values on mount, form path included) is queued for triage with its genuine judgment call intact. Flipping ready + auto-merge.


    Generated by Claude Code

  4. added 2 commits that reference this issue on Aug 17, 2026
    e7663f2
    bc64bfe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions