Skip to content

test(plugin-form): pin form section headings to the _sections convention (objectstack#5408) - #3373

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5408-form-section-tabs-i18n
Aug 5, 2026
Merged

test(plugin-form): pin form section headings to the _sections convention (objectstack#5408)#3373
yinlianghui merged 1 commit into
mainfrom
claude/issue-5408-form-section-tabs-i18n

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Refs objectstack-ai/objectstack#5408(前提证伪,issue 由 PM 按证伪结论另行关闭 —— 见该单评论;本 PR 交付的是缺失的护栏测试,不是症状修复)

结论先行:issue 的前提在 origin/main 上不成立

objectstack#5408 断言「表单的 section tab strip 从不查 _sections,而 detail 页会查」。
对着 origin/main @ 68b6a28 核验:表单这一半早就接上了,而且是和 detail 页
同一个解析器、同一个 key 约定 objects.{objectName}._sections.{sectionName}.label,
miss 时回退 authored label。三处独立调用点:

路径调用点
页面态 tabbed / wizard / split / drawer / modal 分发packages/plugin-form/src/ObjectForm.tsx:165-167(tSec),5 个分支映射各调一次
模态内 tabbed + 堆叠 section + fieldGroups 派生 sectionpackages/plugin-form/src/ModalForm.tsx:588(sectionTitle)、:684
非 tabbed 的分组标题(stacked section divider)packages/plugin-form/src/ObjectForm.tsx:966-968

detail 页那一半在 packages/plugin-detail/src/renderers/record-details.tsx:199,
调的是同一个 useObjectLabel().sectionLabel(objectName, section.name, authoredLabel)
两半没有分岔。PM 补充里「plugin-form 全包零 _sections 命中」是 grep 了字面量
_sections 的结果 —— 解析发生在 @object-ui/i18nsectionLabel() 内部,包里
自然搜不到这个字符串。

实测(见下方证据)zh-CN bundle 带 _sections 时,tab strip 渲染的就是译文。

现场那条 English tab strip 的真正成因:元数据缺 name

_sections按 section 的 name 建索引的。HotCRM 的
src/views/case.view.ts 里,crm_case 默认 form view 的三个 section 只写了
label,没有 name:

form: {type: 'tabbed',sections: [{label: 'Case',columns: 2,fields: [...]},{label: 'SLA',columns: 2,fields: [...]},{label: 'Resolution',columns: 1,fields: [...]},],}

—— 正是 issue 里报的那条 Case / SLA / Resolution。而同一个 object 的
src/translations/zh-CN.tscrm_case._sections 键是
info/status/description(detail 页 case_detail.page.ts 的 section name,
所以 detail 页正常出译文)加 basic/origin/sla/resolution
(case.object.tsfieldGroups 键)。form view 那三个 section 一个 name
都没有,渲染器根本拿不到 key,只能回退 authored label。

这是 producer 侧的元数据缺口,不是 renderer 缺口。按 AGENTS.md #0.1(contract-first),
不能在渲染器里把 label slug 化去猜第二套 key —— 那会把一个错误约定固化成第二份
事实契约。正确修法在下游元数据(给 form view 的 section 补 name)+ 平台侧加一道
门(objectstack#5417)。

那这个 PR 交付什么

真正缺的东西:这套行为在 form 侧一条测试都没有(plugin-form 全包搜不到一处
sectionLabel 的断言)。也就是说,让 issue 作者担心的「两半分岔」在今天是完全没有
护栏的 —— 谁把 tSec 改回 s.label 都不会红。本 PR 补的就是这道护栏,不改一行
生产代码

六条钉子(packages/plugin-form/src/__tests__/sectionLabelI18n.test.tsx):

  1. 页面态 tabbed:tab strip 出译文(工单信息),同一次渲染里未翻译的 sla 回退
    authored SLA —— 译文路径和回退路径一次覆盖。
  2. 模态内 tabbed(formType: 'modal' + contentLayout: 'tabbed'):同上。
  3. 非 tabbed 的 stacked sectioned form:section divider 标题同样出译文
    (PM 点名的「FormSection 标题」那一半)。
  4. bundle 里完全没有 _sections 时,回退 authored label。
  5. parity 断言:直接用 detail 页调的那个 useObjectLabel().sectionLabel、按
    record-details.tsx 的同款入参形状算出期望值,断言 tab strip 文本与之逐项相等。
    form 侧一旦绕开这个解析器就红。
  6. 契约钉:section 只有 label 没有 name 时,即使 bundle 里存在长得很像的键,
    渲染出来的就是 authored label —— 钉住「key 只认 name,渲染器不猜」,也就是上面
    HotCRM 那个 case 的最小复现。

验证

pnpm --filter @object-ui/plugin-form test(容器级 flock + --maxWorkers=2):

 Test Files 33 passed (33)
Tests 322 passed (322)

33 = 原有 32 + 本 PR 新增 1,新文件确实进了这一轮(objectui#3288 的路径过滤坑,靠文件
计数而不是 -- --run path 确认)。单跑新文件 6 passed (6)
type-check 退出 0;lint 0 errors(500 条既有 no-explicit-any warning,与本 PR 无关)。

反向验证(先定方向再跑):把生产代码改坏,看测试往哪边走。

  • 只把 ObjectForm.tsxtSec 改成 s?.label —— 预测只红 2 条(用例 1 和 5),
    用例 2、3 应当仍绿,因为模态与 stacked 两条路各自还有独立的解析调用
    (ModalForm.sectionTitle / SimpleObjectForm)会把 name 再解析一次。实测:
    2 failed | 4 passed,失败正是用例 1、5(expected [ 'Case Info', 'SLA' ] to deeply equal [ '工单信息', 'SLA' ])。方向与预测一致。
  • 再把 ModalForm.tsx:588ObjectForm.tsx:966-968 两处一并改坏 —— 预测用例
    2、3 补红,用例 4、6 恒绿(它们不依赖解析器)。实测:4 failed | 2 passed,
    红的就是 1/2/3/5。

四条依赖解析器的用例全部可红,两条回退/契约钉在两次突变下都稳绿 —— 覆盖是活的,
不是「因为什么都没产出所以恰好通过」。改坏的文件已按原样还原,git diff 对生产代码为空。

无 changeset:纯测试,无用户可见变更。


Generated by Claude Code

…ntion (objectstack#5408)
objectstack#5408 reported that the create/edit form's section tab strip
ignores the `_sections` translations the detail page applies. Verified
against origin/main: it does not — `ObjectForm.tsx` (`tSec`),
`ModalForm.tsx` (`sectionTitle`) and `SimpleObjectForm`'s stacked path all
resolve `objects.<object>._sections.<section>.label` through the same
`useObjectLabel().sectionLabel` the detail page calls, falling back to the
authored label.
What was genuinely missing is coverage: not one test on the form side
pinned that behaviour, so nothing stopped the two halves from drifting
apart again — which is what the issue believed had already happened.
Adds six pins: page-mode tabbed, modal-hosted tabbed, stacked sectioned,
the no-bundle fallback, a parity assertion against the resolver the detail
page uses, and a contract pin that the lookup key is the section's stable
`name` and never a guess derived from its authored label.
No production code changed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 5, 2026 10:24am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-P8DoJyph.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.47KB3.09KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
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)477.09KB104.71KB
core (index.js)2.25KB0.80KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.23KB34.75KB
fields (index.js)227.16KB55.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.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.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)60.83KB17.25KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.01KB28.86KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)231.11KB56.98KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.54KB26.97KB
plugin-gantt (index.js)162.25KB39.55KB
plugin-grid (index.js)185.08KB49.04KB
plugin-kanban (index.js)47.89KB13.18KB
plugin-list (index.js)105.02KB25.36KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.55KB10.59KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.34KB2.82KB
plugin-view (index.js)83.67KB20.43KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
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)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
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-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 5, 2026 10:34
@yinlianghui
yinlianghui added this pull request to the merge queueAug 5, 2026
Merged via the queue into main with commit 0c9983fAug 5, 2026
15 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5408-form-section-tabs-i18n branch August 5, 2026 10:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yinlianghui@claude