Skip to content

fix(detail): inline-edit an address as sub-fields, not a text box reading [Object] - #4222

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4216-inline-address-input
Aug 11, 2026
Merged

fix(detail): inline-edit an address as sub-fields, not a text box reading [Object]#4222
yinlianghui merged 1 commit into
mainfrom
claude/issue-4216-inline-address-input

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4216

Inline-editing an address on the record detail page collapsed it to one text box reading [Object], and saving wrote a bare string over the structured value. location and geolocation are fixed with it — same cause, same two signatures, one defect in three spellings.

Cause

InlineFieldInput's type switch routes the scalar and relational families to dedicated widgets. Every structured-object type matched nothing and fell through to the terminal raw text input at the end of the component, which stringifies through coerceToSafeValue. That helper's general-object case extracts name || label || externalId || id || _id and otherwise returns the literal [Object] — and an address ({ street, city, state, postalCode, country }) or a coordinate pair ({ latitude, longitude }) carries none of those keys.

The defect has two halves and the second is the damaging one:

  1. Display — the edit box reads [Object], so [Object] is what the user sees as the value they are correcting, which makes typing over it the natural gesture.
  2. Write — that box is a plain text input wired to onChange(v), so whatever is typed is emitted as a string replacing the whole object on save. An ordinary double-click inline edit destroys the sub-field structure.

Input path only. #4037 / PR #4215 fixed the display registry, so read mode — including the inline-edit read state before editing starts — already renders a formatted address; once a field is actively being edited, DetailSection renders InlineFieldInput instead, which is why that PR does not touch this.

The fix

Route address, location and geolocation to the widgets the create/edit dialog already uses — AddressField, LocationField, GeolocationField, the form's own structured-value editors (fieldWidgetMap, and FieldEditWidget's "structured-value editors" block). No new editor, and no consumer-side tolerance: the two entry points now cannot diverge on the value shape they write back.

coerceToSafeValue is untouched. Adding an address branch there would only have improved the display half while leaving the write half intact; the routing is what stops these values from reaching it at all, which makes the branch unnecessary rather than merely unwritten.

autoFocus follows the numeric branches' convention — each widget forwards the DOM pass-through set onto its first focusable sub-input (street / the coordinate box / latitude), so entering inline edit lands the caret where a single-input type would. Commit/cancel semantics are unchanged: InlineFieldInput is a controlled input in every branch, and the hosts commit through InlineEditSaveBar, so there is no per-input keyboard contract to mirror. Both hosts (DetailSection body and the HeaderHighlight strip) share the component and get the fix identically; the highlights strip renders a taller editor for these types, as it already does for the routed ImageField / FileField.

Composite fall-through sweep

Every field type reaching the terminal text input before this change, and the verdict for each. Fixed here: the object-valued composites, which are the same-shape routing the card scopes. Everything else is filed.

ClassTypesStringificationDisposition
Object-valued compositeaddress, location, geolocationDestructive — object reads [Object], saves a stringFixed here — routed to the form's widgets
Array-valuedtags, checkboxes, vector, options-less multiselectDestructive — array joined to a string, saved as oneFiled #4220
Container / embeddedobject, composite, record, grid, repeater, jsonDestructive — reads [Object], saves a string over the embedded structureFiled #4220 (likely exclusion, not routing)
Scalar, wrong editortoggle, slider, progress, rating, radioType-lossy — numbers/booleans emitted as strings; radio free-types over its option setFiled #4220
Credentialspassword, secretDestructive + disclosure — the mask renders as the value and is written backFiled #4221
Already a stringtext, textarea, markdown, html, richtext, email, phone, url, time, color, code, qrcode, options-less selectBenign — identityNo action; kept on the terminal input, with controls pinning it
Computed (gated out)formula, summary, rollup, auto_numbern/a — never enter inline editNo action, except the autonumber spelling filed as #4219

The array / container / scalar rows are grouped in #4220 rather than ridden along here because each needs a design call (route vs. exclude; what an options-less checkboxes does), not a one-line routing. #4220 also records the root cause behind the whole tail: @object-ui/fields already exports FieldEditWidget with INLINE_EXCLUDED_FIELD_TYPES and a drift-guard test against the form's type list, and InlineFieldInput hand-rolls a parallel switch that consults neither.

Evidence

Red first. The pin asserts both halves for all three types and failed on both signatures before the fix — 11 failed, 2 passed, the 2 passes being the string-type controls that legitimately keep the terminal input:

