Skip to content

fix(app-shell): record:details 段落设计器补上 name 这个 i18n 锚点位 (#3819) - #3911

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3819-sections-name-field
Aug 9, 2026
Merged

fix(app-shell): record:details 段落设计器补上 name 这个 i18n 锚点位 (#3819)#3911
yinlianghui merged 1 commit into
mainfrom
claude/issue-3819-sections-name-field

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3819

问题

record:details 的 sections 编辑器只给了 label / columns / fields,漏掉了 name。而 name 不是可选的装饰位,它是段落标题的 i18n 锚点 —— plugin-detailrecord-details.tsx 用它去解析 objects.{object}._sections.{name}.label,name 缺失时直接退回作者当时敲进去的那串字:

const translatedTitle = s.name && objectName
  ? sectionLabel(objectName, s.name, rawTitle ?? s.name)
  : rawTitle;

于是 Studio 里搭出来的每个 section 在结构上就不可翻译:所有 locale 下同一串英文,并且必然挂着上游 translation-section-name-missing 这条作者在设计器里修不掉的诊断。这个键此前只能靠手改 source 才能写上 —— 而"不必手改 source"正是设计器存在的理由。

前提复核(origin/main @ 50fa3766ebb2ebf2ec78c5d13b1d627e6a91696f)

三个锚点都还在,issue 前提成立:

  • previews/block-config.tsrecord:details sections itemFields 确实只有 label/columns/fields;
  • plugin-detail/src/renderers/record-details.tsx:199sectionLabel(objectName, s.name, …) 实读在;
  • packages/i18n/src/useObjectLabel.ts:419 的键约定({ns}.objects.{objectName}._sections.{sectionName}.label)在。

改法

新增 Name (i18n key) 文本位,放在每个 section 条目的第一个 —— 与 page:tabs(key)、page:accordion(value)一致:标识符在人类标签之前。snake_case 约定写在 placeholder 里,因为 BlockPropField 没有 description/pattern 这类能力位(与本套件已有的"每个 json 位必须带形状 placeholder"同一条理由)。

两个实现级子问题的判定

① 不做自动派生,也不新造校验。 实测 BlockPropFieldtext 只支持 placeholder,没有 pattern/validate 能力位;按分诊"没有就不为此单独造",不为一个字段单独造校验层,约定交给 placeholder 陈述、强制点仍在上游 lint 规则。派生能力其实存在(InspectorTextField 有一个注释写明"e.g. to derive a dependent field"的 onBlur 钩子),这里刻意不接:label 可能已经是译好的文案,甚至是内联 { en, 'zh-CN' } 映射(record-details 会走 pickLocalized),从它派生等于把某一个 locale 的文案冻进那个本该与 locale 无关的键里。更糟的是这种损坏看不见 —— 译文缺失时渲染器回退到 authored label,派生错的锚点渲染出来和 bug 本身一模一样,要等有人加第二个 locale 才暴露。

② 既有仅 label 的 section 不回填。 打开时锚点位留空、label 原样不动,作者不输入就什么都不写。这一条不需要代码:inspector 是纯读穿的,renderField 只在 commit 回调里写;反过来做回填要在 render 期打补丁,还会让一个只是被点开过的页面变成 dirty。

钉子

  • 配置面:record:details 的 section 条目覆盖率现在从 spec 自身的 RecordDetailsProps 形状推导,不再手列 —— 手写一句 expect(name).toBeDefined() 只能把今天这个洞钉住,spec 下次长出新的 section 键时它照样绿。另加三条:name 是 text 位、placeholder 陈述 snake_case、label 说明是 i18n 键、且排在 label 之前。
  • 交互面(PageBlockInspector.sectionName.test.tsx,仿同目录 visibleWhen 先例):输入位真的渲染出来、输入落进 properties.sections[i].name 且合并而非覆盖同条目其它键、多 section 时写对那一个、既有 label-only section 打开留空、仅渲染不产生任何 patch、改邻位 label 不会派生出 name
  • 刻意不钉 schema 拒绝:与 Page 块检查器提供的 hidden 条件框写的是 spec 不认识的键(canonical 是 visibleWhen) #3229visibleWhen 不同,这个面是宽松的 —— 实测 pin 版 @objectstack/spec@17.0.0-rc.5:PageSchema.parse 根本不拿 RecordDetailsProps 校验 properties,RecordDetailsProps 自己对 section 条目里的未知键也放行。所以无名 section 一直都能 parse,只是永远不可翻译。写一条"提交的 draft 能 parse"是绿得没有意义的断言,本单真正要钉的是这个键可达。测试文件头把这点写清了,免得下一个读者照抄错的判据。

反向验证(方向先判后跑,结果与预判一致)

预判 Red:去掉新 itemField → 9 条红(配置面 3:spec 覆盖率 pin 报 missing: ['name']、形状 pin、顺序 pin;交互面 6:所有定位锚点输入位的用例),15 条绿。实跑:

Tests  9 failed | 15 passed (24)
  × exposes an editor for every key the spec declares on a section
  × the `name` editor is a text box carrying the snake_case convention
  × lists `name` before `label` — the entry identity comes first
  × renders one name box per section
  × typing a name commits it to `properties.sections[i].name`
  × names the right section when several exist
  × the committed name satisfies the renderer guard that reaches the i18n lookup
  × a freshly added section offers an empty name box
  × opens the name box empty and leaves the label alone

AssertionError: section keys with no designer control: expected [ 'name' ] to deeply equal []

失败集合与预判逐条对上。如实说明:保持绿的 15 条里,子问题 ② 的两条(rendering the inspector writes nothing by itselfkeeps the section nameless until the author types one)在改动前也是绿的 —— 它们钉的是"不回填/不派生"这个无代码的决定,走 labelBox 而不是锚点位,因此按定义不可能因为删掉字段而变红。这不是缺陷,是那条决定的正确钉法。另有一条 a freshly added section offers an empty name box 初版只断言 Add 推入 {},删掉字段后会空绿(断言通过是因为什么都没产生);已补上锚点位读数,现在会红。

浏览器实证(preview gallery,无后端)

走真实授权路径:palette 加 record:details 块 → 画布选中 → Add section → 输入锚点。探针直接读 DesignerCard 的 draft state(React fiber 的 queue.lastRenderedState,memoizedState 会慢一帧):

STEP 1  block authored from palette = {"type":"record:details"}
STEP 2  inspector open | "Sections" panel = 1 | name boxes (no sections yet) = 0
STEP 3  after "Add section":
        "Name (i18n key)" label on screen = 1
        anchor boxes = 1 | placeholder = "snake_case, e.g. contact_info"
        value (empty = no backfill) = ""
        draft = {"type":"record:details","properties":{"sections":[{}]}}
STEP 4  label authored, anchor untouched:
        draft = {"type":"record:details","properties":{"sections":[{"label":"Contact info"}]}}
        anchor box value = ""
STEP 5  anchor typed:
        draft = {"type":"record:details","properties":{"sections":[{"label":"Contact info","name":"contact_info"}]}}
        box still shows = "contact_info"
STEP 6  second section — anchor boxes = 2 | values = ["contact_info",""]
        draft = {"type":"record:details","properties":{"sections":[{"label":"Contact info","name":"contact_info"},{}]}}

STEP 4 是子问题 ② 在真实 UI 里的读数:写了 label,name 仍不存在、输入位仍空 —— 没有派生。STEP 5 是"落进 schema"的读数(输入位是受控的,值留得住就说明真的进了 state)。placeholder 量了一下没有被截断:文本 206px / 输入内宽 243px,完整可读。

验证命令

  • 依赖构建:pnpm --filter '@object-ui/app-shell^...' build → 通过
  • 新钉子:pnpm exec vitest run 两个测试文件 --maxWorkers=2Test Files 2 passed | Tests 24 passed
  • 消费半径(12 个文件:previews/inspectors + plugin-detailrecordDetailsInputs.spec-parity / record-details / buildDefaultPageSchema)→ Test Files 12 passed | Tests 204 passed
  • 末次编辑后全仓 pnpm exec turbo run type-check --concurrency=278 successful, 78 total
  • node scripts/check-control-bytes.mjs → OK(3789 文件);改动文件另做 grep -naP 控制字节自查,零命中

边界

只改 previews/block-config.tsrecord:details 一条 + 测试 + changeset。上游 lint 规则本体没动;#3831(plugin-detail/fields)、#3134(表单布局设计面)零相交;content/docs/releases/** 未碰。


Generated by Claude Code

The `record:details` sections editor offered label/columns/fields and omitted
`name`, which is the section heading's i18n anchor: `record-details.tsx`
resolves the heading through `objects.<object>._sections.<name>.label` and falls
back to the authored string when `name` is absent. Every section built in Studio
was therefore untranslatable by construction and carried an upstream
`translation-section-name-missing` diagnostic no designer control could clear.

Adds the `Name (i18n key)` text field, first in each section entry (matching
page:tabs / page:accordion, where the identifier precedes the label). The
snake_case convention lives in the placeholder because `BlockPropField` has no
description/pattern affordance.

Deliberately not derived from `label` (a localized label would freeze one
locale's prose into a locale-independent key, invisibly — the renderer's
fallback makes a wrong anchor render exactly like the bug) and existing
label-only sections are not backfilled (the inspector is read-through; nothing
is written until the author types).

Section-entry coverage is now derived from the spec's `RecordDetailsProps` shape
instead of hand-listed, so the next section key the spec grows fails loudly here
rather than quietly never reaching the designer.
@vercel

vercel Bot commented Aug 9, 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 9, 2026 1:51am

Request Review

@github-actions github-actions Bot added the tests label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
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) 26.07KB 7.56KB
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.65KB 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) 482.39KB 106.34KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.97KB 56.74KB
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.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
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.32KB 1.64KB
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) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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

Copy link
Copy Markdown
Collaborator Author

✅ 验收(PM,session session_01GTRjn8xBqp75dk7kFupVRt)

实物核验:头 4e81e72f1,4 文件 +377/−1,实现面恰 18 行;block-config diff 全读 —— name 放首位循 page:tabs/accordion「标识符先于 label」惯例,「为何不从 label 派生」的锚点稳定性理由写成永久注释;trailer 0(claude/anthropic/co-authored 全零)。
CI 终态(独立复核):20 检查全部 completed,18 success + 2 skipped,零失败。

裁定要点:

  • 两个子问题的判定采纳且论证优于派发词:① 不派生 —— 不止「PM 倾向」,dev 给出机理:label 可能已是译文甚至内联 locale 映射,派生会把某 locale 文案冻进 locale 无关的键,且因缺译回退 authored label,派生错的锚点渲染出来与 bug 本身一模一样、要等第二个 locale 才暴露;顺带实测 BlockPropField 无 pattern/validate 能力位(不为单字段造)与派生钩子存在但刻意不接的现状。② 不回填 —— 实测 inspector 纯读穿,无需代码,零改动即正确。
  • 浏览器探针六步链完整(palette 加块 → Add section → 只写 label 不派生 → 输入落 schema → 第二 section 独立空锚)。
  • 「无意义的绿断言」判断正确:实测 pin 版 spec 对 section 未知键放行、PageSchema.parse 不校验 properties —— 刻意不写「draft 能 parse」这种恒绿断言并在测试头写明判据,防下一个人补上假覆盖。
  • 反向验证 9 红逐条对上;两条按设计恒绿的钉如实说明;一条初版空绿的钉自查补硬 —— 三种处理都对。

转 ready 并挂 auto-merge。越界 #3912(BlockPropField 无校验能力位,observation)与 #3913(块检查器 PROPERTIES 面 zh-CN 下英文,concrete,100+ label)归分诊席。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 01:59
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 2937bcf Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3819-sections-name-field branch August 9, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants