Skip to content

docs(react): the InlineEditContextValue.draft comment teaches the own-key read (objectui#10466) - #10543

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-10466-inline-edit-draft-read-rule
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-10466-inline-edit-draft-read-rule

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10466
Clause-②: no

What was wrong

The draft member's doc comment on the published InlineEditContextValue (packages/react/src/context/InlineEditContext.tsx) ended: "Read a field's live value as draft[name] ?? data[name]."

That read falls back to the saved value whenever the draft value is null or undefined. A field the user emptied is an own draft key with an empty value. So is a single select or radio emptied by a cascade clear, which stages null since PR objectui#10464. A host that copied the comment showed the value the user had just removed. On the highlights strip, that read handed an option widget the pruned value again on every render, an endless loop (objectui#7190; relayed from the card, not re-run here). The comment ships in the dist typings, so it is what a host author sees on hover.

Measured first, on origin/main 6881e9e47

I looked for every reader of the inline-edit draft: each useInlineEdit() consumer outside tests, by git grep over packages/*/src and apps/.

Host What it reads Rule for an emptied field
DetailView (@object-ui/plugin-detail) hands each section data={{ ...data, ...editedValues }}, where editedValues is the draft spread: an own key wins
DetailSection its data prop (the merged record above); from the session it reads only fieldErrors inherits the spread
HeaderHighlight stagedRecord = { ...data, ...draft }, then stagedRecord[field.name] spread: an own key wins
InlineEditSaveBar writes the draft; reads no live value not a reader
containers.tsx (@object-ui/components) .editing only not a reader
  • The two readers agree: both use the spread.
  • None of them uses an own-key test (in draft, hasOwnProperty, Object.hasOwn), and no exported helper does the merge. So the comment names the spread and points at no helper.
  • The form hosts do not read this draft. ObjectForm and the form renderer keep their own form state, and the grep above finds no useInlineEdit() call in them.

The change

  • Only the JSDoc sentence changes. It now says to read the value from the staged record { ...data, ...draft }. An own draft key wins even when its value is empty (null, undefined, ''), and data[name] is read only when the key is absent. It does not name ??. No runtime code changes and nothing new is exported.
  • One pin: packages/react/src/context/__tests__/InlineEditContext.draftReadRule-10466.test.ts.
  • One patch changeset for @object-ui/react. check-changeset-presence counts both touched src/ files as published source of a released package.

Why a pin

This package already pins prescriptive doc text by reading the source: LazyPluginLoader.jsdocExample.test.ts and useNavigationOverlay.docExampleRecordSource-7638.test.ts. The new pin follows that pattern and stays loose. It reads only the code spans of the draft doc comment, never its prose, and asserts two things:

  1. No span reads the draft and then falls back through a nullish or falsy operator.
  2. At least one span names a read that keeps an own draft key: the staged-record spread with the draft last, or an own-key test.

Each matcher has synthetic controls. They show it fires on the old spelling and on the reversed spread, and stays silent on the own-key spellings.

Ablation: put the old doc back, watch the pin go red, restore

  • Tool: ablation-replace.mjs in wrap mode at a0bef54ea, inside a restore trap.
  • Mutation: the new doc lines were replaced with the old sentence. I checked before the run that this rebuilds the BASE file byte for byte.
  • Proof it landed on disk: the anchor count went from 1 to 0 and the replacement count from 0 to 1. The blob changed from 8457b8281762 to 0239d8089256. By grep, spread spans went from 1 to 0 and fallback spans from 0 to 1.
  • Result: I predicted red, and it went red: Tests 2 failed | 3 passed (5). The two failures were the two assertions on the real comment; the three controls stayed green.
  • Restore: the blob after restore equals the blob at HEAD (8457b8281762), and git diff HEAD is empty (0 bytes).
  • No build step in between: the pin reads the .tsx source by path, not dist.

Local verification (all at a0bef54ea)

  • pnpm exec vitest run --maxWorkers=2 packages/react/ gave Test Files 96 passed (96) and Tests 1303 passed (1303). The pin is in that population: vitest list --filesOnly packages/react/ lists 96 files, and the pin is one of them.
  • pnpm --filter '@object-ui/react^...' build built types, core, data-objectstack and i18n: VERDICT command-exit 0.
  • pnpm --filter @object-ui/react type-check echoed tsc --noEmit && tsc -p tsconfig.test.json: VERDICT command-exit 0. The test project's --listFiles includes the pin (1 of its 96 test files).
  • Gates, all exit 0:
    • check:control-bytes;
    • check:new-line-citations (0 new citation(s));
    • check-changeset-presence;
    • check:changeset-claims: nothing flagged. The born-false reading found 0 line addresses, the self-contradiction reading was clean, and no pending changeset names a touched file;
    • check:pending-changeset-literals, check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite;
    • check:test-path-roots, check-type-check-coverage, check:spec-symbols, check:shell-escape-residue.
  • eslint, narrowed to the touched files, with the narrowing measured:
    • Scope, from eslint itself: of the 3 touched paths, eslint ignores the changeset .md ("File ignored because no matching configuration was supplied").
    • Count, from --format json: 2 files, 0 errors, 5 warnings. The same file at BASE gives the same 5 warnings (no-explicit-any x4, react-refresh/only-export-components x1), so none of them is new.
    • Untouched files are safe: eslint.config.js does not enable type-aware linting (no projectService, no parserOptions, no TypeChecked preset; typescript-eslint appears 9 times as a control). So this diff cannot change a lint result in any untouched file.
    • The repo-wide pnpm lint is left to CI.

Acceptance notes

  • The published CHANGELOGs of @object-ui/plugin-detail and @object-ui/app-shell still quote the old read in their objectui#2407 P2 entries. They are historical record, so they are left alone.
  • The comment in HeaderHighlight ("An OWN draft key wins even when its value is undefined") already agrees with the new doc, so it is unchanged.

Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC


Generated by Claude Code

…-key read

The `draft` member's doc comment told hosts to read a field's live value
with a nullish fallback to the saved record. A field the user or a cascade
clear emptied is an own draft key with an empty value, so that read handed
back the value just removed (the prune loop objectui#7190 measured on the
highlights strip). The comment now teaches the staged-record spread: an
own draft key wins even when empty, and the saved value is read only when
the key is absent. No runtime change.

Adds a source-text pin over the comment's code spans, in the pattern of
the package's existing doc pins, and a patch changeset for @object-ui/react.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3046.2 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-DQT-kKNE.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) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
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.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
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) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
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) 544.97KB 130.34KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 257.02KB 65.07KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.37KB 41.83KB
plugin-grid (index.js) 215.49KB 58.91KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.55KB 28.29KB
plugin-map (index.js) 23.82KB 7.42KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 88.00KB 21.97KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
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 (expression.js) 0.20KB 0.18KB
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.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 08:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 2ad3671 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10466-inline-edit-draft-read-rule branch September 25, 2026 08:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant