Skip to content

feat(fields): RichTextField honors mobile_fullscreen with a fullscreen editing dialog (#3301) - #3302

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3301-richtext-fullscreen
Aug 3, 2026
Merged

feat(fields): RichTextField honors mobile_fullscreen with a fullscreen editing dialog (#3301)#3302
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3301-richtext-fullscreen

Conversation

@xuyushun441-sys

@xuyushun441-sys xuyushun441-sys commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Closes #3301

按 2026-08-03 的维护者裁决走 A 路:兑现承诺,不收窄mobile.fullscreenLongTextfield:markdown / field:html 从「盖了没人读」变成真的能用。

缺陷复核(premise check,全部确证)

断言 复核结果
RichTextField 不读 flag 确证。修改前 grep -n "fullscreen|mobile_" packages/fields/src/widgets/RichTextField.tsx 零命中
field:markdown / field:html 都落到 RichTextField 确证(packages/fields/src/index.tsx:2349,2353,两处 createFieldRenderer(RichTextField)
string-multiline 无人产出 确证。本仓 + objectstack 兄弟仓(排除 node_modules/dist/.git)全类型 grep,全仓合计 1 处命中,就是判定式那一行本身

改了什么

读取(唯一载体)RichTextFieldfield.mobile_fullscreen只此一处。没有新 prop、没有第二载体、没有 ?? 兜底链——与 TextAreaField 逐字同构。拼错的 flag 在两个 widget 里都保持 inert,不会被某一侧的宽容消费悄悄接住(AGENTS #0.1)。

复用决策:抽出共享件。展开按钮 + 对话框 + draft/commit 状态机进入新的 packages/fields/src/widgets/FullscreenFieldEditor.tsx,两个 widget 共用;per-widget 的只有编辑器本身children 注入)。

判断依据:fullscreenLongText一个表单级设置,用户打开它期待一种行为。手写第二份状态机就是本 issue 的缺陷再来一遍(只是多绕一步)——它会漂移,而且没有任何东西会报告漂移。抽出来之后这一点是机械可验的:见下方 sabotage C,改坏共享件的 commit 一处,TextAreaFieldRichTextField 的测试同时变红。

对话框里是真编辑器RichTextEditorSurface 内联与全屏两处渲染同一份(同 format 指示、同编辑器),fullHeight 是唯一差异。若对话框内联一个裸 Textarea,两个界面当场就已经不一致(少了 format 指示),且这个 widget 每长出一个 affordance 就再不一致一次。

⚠️ 一句实话:本 widget 今天的「富编辑器」就是一个 mono Textarea + format 指示行,没有工具栏可接(源码注释自陈 "A full implementation would use TipTap, Lexical, or Slate")。所以本 PR 兑现的是「全屏里是这个 widget 真正的编辑面,不是降级副本」,而不是凭空发明一条工具栏——后者会是没有验收标准的 scope creep。等编辑器升级时,两处一起升级。

string-multiline 死分支删除:证据见上表,ObjectForm 判定式里附了注释说明依据。剩下 4 个 type 每个都有真实 reader。无既有测试引用该字串,故无测试需要调整。

行为一致性:draft 在打开时从已提交值播种、输入期间保持本地(RHF 不会因用户可能取消的编辑被标脏)、"Done" 提交一次、"Cancel" 丢弃。testid 沿用既有约定但按 widget 命名空间(richtext-fullscreen-toggle / -dialog / -input / -save)——一个表单里可以同时有 textarea 和 markdown 字段,共用一个 id 会让测试说不清自己找到的是谁。焦点管理 / Esc / overlay 沿用仓内 Dialog(Radix),未另行作答。

验证证据

Sabotage(每条:改坏见红、还原见绿)

# 破坏点 结果
A RichTextField 停止读 flag(还原到 #3301 之前) 8 红(全部 rich-text 正向断言),负向控制保持绿
B 对话框内换成裸 Textarea(降级副本) 恰好 1 红puts the REAL editor in the dialogexpected [ span ] to have a length of 2 but got 1
C 共享件的 commit 不回传 draft 3 红,跨两个 widgetRichTextField + TextAreaField 的 commit 测试 + 集成链的表单状态测试 → 证明复用是真共享
D 共享件打开时不重新播种 draft 恰好 1 红discards the draft on Cancel
E 生产侧去掉 fullscreenLongText 开关(无条件盖章) 2 红TextAreaFieldRichTextField 两条负向控制

测试

pnpm exec vitest run packages/fields packages/plugin-form --maxWorkers=2
 Test Files  76 passed (76)
      Tests  855 passed (855)

新增/扩展:

  • packages/fields/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx(新,8 例)— 含 readonly 不出 affordance、Cancel 丢弃且重开从已提交值播种、无第二载体。
  • packages/plugin-form/src/__tests__/ObjectForm.mobileFullscreen.test.tsx(扩展,+4 例)— 镜像 fix(plugin-form,types): carry mobile_fullscreen on the field metadata, where widgets actually read (#3245) #3300 范式,真 ObjectForm → 真渲染器 → 真 RichTextField,除 dataSource 外无 mock:自动生成的 field:markdown + field:html 双双出现 affordance;未开启则不出现;对话框内编辑 → 关闭 → 通过内联控件观察到 react-hook-form 状态已更新;textarea + markdown 混合表单由同一个设置同时点亮两族 widget。
  • TextAreaField 既有测试未做任何修改,全绿(回归不变)。
turbo run type-check --filter @object-ui/fields --filter @object-ui/plugin-form
 Tasks:    13 successful, 13 total

turbo run lint --filter @object-ui/fields --filter @object-ui/plugin-form
 Tasks:    2 successful, 2 total     (0 errors;warnings 均为既有 `field as any` 等既存形态)

FullscreenFieldEditor.tsx 零 lint warning。changeset:@object-ui/fields + @object-ui/plugin-form minorpnpm changeset:check 双绿)。

需要维护者裁决的一处(未擅自修改

packages/types/src/field-types.tsmobile_fullscreen 的 JSDoc 现在过时了:

* **Consumer**: `TextAreaField` (`@object-ui/fields`), which reads it off …

本 PR 之后消费者是两个packages/types 在本单的范围栅栏外(指示要求「停下回报而不是直接改」),所以这行注释原样保留,请裁决是否另开一个 2 行 docs PR 补上 RichTextField。留着不改的代价正是本 issue 那类 declared-vs-actual 漂移,只不过方向反了:声明比实现

packages/types/src/objectql.ts:1265 那句 fullscreenLongText: true, // textarea/rich-text get an "expand" button 不必改——本 PR 之后它第一次成为事实。)

未做

  • 浏览器 dogfood 未做(本单证据等级为测试级;集成链已无 mock 地跑通真渲染器)。
  • 未动 apps/consolepackages/componentspackages/typespackages/speccontent/docs/releases/
  • issue 里提到的 TextAreaField.tsx as any 消费侧洞:未处理,issue 自陈「不必然与上面同单处理」,且彻底修复需先决定 widget 如何收窄 FieldMetadata 联合。

…een editing dialog (#3301)

`ObjectFormSchema.mobile.fullscreenLongText` is documented as "textarea/
rich-text get an expand button", and `ObjectForm` has always stamped
`mobile_fullscreen` onto `field:markdown` / `field:html` fields to deliver
it. Both types resolve to `RichTextField`, and that widget never read the
flag — a producer with no consumer. Turning the setting on gave a phone
user an expand affordance on textareas and nothing at all on markdown or
HTML fields, with nothing reporting that half the feature was inert.

`RichTextField` now reads `field.mobile_fullscreen` — the single metadata
carrier (#3233), the same single read `TextAreaField` performs — and
renders the same affordance and full-height dialog. No prop spelling, no
`??` fallback chain: a misspelled flag stays inert in both widgets rather
than being quietly caught by a tolerant consumer.

The affordance, the dialog and the draft/commit semantics move into a
shared `FullscreenFieldEditor` that both widgets render; only the EDITOR
is per-widget. One form-level setting should produce one behaviour, and a
second hand-written copy of that state machine is this same defect with
an extra step. The rich-text dialog hosts the widget's real editing
surface rather than a bare textarea, so both positions upgrade together.

Removes a dead type from the producer: `'string-multiline'` was stamped
here and `grep -rn` finds it exactly once across both this repo and
`objectstack` — that line itself. No producer, no registry key, no
reader. The four remaining stamped types each have a real reader.

Coverage: the integration chain (real `ObjectForm` → real form renderer →
real `RichTextField`, no mocks) now covers rich text, including a
fullscreen edit committing back into react-hook-form state and a mixed
form lighting up both widget families from the one setting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 3, 2026 2:32pm

Request Review

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CpImuaCh.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.47KB 3.09KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 476.66KB 104.69KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.23KB 34.75KB
fields (index.js) 224.62KB 55.04KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 37.96KB 10.54KB
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.74KB
mobile (index.js) 1.50KB 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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 60.54KB 17.13KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 112.01KB 28.86KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 230.56KB 56.80KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.25KB 39.55KB
plugin-grid (index.js) 185.08KB 49.04KB
plugin-kanban (index.js) 47.89KB 13.18KB
plugin-list (index.js) 104.94KB 25.32KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.55KB 10.59KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.67KB 20.43KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
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) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 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-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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 (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
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

Projects

None yet

2 participants