Skip to content

feat(core,components,fields): labelling 扩为三值闭枚举,display 四 + grid 可编辑态不再丢 host id (#4857) - #5036

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4857-labelling-display
Aug 17, 2026
Merged

feat(core,components,fields): labelling 扩为三值闭枚举,display 四 + grid 可编辑态不再丢 host id (#4857)#5036
yinlianghui merged 1 commit into
mainfrom
claude/issue-4857-labelling-display

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4857

实施依据

2026-08-17 05:03Z 维护者裁定(评论 5312059193,与 #4871 合裁)+ 06:36Z 实测更正(评论 5312685254)。卡面三选不再开放:方案 1(disabled→readonly 重映射)被裁否,ObjectForm.tsx:687 一字未动;slider / signature 已由 PR #4933 交付,本 PR 不触碰;剩余五行 = display 四 + grid。

变更

1. 声明扩展(公开契约键,packages/core)

ComponentMeta.labelling 从二值扩为三值闭枚举 'control' | 'group' | 'display' —— 全仓唯一词表,即合裁给「host 如何得知 widget 将渲染什么」的统一答案,⛔ host 本地变体。'display' 语义写入注释:渲染面在任何状态下都是纯展示、无可聚焦控件;host 不发 label 的 for(任何状态下都不存在 labelable 元素可指),改走 #4788 已裁的容器通道(id + aria-labelledby + aria-describedby + role="group"),且与 'group' 不同,widget 侧不消费任何键 —— host 容器即命名面。

zod 侧核查:全仓不存在 ComponentMeta 的 zod schema(packages/core@objectstack/spec 均无 labelling 相关 schema),无可同步项。

2. display 四接 host 容器(packages/components + packages/fields)

formula / summary / auto_number / vector 声明 'display'。form renderer 的 host 门从「readonly === true」扩为「readonly === true 声明 'display'」:

3. grid:先重测,实测推翻卡面预设后按复合体落地

06:36Z 更正明言勿按 slider/signature 外推、grid 需重测。同套 probe(真 form renderer + 裸注册,description 设置,编辑态,RESOLVES-LABELABLE 口径)实测 origin/main e71c854ce:

bare(无 columns) for=DANGLING hostIdEl=NONE consumers=0 focusables=[button]
realistic(列+行)for=DANGLING hostIdEl=NONE consumers=0 focusables=[drag, input(Item),
                  input(Qty), button(Duplicate row), button(Remove row),
                  input(Item), input(Qty), button(Add)]

读数裁决:bare 配置唯一 focusable 是辅助动作 "Add line" button —— 它确实 labelable,但把 host id 落上去意味着字段 label 命名「加行」动作、点 label 会插行(label 激活转发);realistic 配置是 8 个 focusable 的单元格输入矩阵。即 grid 是复合体(address 形态),「其控件是单个 labelable button」的前提被实测推翻,方案 3 单用不成立。落地:声明 labelling: 'group',root 容器按 CheckboxesField 的键位逐键消费 host 通道 —— 编辑/list 态容器收 id、label IDREF、describedby,namearia-invalid#3291/#3318 界线扣下(grid 的 invalid 本就按单元格上报;aria-invalid 轴归 #3318 账本,本 PR 前后都不落任何 host aria-invalid,该轴零变化);readonly 分支(替换性表格,无字段输入)走 toHostGroupProps('instead-of-the-inputs'),名字与描述同落容器。

4. 配套门:registered ⇒ declared

FIELD_TYPES_GROUP_LABELLED(Set)升级为 FIELD_WIDGET_LABELLING —— 以 fieldWidgetMap 字面键联合为键的穷尽 Record(#3935 的 Record 形态,并照 group-labelling-declaration.test 既有集合断言扩展,未另起炉灶):往 map 加 widget 而不定 labelling 是编译期红(tsc 报缺键),而非静默退回 control 路径 —— 裁定点名的漏声明陷阱。运行时 meta 侧保留 #3961 已钉的「'control' 以缺省拼写」约定(第三方 widget 本就以缺省进入 control 路径,host 读法不因此双轨;Record 里的 'control' 条目仍是强制决定 ——「被缺省」与「决定用缺省」是两个事实)。声明测试新增:display 逐项 + 精确集合断言、record↔FORM_FIELD_TYPES 键位奇偶断言、record↔注册 meta 逐键一致断言。Record 已导出,供姊妹单 #4871 的 metadata-admin 宿主复用同一词表。

测试

  • 新增 packages/fields/src/__tests__/display-grid-host-channels-e2e.test.tsx:display 四可编辑态(含 disabled: true 的真实 ObjectForm 形态)+ readonly 态、grid 编辑/readonly/无 label 三态、control 绿对照;
  • packages/components form-readonly-host-group.test.tsx 增补 display 声明 describe(编辑态包裹、disabled 路径、双臂同真仅一层、未声明回退钉、无 label 不包);
  • readonly-host-plumbing-e2e.test.tsx 中 formula 编辑态残留钉按其自述「后改需是 deliberate 的」升级为未声明回退钉(该文件裸注册无声明,断言不变,语义改为钉住门要防的静默退化形状);
  • 全量:fields+components 3121 通过、plugin-form/plugin-detail/plugin-grid 1940 通过、app-shell 3952 通过(1 例既有 skip),仓根 turbo run type-check 通过,check-control-bytes 通过,改动文件 eslint 0 error。

反向验证(先书面预判,commit 后变异,checkout 还原)

  1. 撤 display 分支(保留声明):预判 fields e2e 可编辑态 5 例红于 expect(host).not.toBeNull()、components 新 describe 2 例红,readonly 与 grid 全绿 —— 实测 7 红 24 绿,与预判一致。
  2. 从 Record 摘 vector 声明:预判 tsc 红(缺键)+ 声明测试红且点名 vector;实测 tsc TS2741: Property 'vector' is missing、vitest 3 红(逐项 + 精确集合 + registered⇒declared 奇偶)—— 比 PR fix(metadata-admin): 审计面板锁状态列补齐中文,词表键绑 lockState union (#5004) #5023 变异①先例的「tsc 红 vitest 绿」更强一档:本门在 Record 之外另有读 Record 的运行时奇偶断言,vitest 同红。
  3. formula 错标 'control':预判 tsc 绿、声明测试的 display 逐项与精确集合断言红(record↔meta 奇偶断言绿 —— 两者一致地错),e2e 绿(裸注册硬编码 'display',不读 Record)。实测 tsc exit 0、恰好 2 红,与预判一致。声明↔渲染面一致性由两文件合取持有:声明文件钉注册边界(本变异被它抓住),e2e 文件钉「声明 ⇒ 渲染」—— [fields] 复合/分组 field widget 丢弃或错投 host 下发的控件 id:address / geolocation 的表单 label for 悬空,checkboxes / radio / rating / file 的 for 落在不可 label 的 div 上 #3961 起两文件自述的既有分工,无新缺口,故不另立 finding。

边界

changeset:core / components / fields 三包 minor(公开契约键扩展,向后兼容 —— 对照 #3985/#3832 先例;仓规 fixed group ⛔ major)。


Generated by Claude Code

… + grid stop dropping host ids in editable state

ComponentMeta.labelling extends to the closed three-value enum
'control' | 'group' | 'display' (objectui#4857, ruled jointly with
objectui#4871): 'display' declares a widget whose whole surface is a pure
display in EVERY state, so the form renderer applies the objectui#4788 host
container (id + aria-labelledby + aria-describedby + role="group") in the
editable state too, while the readonly === true arm keeps its exact #4788
semantics. formula / summary / auto_number / vector declare 'display' — on
the real object-form path they arrive `disabled`, never `readonly` (a
deliberate distinction left untouched; option 1 was rejected), so their
labels' `for` dangled in every editable form.

grid was re-measured before classification: bare config offers one focusable
— the auxiliary "Add line" button, which `for` must not address (a label
click would insert a row) — and realistic configs are tables of per-cell
inputs. A composite, so it declares labelling: 'group' and its root
container consumes the host id, name and description (CheckboxesField's
split: `name` and `aria-invalid` withheld).

Companion registry gate: FIELD_WIDGET_LABELLING is a Record keyed by the
widget map's literal key union — registering a widget without a labelling
decision is a compile error, not a silent fall-through to the dangling-`for`
path — with runtime parity assertions in the declaration test.

Fixes #4857

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
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) 40.21KB 10.79KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.71KB 1.22KB
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.02KB 0.88KB
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) 502.19KB 112.21KB
core (index.js) 4.11KB 1.62KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.03KB 44.08KB
fields (index.js) 234.25KB 58.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 39.16KB 10.97KB
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) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
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.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 241.12KB 60.43KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 120.42KB 29.03KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.61KB 53.03KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.17KB 26.99KB
plugin-map (index.js) 18.72KB 6.09KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
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.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
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 (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 3.05KB 1.52KB
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 验收 ✅ ACCEPT(#4857,批次 21 —— 05:03Z 合裁 + 06:36Z 更正的实施件,声明切片按裁定由指定模型席执行)

实物核验(merge-base e71c854ce 与报告一致):15 files,+830/−104 —— ComponentMeta.labelling 三值闭枚举唯一落点 Registry.ts(zod 侧实测无对应 schema,如实记零而非造一份);display 四声明接 #4788 容器(host 门 = readonly===true 声明 'display',readonly 臂语义不动、ObjectForm.tsx:687 一字未动);配套门升级为以 fieldWidgetMap 键联合为键的穷尽 Record(漏声明编译期红)+ 运行时奇偶断言,导出供 #4871 复用。slider/signature 各 2 行为声明表迁移,行为零触碰(PR4933 交付保全)。模型标识 msg/diff 双 0;releases/ 零触碰。

CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。

grid 重测裁定采信:卡面「控件是单个 labelable button」被同套 probe 推翻 —— bare grid 唯一 button 是加行动作,label 落上去等于命名「加行」且点 label 插行;realistic 是 8 焦点复合体。按裁定「先重测勿外推」改声明 'group'(address 形态,name/aria-invalid 扣下与 #3318 账本一致)—— 这正是 06:36Z 更正预防的那类错,重测把它抓住了。

反向验证读数:①撤 display 分支 7 红且落点精确;②Record 摘键 → tsc TS2741 + vitest 3 红 —— 如实归类为比 PR5023 变异①先例强一档(本门另有运行时奇偶断言);③错标 'control' → 恰 2 红,且「声明↔渲染面一致性有没有人管」被两文件分工(声明文件抓注册边界、e2e 抓声明⇒渲染)实测确认,无新缺口不立 finding —— 变异③正是派发词要求探测的那个缺口,答案是实测出的「有人管」。

#4871 于本 PR 落 main 后解锁回队(其 meta 层复用本 PR 导出的 Record 与类型)。undraft + auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 18:51
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 167ec42 Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4857-labelling-display branch August 17, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants