Skip to content

fix(fields): carry the spec's optional location keys across a coordinate edit - #6713

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6664-location-field-preserve-altitude-accuracy
Aug 28, 2026
Merged

fix(fields): carry the spec's optional location keys across a coordinate edit#6713
os-sales merged 1 commit into
mainfrom
claude/issue-6664-location-field-preserve-altitude-accuracy

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes #6664

LocationField edits a location's coordinate pair as one comma-separated text box, and
rebuilt its emission as a fresh object from the parsed text:

onChange({ lat, lng });

@objectstack/spec declares the stored shape as { lat, lng, altitude?, accuracy? }, so a
stored altitude / accuracy was gone the moment a user retyped the coordinates. Nothing
warned; the two keys simply were not in the object handed to onChange.

Why this is worth fixing even with no producer

Triage measured both questions the card left open: no producer writes either key — not
in objectui, not in objectstack. It queued the card anyway, on a third reading: both keys
are registered on the authorable surface
(objectstack packages/spec/authorable-surface.base.json:3438,3442), so the platform has
already promised customers they may author them. The population that decides this is "does
a customer write it", which neither repo can measure — hence a real defect at p2 rather
than an implementation-first key.

This predates #6272

The drop is not a regression from #6272. Before that flip the widget emitted
{ latitude, longitude } and discarded the rest identically. What #6272 changed is only
that the declared value type is now the spec's — so the type claimed four keys while the
write path handled two. Keeping this out of that card's atomic fence was correct: the A1
ruling covered a bare two-sided rename and nothing else. This PR closes the gap that flip
made visible.

The fix

A key-by-key pick of exactly the two spec-declared optional keys, out of a value that is
already a valid LocationValue:

function carryOptionalKeys(lat: number, lng: number, previous: unknown): LocationValue {
  const emitted: LocationValue = { lat, lng };
  if (!isLocationValue(previous)) return emitted;
  if (isFiniteNumber(previous.altitude)) emitted.altitude = previous.altitude;
  if (isFiniteNumber(previous.accuracy)) emitted.accuracy = previous.accuracy;
  return emitted;
}

No spread, and no Object.assign — deliberately. A stored record may still hold the
retired latitude / longitude spelling, and spreading the incoming value would carry
that dialect straight back into the emitted object and undo #6272's rename. The two ...
occurrences left in the file are the pre-existing ...props rest parameter and the
toDomProps DOM pass-through; the value itself is never spread.

Each optional key is taken only when it is a usable number. Measured against the spec:
z.number() rejects NaN, Infinity and a numeric string alike (invalid_type at
[altitude]), so carrying one of those forward would make this widget emit a value the
platform's own validator refuses. Leaving it behind narrows the emission — it emits
less than it was handed, never more — rather than widening what the widget tolerates.

The finite test was extracted into a named isFiniteNumber predicate so the coordinates
and the two optional keys are held to the same check instead of two copies that can drift.

Tests

packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx — 12 pins.

Red before, green after. On the merge-base (5967be095), with the test file present
and the widget untouched, the five positive pins fail and the rest pass:

Tests  5 failed | 7 passed (12)

AssertionError: expected { lat: 31.2304, lng: 121.4737 } to deeply equal { lat: …, …(2) }
- Expected
+ Received
  {
-   "accuracy": 12,
-   "altitude": 5,
    "lat": 31.2304,
    "lng": 121.4737,
  }

After the fix, all 12 pass, and so do the 12 existing #6272 pins in
LocationField.specShape.test.tsx (24/24 across the two files).

Negative control — green both before and after, and that is the correct reading. Four
tests build an old-dialect input carrying latitude/longitude and assert the emitted
object does not contain them, plus the general form: no key outside the spec's own
declared set, read off LOCATION_SCHEMA.shape rather than copied into a literal. These
cannot be red today, because today's code emits a freshly built { lat, lng } that
trivially carries no dialect. They are not vacuous — they guard the future regression
the card's fence names: someone "simplifying" the key-by-key pick into
onChange({ ...value, lat, lng }). Under that rewrite the first of them goes red, because
its stored value carries both spellings. No positive pin can provide that guard.