× renders the real sub-field editor, never a single box reading "[Object]"
× never emits a bare STRING that replaces the whole object
...
AssertionError: expected [input] to be null
+ Received:
value="[Object]"
AssertionError: expected 'string' not to be 'string' // Object.is equality

The write-half pin is deliberately query-independent (it types into the first box the editor renders, whatever that is). Addressing a named sub-input instead would have failed with "unable to find an element" — a signature naming the missing sub-input, not the missing structured write, which are two different defects.

After the fix:Test Files 1 passed (1) / Tests 13 passed (13).

Affected-package suites — repo root, path-filtered per AGENTS.md:

pnpm exec vitest run packages/plugin-detail/ packages/fields/ --maxWorkers=2
Test Files 143 passed (143)
Tests 1738 passed (1738)

Reverse verification. Predicted RED with both signatures returning. Removed only the routing (git checkout origin/main -- packages/plugin-detail/src/InlineFieldInput.tsx), keeping the test: 11 failed / 2 passed — the identical split and test set as the pre-fix run, both signatures back. Restored with git checkout HEAD -- ..., green again at 13/13.

Gates:type-check green for @object-ui/plugin-detail and @object-ui/fields (after building the dependency closure); eslint on both changed files 0 errors (32 pre-existing no-explicit-any warnings, the same pattern the sibling branches already use); check-control-bytes OK across 3917 tracked files; check-changeset-presence OK. No copy was touched, so no i18n gate applies — the widgets' sub-labels are the form's existing ones and this change adds no strings.

Changeset:.changeset/inline-address-input-4216.md, patch on @object-ui/plugin-detail. Not a skip-changeset candidate — the change is user-visible (objectui#3724).


Generated by Claude Code

…ding [Object]
InlineFieldInput's type switch routed the scalar and relational families to
dedicated widgets; every structured-object type matched nothing and fell
through to the terminal raw text input. That fallback stringifies through
coerceToSafeValue, whose general-object case returns the literal [Object] for
a value carrying none of name/label/externalId/id/_id — and then emits what
the user types as a STRING that replaces the whole object on save.
Route address, location and geolocation to the widgets the create/edit dialog
already uses, so the object round-trips and the two entry points cannot
diverge on the value shape they write back. coerceToSafeValue is untouched:
the routing is what stops these values from ever reaching it.
Fixes#4216
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 3:30am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-BJKA05gi.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.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)486.47KB107.53KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)143.81KB37.39KB
fields (index.js)228.33KB56.58KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.52KB30.68KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)238.21KB59.54KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.18KB26.70KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 04:16
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit 6314e87Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4216-inline-address-input branch August 11, 2026 04:17
yinlianghui pushed a commit that referenced this pull request Aug 11, 2026
Three residual reds on main after the rc.5→rc.6 spec bump (#4169), none of
which the train cars themselves covered.
QUICK_REFERENCE.md's "Current Release" block still quoted `^17.0.0-rc.5` for
both `@objectstack/spec` and `@objectstack/client` while every manifest it
names as its anchor declares `rc.6`. The pin test for that block
(quick-reference-current-release-4143) landed on main AFTER the train was
built, so no car ever ran against it.
recordDetailsInputs.spec-parity carried `layout: 'custom'` in both fixtures of
its object-sections case. `layout` was removed in @objectstack/spec 17.0.0
(objectstack#6946, ADR-0087 D2) and rc.6 gave it a named `never` rejection, so
the object-form fixture failed on `layout` while its `sections` were valid —
and the id-list fixture above it kept PASSING on `layout`'s own `invalid_type`
without ever reaching `sections`. Dropping the key puts both verdicts back on
`sections`; the id-list case now also pins the issue PATH, since a bare code
could not tell the two rejections apart.
AddressField re-exports `AddressValue` through its declaring module rather than
bare. main's #4222 moved the declaration into the pure `address-format` module
while this train re-sourced the type from `@objectstack/spec/data`; the merge
keeps both by importing the spec type in `address-format` (where the definition
now lives) and letting the widget re-export from there, which is the form
check:spec-symbols resolves to a declaration site instead of reading as a
second local declaration of a name the spec owns.
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.

Inline-editing an address on the detail page collapses it to one text box reading [Object], and saving overwrites the structured value with a string

2 participants

@yinlianghui@claude