Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11541-detail-markdown-editor
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11541-detail-markdown-editor

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11541
Clause-②: no

What changed

record:details edit mode now gives a markdown field an editor: the fields package's multi-line TextAreaField.

  • packages/plugin-detail/src/fieldEnrichment.ts: markdown joins DETAIL_ROUTED_INLINE_TYPES, the detail page's carve-out from the fields package's shared INLINE_EXCLUDED_FIELD_TYPES (the carve-out the upload widgets already use). Both detail hosts (DetailSection, HeaderHighlight) read it through isInlineExcludedDetailFieldType, so both now open the editor. INLINE_EXCLUDED_FIELD_TYPES is untouched, so a grid cell still excludes markdown.
  • packages/plugin-detail/src/InlineFieldInput.tsx: a routed markdown branch renders TextAreaField, and markdown joins INLINE_ROUTED_FIELD_TYPES. The INLINE_PLAIN_TEXT_FIELD_TYPES docblock no longer says the hosts never open an editor for markdown.
  • html and richtext: unchanged, still excluded.
  • Drift guard inlineEditTypeCoverage.test.tsx: markdown moves from excluded to routed.
  • One @object-ui/plugin-detail patch changeset.

objectui#4228's reason, read first (the ruling's stop condition)

objectui#4228's blast-radius table records its detail-row reason for markdown: "heavy editor; a one-line text box is lossy". The shared set's own comment is "Heavy / full editors — better in the record form than a cell", which is an argument about a grid cell. Neither holds for a multi-line editor in a detail row, so the stop condition does not fire.

Deviation from the ruling's wording: textarea has no multi-line editor in record:details