One of them pins why those assertions read the emitted object's keys and not
safeParse: LocationValueSchema is a plain, non-strict z.object, so it accepts a
spread-polluted object and merely strips the unknown keys from its parsed output while the
value handed to onChange keeps them. The schema cannot be the fence's guard.

Deliberately not in scope

LocationCellRenderer and ObjectMap still read lat/lng only, so neither displays the
carried keys. Triage measured that and ruled it a feature needing its own card; it is
not written here.

Changeset

'@object-ui/fields': patch. This restores data the declared value type already promised —
no API added, no accepted input retired, and the anti-spread fence keeps the emitted key
set a subset of the spec's declared four. That matches how this repo scores a change to
what a surface writes (cf. view-overlay-write-patch-only-5233); minor here is
reserved for retiring an accepted read (cf. view-column-io-canonical-identity-5725).
Per AGENTS.md a changeset in this repo never declares major.

Verification

All at bcfdb556e, the final commit.

check result
pnpm exec vitest run packages/fields/ packages/plugin-detail/ 226 files, 2910 tests passed
pnpm --filter @object-ui/fields type-check green (tsc --noEmit && tsc -p tsconfig.test.json)
npx eslint packages/fields 194 files, 0 errors
node scripts/check-lint-coverage.mjs 46/46 packages linted, 0 outstanding errors
node scripts/check-control-bytes.mjs OK, 5561 tracked text files
node scripts/check-spec-symbol-derivation.mjs OK
node scripts/check-changeset-presence.mjs OK, 1 changeset for 2 published source files
node scripts/check-changeset-no-major.mjs / -fixed / -overwrite OK
node scripts/check-vi-mock-specifiers.mjs OK
node scripts/check-package-self-import.mjs OK
node scripts/check-readme-exports.mjs OK, 0 unbuilt
node scripts/check-published-dist-tooling.mjs OK after a full 43-task build
pnpm exec vitest run scripts/ 85 files, 2389 tests passed

Both edited files were confirmed present in the tsconfig.test.json program via
tsc --listFiles, so the green type-check really covers the new test file rather than
skipping it.

The repo-wide pnpm lint was narrowed to packages/fields locally. That narrowing
excludes nothing: eslint.config.js enables no type-aware linting (no project or
projectService) and none of the 17 local rules in eslint-rules/ reads the filesystem,
so a verdict is a pure function of the single file being linted and this three-file diff
cannot move the verdict on a file it did not touch — and check-lint-coverage.mjs
independently reports 0 outstanding errors across all 46 packages.


Generated by Claude Code

…ate edit

`LocationField` edits `{ lat, lng }` as one comma-separated text box and rebuilt
its emission as a fresh object from the parsed text, so the two OPTIONAL keys
`@objectstack/spec` declares alongside them — `LocationValue` is
`{ lat, lng, altitude?, accuracy? }` — were dropped the moment a user retyped
the coordinates. Nothing warned. Both keys are registered on the platform's
authorable surface, so a customer may author them even though neither repo
produces them today.

The drop predates objectui#6272: before that flip the widget emitted
`{ latitude, longitude }` and discarded the rest identically. #6272 changed only
the declared value type, so the type claimed four keys while the write path
handled two.

The carry is a key-by-key pick of exactly those two keys out of an
already-valid `LocationValue` — deliberately not a spread of the incoming
value, which would carry a stored record's retired `latitude` / `longitude`
spelling back into the emission and undo #6272's rename. A negative control
pins that, and reads the emitted object's own keys rather than `safeParse`
because `LocationValueSchema` is non-strict and accepts a polluted object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions github-actions Bot added the tests label Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3232.1 KB 3266.6 KB
Main entry chunk (gzip) 157.2 KB 350 KB
Entry file index-DtXh3Z4b.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 509.32KB 115.60KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.20KB 60.12KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.26KB 34.43KB
plugin-designer (index.js) 212.80KB 43.15KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.51KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.01KB 27.57KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to 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.

finding(fields): LocationField drops the spec's optional altitude / accuracy when a user edits the coordinate pair

2 participants