Skip to content

fix(fields): a detail-page address reads as a formatted address, not stringified JSON - #4215

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-4037-address-display-formatter
Aug 11, 2026
Merged

fix(fields): a detail-page address reads as a formatted address, not stringified JSON#4215
yinlianghui merged 3 commits into
mainfrom
claude/issue-4037-address-display-formatter

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4037

The gap

The display (read) registry mapped address straight to JsonCellRenderer, so a populated address rendered as {"street":"中策路 1 号","city":"杭州",…} on a record detail page — while a location field in the same field group rendered formatted, and the create/edit dialog rendered the same value as proper Street / City / State / ZIP / Country inputs. Display-side only: the input registry has always carried address.

Premise verified against origin/main before implementing, and it still holds: packages/fields/src/index.tsx had address: JsonCellRenderer in getCellRenderer's map, exactly as the source thread's triage recorded.

Both read surfaces, one path

The card names two surfaces — detail read mode and the inline-edit read state. DetailSection builds onedisplayValue (packages/plugin-detail/src/DetailSection.tsx:244) and renders it from both the mobile read row and the desktop row that carries the pencil affordance, so a single registry entry fixes both. Both are pinned by tests driving the real DetailSection, not the renderer.

Layout choice and its source

The layout is not invented here. AddressField's readonly branch already collapsed a stored address to one line, and that rule is now the only implementation:

Street, City, State ZIP, Countrystate and the postal code share one comma group, space-separated inside it ("CA 94102"), either one able to occupy the group alone.

It moved into a pure packages/fields/src/widgets/address-format.ts that both the widget and the new AddressCellRenderer call, so a readonly form and a detail page cannot spell one stored address two ways. A copy next to the renderer would have been a rule that drifts — which is the failure the issue reports, one surface later. The module is deliberately React-free so the eager barrel can format a cell without pulling AddressField and its inputs out of the lazy widget chunk.

On locale-aware ordering: the report was zh-CN, and Chinese postal convention is largest-to-smallest (country first). I did not introduce locale-dependent ordering. Neither the issue nor the source thread objectstack#5019 (read in full, including both triage comments) establishes a format ruling, and the sub-field set is the input widget's own. Reordering by locale would make the read line disagree with the readonly form line and with the input order users type in — a bigger inconsistency than the one being fixed, decided unilaterally. The existing single rule is applied uniformly; if the maintainer wants locale-ordered addresses, that is one deliberate change to formatAddress affecting every surface at once, which is exactly the point of it having one home.

Graceful partials

Absent, non-string and whitespace-only parts collapse and are dropped rather than spaced over: a street-only address is 中策路 1 号, never , , , and never the string undefined. Legacy records whose postal code was written under zipCode (objectstack#5143) still render it, matching what the input widget reads.

Nothing is silently swallowed: an object carrying no recognized part keeps today's compact-JSON rendering rather than disappearing, {} and null show the usual empty placeholder, and a plain string address passes straight through.

Reverse verification

Predicted direction first, then measured. Reverting only the two fix files to origin/main (git checkout origin/main -- …, never git stash) and re-running:

Tests 12 failed | 5 passed (17)
→ expected '{"street":"中策路 1 号","city":"杭州","stat…' not to match /[{}]/
→ expected '{"city":"SF","state":"CA"}' to be 'SF, CA'
→ expected '{"city":"San Francisco","state":"CA",…' to be 'San Francisco, CA 94102'
→ Unable to find an element with the text: 中策路 1 号, 杭州, 浙江 310000, 中国

That is the issue's own signature. Two reported deviations from the naive expectation, both honest:

  1. "carries the full value: every authored sub-field appears" stays GREEN when reverted — stringified JSON also contains every part. It is a completeness assertion, not a defect pin, and is reported as such rather than counted as red-first evidence.
  2. The location control was initially red too, because it asserted the no-JSON signature over a container that also held the address field — it restated the tests above instead of controlling for them. Fixed in its own commit: the control now renders location alone and is green in both directions, which is what "the sibling formatter is untouched" actually claims.

Controls: location / geolocation formatting unchanged, json / object / composite / record still stringify, address inputs unchanged on every surface including the dialog.

Verification

  • npx vitest run packages/fields/ packages/plugin-detail/142 files, 1725 tests passed
  • New: 13 cases in AddressCellRenderer.test.tsx (resolved through getCellRenderer, so a registry regression is caught, not merely a formatter change) + 4 in DetailSection.addressDisplay.test.tsx
  • type-check both packages: Done. lint both packages: 0 errors
  • check:control-bytes: OK (3909 files); explicit control-byte self-scan of every touched file: clean
  • i18n gates green — "No en value changed in this range". No i18n discipline needed here: this is pure value formatting with no labels or copy; the only user-visible strings are the record's own stored data
  • Changeset: patch for @object-ui/fields. plugin-detail's only change is a test, which releases nothing

Scope

index.tsx diff is strictly the address entries (one import, one renderer, one registry line) — no ImageField (#4141), no app-shell/ListView (#4155), no data-objectstack (#4139), no AppHeader (#4197).

Out of scope, filed separately: actively inline-editing an address collapses it into one raw text box reading [Object] via coerceToSafeValue, and typing writes a string over the structured value. That is the inline-edit input path missing the address widget, a different defect from this display-registry gap — not fixed here.


Generated by Claude Code

@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 2:47am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-CpKkhLym.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)140.66KB36.25KB
fields (index.js)227.64KB56.42KB
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)237.80KB59.48KB
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.04KB26.67KB
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 03:00
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit e2e6360Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4037-address-display-formatter branch August 11, 2026 03:01
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.

Record detail page renders address fields as raw JSON — the display registry has a location formatter but no address one

2 participants

@yinlianghui@claude