Skip to content

fix(console): omit a cleared server-owned field from a create submit - #6060

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5883-formpage-omit-server-defaults
Aug 24, 2026
Merged

fix(console): omit a cleared server-owned field from a create submit#6060
yinlianghui merged 1 commit into
mainfrom
claude/issue-5883-formpage-omit-server-defaults

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5883

The mechanism

Since #5727 a create form on the console's two form routes correctly opens a runtime-default control empty and leaves the key out of values. But handleSubmit submitted valueswholesale, and a rendered control writes back through its onChange the moment the user touches it — so a submitter who types into a server-owned field and then clears it puts the key back, holding a blank.

ObjectQL.applyFieldDefaults resolves a declared default only for a field arriving absent or null. A blank string is neither, so submitting one stores '' and silently defeats the declaration: the same suppression #5727 closed, reached by the other door.

Reproduced before fixing — the card was explicitly not browser-verified, so its reachability claim was not inherited. FormPage.serverDefaults.test.tsx at the pre-fix commit: 5 failed, 4 passed (9), the four passes being the counter-probes, which are guards on both sides of the change.

The fix

omitServerOwnedBlanks in FormPage.tsx drops those keys from a CREATE payload on both the internal /forms/:name and the anonymous /f/:slug route. The redirect submit behaviour's token scope moves from values to the same payload, since "the values as submitted" is what that scope has always claimed to be and the two now differ.

The classifiers are not re-implemented. Both come from @object-ui/core: isRuntimeDefault (already imported in this file for #5727's seeding fence) and isMissingForRequired. That is where they live precisely so every layer deciding server-ownership reads one answer, and it is how plugin-kanban and @object-ui/components' form renderer read them too.

Delta from the card: the sibling helper is not importable

The card and the dispatch both assumed the shape question was "adapt the console's data to omitServerResolvedDefaults, or adapt its inputs". Measured on the merge-base, neither is available: @object-ui/plugin-form's exports map publishes . alone, and its root src/index.tsx does not re-export schemaDefaults — so omitServerResolvedDefaults has no spelling an importing package can write. Reaching it would mean editing another package's published surface, outside this card's file surface. Recorded as a separate observation in #6059, which is left open for triage and is not addressed by this PR. omitServerResolvedDefaults itself is only a local application of the same two @object-ui/core calls, so the thing that must not fork has not forked.

What "empty" means, measured rather than assumed

A filter testing for '' would have left arms behind. Measured on this renderer's own controls, submitting after a touch-then-clear on each:

armcleared write
text / user / email / url / password / textarea / date / time / datetimestring ""
number / integer / decimal / currencyobject null — the arm spells e.target.value === '' ? null : Number(...), so no NaN is reachable
select / picklist / enum (back to the placeholder option)string ""
boolean / toggle / checkbox (toggled on then off)boolean false — a real value, not a clear
radiono cleared state; only a picked option is ever written

Both reachable empty spellings are inside isMissingForRequired, and false correctly is not — which is exactly why the fix reads that predicate rather than testing for a blank string.

Boundaries, each pinned

  • CREATE only. On an edit form the token was resolved at insert, so a cleared column is a deliberate removal; dropping the key would silently discard the user's edit.
  • Runtime defaults only. A field with no default, or with a static one (which fix(plugin-form): seed create forms from the object schema's declared defaults #4068 says is seeded into the control), keeps its blank — the user removed a value that was there, or is expressing "leave this empty".

Verification

Direction predicted before running: the positive assertions go red on the submitted payload, the counter-probes stay green.

Ablation — call site reverted to const payload = values at 5bd1ee015, mutation proven on disk by grepping the injected marker (1 hit) and separately the removed text (0 hits), landing site printed and anchor uniqueness asserted before writing; restored under trap ... EXIT INT TERM with a cwd-independent git -C /abs/worktree checkout -- /abs/worktree/apps/console/src/components/FormPage.tsx:

legresult
mutatedTests 5 failed | 4 passed (9) — every positive assertion red, every counter-probe green
restoredTests 9 passed (9), injected marker 0 hits, removed text back, git diff HEAD --stat empty

No rebuild step sits between the mutation and the measurement: the console's vitest run aliases workspace packages to their src/, and the mutated file is the console's own source.

Gate union, run after the final commit, at 5bd1ee015:

gateverdict line
pnpm --filter @object-ui/console type-check (hyphenated; script name echoed as > @object-ui/console@17.6.0 type-check)exit 0
pnpm exec vitest run over 14 console form suitesTest Files 14 passed (14) · Tests 236 passed (236)
eslint on the two changed files (plain, no --no-inline-config)21 problems (0 errors, 21 warnings) — base measured at 20; delta is +1 react-refresh/only-export-components, the warning each of this file's 12 existing exported helpers already carries
node scripts/check-phantom-dependencies.mjsEvery in-scope import is declared by the package that publishes it.
node scripts/check-changeset-presence.mjs2 source file(s) ... declares 1 changeset(s)
node scripts/check-control-bytes.mjsOK (scanned 5000 tracked text file(s))
check-changeset-no-major / check-changeset-fixed / check-lint-coverage / check-type-check-coverageexit 0 each

The first type-check run was the unbuilt-closure red (Cannot find module '@object-ui/...') rather than a missing-node_modules one; fixed by pnpm --filter '@object-ui/console^...' build, not by reinstalling.

Generated by Claude Code


Generated by Claude Code

A field whose declared defaultValue is an instruction the server resolves
per insert opens EMPTY on a create form since #5727, and its key stays out
of `values`. But `handleSubmit` submitted `values` wholesale, and a control
the user touches writes back through its `onChange` — so typing into such a
field and then clearing it put the key back holding a blank.
`ObjectQL.applyFieldDefaults` resolves a declared default only for a field
arriving absent or null, so that blank was stored and the declaration
silently defeated: the same suppression #5727 closed, reached by the other
door.
`omitServerOwnedBlanks` drops those keys from a CREATE payload on both
routes. It composes the two canonical classifiers from `@object-ui/core` —
`isRuntimeDefault` (already read here for #5727's seeding fence) and
`isMissingForRequired` — rather than re-deriving either, so "server-owned"
and "left empty" mean the same thing here as on the plugin-form chain.
Emptiness is the shared PRESENCE predicate because the arms disagree about
what a cleared control writes: the string arms write '', the number family
writes null, and `false` stays a real value.
Boundaries pinned by counter-probes: a blank cleared from a field with no
runtime default (or a static one) is still submitted, and an edit submit is
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3219.6 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-BTx_64c0.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.12KB61.87KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@os-litant