The ruling says "markdown gets the multi-line editor textarea already uses in record:details edit mode". Measured on origin/main c476be0: textarea is a member of INLINE_PLAIN_TEXT_FIELD_TYPES, so in record:details edit mode it edits in the terminal plain-text input, an INPUT element of type text. That is a ONE-line box, the very "one-line text box" objectui#4228 kept markdown out of.

  • Chromium 141 (headless, /opt/pw-browsers/chromium): an INPUT of type text given the value "# T\n\npara\n" reads back "# Tpara". A TEXTAREA reads back the bytes it was given. HTML value sanitization for a text input strips line breaks.
  • happy-dom 20.11.2 (this repo's test environment) answers the same: "# Tpara".
  • Component level, at c476be0: InlineFieldInput with type: 'textarea' and a multi-line value renders that INPUT, its value reads "# TitleFirst paragraph.- a- b", and one keystroke emits the flattened string.

Routing markdown onto textarea's branch would therefore land it on the lossy box and fail the ruling's own pin ("its save reads back unchanged, newlines included"). Ablation leg A below measures exactly that. The ruling's intent, a multi-line editor that is lossless for a stored string, is met with TextAreaField: the fields package's multi-line widget, the one a textarea field edits with in the record form and in a grid cell (FieldEditWidget maps textarea to it). As a consequence markdown lands in the drift guard's routed bucket, not in textarea's benign one.

Pins

New file InlineFieldInput.markdownEditor-11541.test.tsx. Its values carry blank lines and a trailing newline, and are compared byte for byte.

  • InlineFieldInput: a TEXTAREA seeded with the stored value, never the terminal input; typing emits the exact string; autoFocus lands on it.
  • DetailSection: the pencil shows on a markdown 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 notes field set to the edited string, the store holds it, and re-entering edit seeds the editor with it.
  • html / richtext: no pencil and no editor on either host.
  • Grid: plugin-grid's inline-edit-options.test.ts gains one assertion, isFieldInlineEditable({ type: 'markdown' }) is false, at the grid's own gate.

Verification

At 950c768 (the change itself):

  • pnpm exec turbo run build --filter="@object-ui/plugin-detail^..." --filter="@object-ui/plugin-grid^..." --concurrency=2: 13 of 13 tasks successful.
  • pnpm --filter @object-ui/plugin-detail --filter @object-ui/plugin-grid run type-check: both Done, exit 0. tsc -p tsconfig.test.json --listFiles confirms both test projects compile the touched test files.
  • pnpm exec vitest run --maxWorkers=2 packages/plugin-detail/ packages/plugin-grid/src/inline-edit-options.test.ts: Test Files 233 passed, 1 skipped (234); Tests 2318 passed, 8 skipped.

Ablations, run after the change was committed. Each mutation went through ablation-replace.mjs (anchor must hit, blob must change) with an EXIT/INT/TERM restore; each restore is proven by the blob hash equal to HEAD's and an empty git diff HEAD.

  • Leg A, the ruling's literal route (markdown falls to textarea's terminal input): 8 failed, 159 passed. The first failure prints the text INPUT holding the value.
  • Leg B, no carve-out (markdown removed from DETAIL_ROUTED_INLINE_TYPES): 7 failed, 160 passed (both hosts, the round trip, the blast-radius row, the drift guard's partition).
  • Leg C, markdown removed from the shared INLINE_EXCLUDED_FIELD_TYPES: the grid pin goes red, 1 failed, 14 passed.

At c65a031 (final head; a typing-only follow-up that removes two as any casts and one any parameter annotation):

  • pnpm --filter @object-ui/plugin-detail run type-check: exit 0.
  • pnpm exec vitest run --maxWorkers=2 over the six touched or adjacent test files: 6 files, 215 tests passed.
  • eslint on the touched files: 0 errors. The warnings are all no-explicit-any; one is on a line this PR adds, the new markdown branch's field as any, written the way its sibling branches are.
  • check:control-bytes, check:new-line-citations (0 new, HEAD c65a031), check:changeset-claims, check:pending-changeset-literals, check-changeset-presence, check-changeset-overwrite, check-changeset-no-major: all exit 0.
  • Declared narrowing: the full plugin-detail suite ran at 950c768 and was not re-run at c65a031. The difference between them is type-only. CI runs the full farm.

Acceptance notes

  • Out of scope, reported for the seat, not changed here: in record:details edit mode a textarea field edits in the one-line terminal input, so a stored multi-line value loses its line breaks on the first keystroke and the save writes the flattened text (measured above). This PR leaves textarea alone; the INLINE_PLAIN_TEXT_FIELD_TYPES docblock now says the line-break loss holds for it, instead of letting "nothing is lost" read as covering it.
  • The record form edits markdown with RichTextField (a Textarea plus a format header). This PR follows the ruling's "the multi-line editor textarea uses" and routes to TextAreaField. Moving the detail row to RichTextField would be a one-line change if the maintainer prefers the form's own markdown widget.
  • The brief expected existing textarea save pins in InlineFieldInput.test.tsx; there are none. The round-trip harness follows InlineEditSaveBar.cascadePruneWire-10291.test.tsx.
  • objectui#11512's packages/plugin-detail/README.md is not touched.

Generated by Claude Code

claude added 2 commits October 3, 2026 14:35
…n a multi-line textarea (objectui#11541)

A markdown row offered no pencil and no editor in edit mode: the detail
hosts consult the fields package's shared inline-edit exclusion, and
markdown is in it. #4228 recorded the detail-row reason as "heavy editor;
a one-line text box is lossy". The row now routes markdown to the fields
package's multi-line TextAreaField through DETAIL_ROUTED_INLINE_TYPES, the
carve-out the upload widgets already use, so neither reason applies. The
grid cell keeps the exclusion; html and richtext stay excluded.

The INLINE_PLAIN_TEXT_FIELD_TYPES docblock no longer says the hosts never
open an editor for markdown, and records that the one-line terminal input
strips line breaks, which also holds for a multi-line textarea value.

Pins: a markdown editor seeded and emitted byte for byte with blank lines
and a trailing newline, both detail hosts, a save that reads back
unchanged, html/richtext still open none, and the grid gate still refuses
markdown. The type-coverage drift guard moves markdown to routed.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…light fixtures (objectui#11541)

Drops two `as any` casts in the new pin file (a plain name/label pair is a
HighlightField) and the `any` on the markdown branch's onChange parameter,
which TextAreaField already types as string. No behaviour change.

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) 3308.4 KB 3330.4 KB
Main entry chunk (gzip) 150.3 KB 350 KB
Entry file index-BjOs-FA1.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) 231.08KB 64.07KB
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.59KB 64.56KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.96KB 44.52KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.05KB 64.60KB
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

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

Projects

None yet

1 participant