Skip to content

fix(plugin-detail): record:details edit mode edits a textarea field in a multi-line textarea (objectui#11562) - #11567

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11562-detail-textarea-multiline
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11562-detail-textarea-multiline

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11562
Clause-②: no

What changed

record:details edit mode now edits a textarea field in the fields package's multi-line TextAreaField, so a save keeps the line breaks the value was stored with.

  • packages/plugin-detail/src/InlineFieldInput.tsx: textarea leaves INLINE_PLAIN_TEXT_FIELD_TYPES (class D, objectui#4220) and joins INLINE_ROUTED_FIELD_TYPES. It takes the branch markdown got in objectui#11541, which renders TextAreaField. The set's docblock drops the warning objectui#11541 wrote about textarea. Its "nothing is lost" sentence now covers only the single-line members left: text, email, phone, url.
  • Drift guard inlineEditTypeCoverage.test.tsx: textarea moves from benign to routed. The benign bucket's wording now says single-line.
  • InlineFieldInput.delegation.test.tsx: textarea leaves the class-D loop. The class-D pins for text, email, url and phone are unchanged and green.
  • New pin file InlineFieldInput.textareaEditor-11562.test.tsx.
  • One @object-ui/plugin-detail patch changeset.

Deviation from the ruling's mechanism words: DETAIL_ROUTED_INLINE_TYPES is untouched

The ruling reads "Route textarea through DETAIL_ROUTED_INLINE_TYPES to the fields package's multi-line TextAreaField". The PR does not add textarea to that set. Measured, it is the wrong lever for this type:

  • DETAIL_ROUTED_INLINE_TYPES exists only to exempt members of the fields package's shared INLINE_EXCLUDED_FIELD_TYPES from the detail hosts' gate. markdown needed it because markdown is in that set. textarea is not (EDIT_WIDGETS maps it to TextAreaField), so both hosts already opened an editor for it. On origin/main the pin "the details body shows the inline-edit affordance on a textarea row" is green, and the row in edit mode renders exactly one editor: the one-line terminal input.
  • Adding it anyway is refused by an existing pin. Probe leg D below: with textarea in DETAIL_ROUTED_INLINE_TYPES, inlineCredentialGate.test.tsx fails "every exempted type is a member of the shared exclusion (no free-floating entries)" with "textarea must be in the shared set". Nothing else changes.

The substance of the ruling lands as written. The row routes textarea to TextAreaField, and the drift guard moves it from "benign" to "routed". The route lives where markdown's editor lives: INLINE_ROUTED_FIELD_TYPES and the editor branch.

The defect, measured

On origin/main 072b7e8, textarea edited in the terminal input, an INPUT element of type text. The browser strips line breaks from the value such an input is given. A stored note "Call back Monday.\n\nBudget approved, needs legal review.\n" showed as "Call back Monday.Budget approved, needs legal review.". One keystroke made that flattened string the value the Save batch wrote.

Pins

New file InlineFieldInput.textareaEditor-11562.test.tsx. The multi-line value has a blank line inside and a trailing newline, and every comparison is byte for byte.

  • InlineFieldInput: a TEXTAREA seeded with the stored value, never the terminal input. One keystroke appended to what the editor shows emits the stored text plus that keystroke. autoFocus lands on it.
  • DetailSection: the pencil shows on a textarea row. Edit mode renders one editor, seeded byte-identical.
  • HeaderHighlight: the same editor inside the edit session.
  • Save round trip: InlineEditProvider + DetailSection + InlineEditSaveBar over a store-backed update. The PATCH body is exactly the edited string, the store holds it, and re-entering edit seeds the editor with it.
  • Single-line control: the keystroke pin and the round trip also run with "Call back Monday.". Both are green on origin/main and after the fix, so a single-line value saves exactly as before.
  • Grid cell: FieldEditWidget, which ObjectGrid's cell editor delegates to, seeds a TEXTAREA with the multi-line value byte for byte. This PR does not touch that path. The pin records that the row and the cell now edit with one widget.

Verification

All at 5af2bb9 (the only commit), from the worktree.

  • pnpm --workspace-concurrency=2 --filter "@object-ui/plugin-detail^..." build: exit 0. 12 projects in scope, all 11 with a build script printed Done (test-support has none).
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-detail/: Test Files 233 passed, 1 skipped (234). Tests 2313 passed, 8 skipped (2321).
  • pnpm --filter @object-ui/plugin-detail run type-check: exit 0. tsc -p tsconfig.test.json --listFiles lists all three touched test files.
  • eslint over the 4 touched source and test files (--no-inline-config --format json): 4 files, 0 errors. The three edited files carry the same warning counts as at 072b7e8 (42, 1 and 0, all no-explicit-any or react-refresh), and the new file carries none. eslint.config.js sets no parserOptions project, so linting is not type-aware and this diff cannot change the result for an untouched file.
  • check:control-bytes, check:new-line-citations (0 new), check:changeset-claims, check:pending-changeset-literals, check-changeset-presence, check-changeset-overwrite, check-changeset-no-major, check-changeset-fixed, check-test-path-roots, the three check-vi-mock-* checks, check-unreferenced-sources, check-type-check-coverage, check-lint-coverage: all exit 0.

Ablations, after the commit. Each mutation went through objectstack's ablation-replace.mjs, or a trap-armed whole-file swap for leg C. Each restore is proven by the blob hash equal to HEAD's and an empty git diff HEAD. Each leg ran the new pin file, the drift guard and the delegation suite.

  • Leg A, textarea put back into INLINE_PLAIN_TEXT_FIELD_TYPES: 2 failed, 113 passed. The drift guard's "no type is claimed twice" and its benign-list pin fail. The new pins stay green because the routed branch runs before the class-D test. A first attempt at this leg did not run: the tool refused an anchor that its own replacement contained, and nothing was measured.
  • Leg B, only the branch's textarea disjunct deleted: green, 115 passed. This direction is the expected one. The type is no longer class D, so it falls to the delegation tail, and FieldEditWidget renders the same TextAreaField. See the acceptance notes.
  • Leg C, InlineFieldInput.tsx swapped whole to 072b7e8 (byte-identical to origin/main): 9 failed, 106 passed. 7 are in the new pin file: every multi-line pin and every pin on the editor's kind. The 2 single-line controls stay green. The other 2 are in the drift guard.
  • Leg D, the ruling's mechanism words: textarea added to DETAIL_ROUTED_INLINE_TYPES (with inlineCredentialGate.test.tsx in the run): 1 failed, 151 passed, as described above.

CI runs the full farm. Locally only plugin-detail was run. The plugin-detail sets this PR edits are not exported from the package index, and outside plugin-detail no test imports them or queries the terminal input on a textarea row.

Acceptance notes

  • Leg B shows that for textarea the explicit branch and the delegation tail reach the same widget. So the drift guard cannot tell a routed textarea from a delegated one: deleting the branch alone stays green, and the user still gets TextAreaField. The branch follows the ruling's "routed" bucket and markdown's precedent. What the pins do guard is the actual regression, textarea landing back on the one-line input (leg C, and leg A at the guard).
  • The grid's record overlay (RecordDetailPanel, which plugin-grid imports from this package) renders the same detail hosts, so it gets the multi-line editor too. The grid CELL still edits with FieldEditWidget, unchanged.
  • packages/plugin-detail/README.md is not touched. objectui#11512 edits it, and it does not describe per-type inline editors. No content/docs page lists which editor a detail row uses per type, so no guide page changes.
  • Reach, from the card: notes: Field.textarea on objectstack examples/app-crm's lead and examples/app-showcase's task.

Generated by Claude Code

…n a multi-line textarea (objectui#11562)

`textarea` leaves INLINE_PLAIN_TEXT_FIELD_TYPES (class D) and joins
INLINE_ROUTED_FIELD_TYPES; InlineFieldInput routes it to the fields
package's multi-line TextAreaField, on markdown's branch (objectui#11541).
The terminal input is a one-line INPUT of type text, and the browser strips
line breaks from the value it is seeded with, so a stored multi-line note was
saved flattened after one keystroke.

The hosts' gate is unchanged: textarea is not in the shared
INLINE_EXCLUDED_FIELD_TYPES, so DETAIL_ROUTED_INLINE_TYPES does not need it.

Drift guard: textarea moves from benign to routed. Class-D pins for text,
email, url and phone stay. New pins in
InlineFieldInput.textareaEditor-11562.test.tsx.

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

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3309.4 KB 3330.4 KB
Main entry chunk (gzip) 150.3 KB 350 KB
Entry file index-CEO6dV6r.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.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 261.62KB 66.20KB
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.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.26KB 38.44KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.60KB 64.57KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.57KB 45.31KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.02KB 64.63KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.16KB 1.96KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 5.07KB 2.39KB
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) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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 October 3, 2026 17:12
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 17:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit ab18797 Oct 3, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11562-detail-textarea-multiline branch October 3, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant