Skip to content

fix(plugin-detail,types): record:highlights 尊重条目上声明的 readonly,不再让表头 chip 覆写平台维护的列 - #3356

Merged
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-5077-highlights-readonly
Aug 4, 2026
Merged

fix(plugin-detail,types): record:highlights 尊重条目上声明的 readonly,不再让表头 chip 覆写平台维护的列#3356
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-5077-highlights-readonly

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

修复 objectstack-ai/objectstack#5077(跨仓,故不用裸 Fixes #)。下游追踪:yinlianghui/hotcrm-heimao#61。

这个 PR 是草稿,且是有意不完整的 —— 见下方"需要维护者决策的两点"。objectui 侧的这一半在任何一种决策下都是必需的,先落地以便决策有具体的东西可依附。

问题

详情页表头 chip 由 record:highlights 渲染,支持双击行内编辑。HeaderHighlight 的可编辑性门 一直field.readonly:

const isReadonly = field.readonly === true || objectDefField?.readonly === true;

RecordHighlightsRenderer 的条目归一化把每个条目按固定四键重建:

typeof f === 'string' ? { name: f } : { name: f?.name, label: f?.label, icon: f?.icon, type: f?.type }

readonly 在那个检查的 上一层 就被丢掉了,所以这个门永远无法从作者写的元数据触发 —— 只有 schemaField.readonly 能触发,而那条路对本 issue 的场景是死路:把对象字段标 readonly 会连带让跨对象 hook 自己的回写被 stripReadonlyFields 剥掉(objectstack-ai/objectstack#2948)。

结果:hook 维护的汇总列(rollup、审批回写的等级)在表头 chip 上可以被任何人手工改掉。实测的破坏是持久的 —— 值一直错到某次不相关的子行改动重新触发 rollup 为止。

改动

  • packages/plugin-detail/src/renderers/record-highlights.tsx —— 归一化时带上 readonly。仍然逐键重建而不是展开对象,条目形状保持封闭,未声明的键依旧不会被静默转发。
  • packages/types/src/views.ts / record-components.ts —— 把 readonly?: boolean 声明到 HighlightFieldRecordHighlightsComponentProps.fields[] 条目联合类型上,与 DetailViewField.readonly 对齐。
  • packages/plugin-detail/src/HeaderHighlight.tsx —— 既然 readonly 已是声明键,去掉门里的 (field as any) 断言。

redactFields / enforceFieldSecurity 未动:那两个是移除 chip,而这里要的是锁定一个"存在的意义就是被看见"的值。

与 issue 评论的实测偏差(重要)

2026-08-04 的浏览器实测评论提出的"建议 2"说:可编辑性门解析有效 type 的来源与显示渲染器不同,所以作者写的 type: 'formula' 到不了门上。在 HEAD 上这一条不成立。 编辑前我先跑了探针测量:

resolvedType = field.type || objectDefField?.type   // 门和显示渲染器选择读的是同一个值

作者写的 type: 'formula' | 'summary' | 'rollup' | 'auto_number' 已经能正确禁用行内编辑。所以本 PR 里那 8 个 type 相关的用例是回归钉(pin tests),不是修复 —— 一旦将来有人再把门和渲染器选择拆成两套 type 解析,它们会立刻失败。

评论里"两个消费者对同一个字段意见不一致"的直觉是对的,只是方向反了:真正的洞是作者写的显示 type 会 放宽 门。已另行归档,见下。

需要维护者决策的两点(均已作为未指派 issue 归档)

  1. RecordHighlightsField does not declare readonly, so the spec silently strips the key the chip gate reads objectstack#5176 —— spec 没有声明 readonly RecordHighlightsField 是恰好 {name,label?,icon?,type?} 四键,非 .strict(),所以 RecordHighlightsProps.parse()静默剥掉 readonly(已实测)。今天本 PR 能端到端生效,只是因为 ComponentPropsMap 目前在加载路径上没有消费者、PageComponentSchema.propertiesz.record(z.string(), z.unknown()) 原样透传。协议 15 已经把三个 UI schema 翻成 .strict()(ADR-0089 D3a),方向很明确;等 props map 真的接入校验那天,作者写的 readonly 要么静默消失(chip 悄悄恢复可编辑,本 issue 的破坏原样回来,且没有任何诊断),要么直接 parse 报错。按 AGENTS.md #0.1,渲染器读一个 spec 未声明的键正是要避免的"宽容消费者"。建议在 spec 声明该键,并同步进 block inputssdui.manifest.json,让 AI 作者读得到。
  2. An authored display type can WIDEN inline-edit on a computed field — type: 'number' on a rollup unlocks the chip #3355 —— 作者写的显示 type 会放宽计算字段的编辑门。 对象声明 score: { type: 'formula' },作者为了排版写 { name: 'score', type: 'number' },chip 就变成可编辑(实测)。DetailSection 有完全相同的形状,所以要改必须两处一起改,否则又会出现 fieldEnrichment.ts 当初就是为了消除的漂移。这正是上游 app 的真实配置(他们为绕过 Console: percent display renderer's fixed-width progress bar clips the value in a record-highlights chip (33% renders as 3) objectstack#5066 给一个 rollup 写了 type: 'number'),也就是说实测到的那次数据破坏走的是这条路径。建议改成"只收窄不放宽"。

两点都超出本单范围(本单 = 实测评论的建议 1 + 2),故未在此 PR 内修改。

验证

npx vitest run packages/plugin-detail packages/types --maxWorkers=2
  Test Files  69 passed (69)
       Tests  773 passed (773)

pnpm --filter @object-ui/plugin-detail --filter @object-ui/types run type-check
  packages/types type-check: Done
  packages/plugin-detail type-check: Done

npx eslint <5 个改动文件>
  ✖ 33 problems (0 errors, 33 warnings)     # warning 全部是改动行之外既有的 any/unused

新增用例 10 个(RecordHighlightsRenderer.readonly.test.tsx),覆盖作者元数据 → chip 的整条路径而非孤立的门。已验证其防回归有效性 —— 临时撤掉归一化里的那一行后:

 × renders `{ name, readonly: true }` as NON-editable
 × keeps `readonly` per-entry — one locked chip does not lock its siblings
 Tests  2 failed | 8 passed (10)

恰好是依赖修复的 2 个失败,8 个 type 钉用例照常通过 —— 与"建议 2 在 HEAD 上已成立"的测量互相印证。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9


Generated by Claude Code

claude added 2 commits August 4, 2026 07:40
…` entry normalization (objectstack#5077)

WIP checkpoint — code + tests complete, changeset/typecheck/lint pending.

HeaderHighlight's editability gate has always consulted `field.readonly`, but
RecordHighlightsRenderer rebuilt each authored entry from a fixed four-key list
({name,label,icon,type}), dropping `readonly` one layer BEFORE the check that
would honour it. The gate could therefore never fire from authored metadata,
leaving hook-maintained columns (rollups, approval-written grades) inline
editable on the detail-page header strip — a user could overwrite a computed
value and it stayed corrupted until an unrelated child-row touch re-fired the
rollup (downstream yinlianghui/hotcrm-heimao#61).

- record-highlights.tsx: copy `readonly` through normalization.
- types: declare `readonly?: boolean` on `HighlightField` and on the
  `RecordHighlightsComponentProps.fields[]` entry union, mirroring
  `DetailViewField.readonly`; drops an `any` cast in the gate.
- tests: 10 cases pinning the authored-metadata -> chip path.

MEASURED DEVIATION from the issue's 2026-08-04 comment (suggested fix 2): at
HEAD the editability gate ALREADY reads the authored entry type
(`resolvedType = field.type || objectDefField?.type`), the same resolution the
display-renderer selection uses, so an authored `type: 'formula' | 'summary' |
'rollup' | 'auto_number'` already disables inline edit. Verified by probe before
editing; the eight type-gate tests here are therefore PIN tests, not fixes.

REMAINING STEPS: (1) .changeset patch entry; (2) pnpm typecheck + lint on
plugin-detail/types; (3) draft PR. Two open questions for the maintainer are
recorded in the PR body — `readonly` is not declared in @objectstack/spec's
`RecordHighlightsField`, and an authored display `type` can currently WIDEN
editability on a computed object field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Also records the two out-of-scope findings filed while measuring:
objectstack-ai/objectstack#5176 (spec does not declare `readonly` on
`RecordHighlightsField`, so `RecordHighlightsProps.parse()` strips it) and
#3355 (an authored display `type` can widen inline-edit
on a computed field).

Verified: plugin-detail + types type-check Done; 773 tests pass; eslint on the
changed files reports 0 errors.

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

vercel Bot commented Aug 4, 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 4, 2026 7:45am

Request Review

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 4, 2026 07:47
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 23018cc Aug 4, 2026
16 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-5077-highlights-readonly branch August 4, 2026 07:47
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-Br1ZjntT.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) 477.09KB 104.71KB
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) 226.73KB 55.54KB
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) 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.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.71KB 56.84KB
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

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 4, 2026
…ever widens it (objectstack-ai#3355) (objectstack-ai#3357)

Both detail-surface editability gates resolved ONE effective type with display
precedence (`viewFieldType || objectFieldType`), so an authored non-computed
`type` erased the object's `formula`/`summary`/`rollup`/`auto_number`
declaration from the gate's view and made a machine-owned column inline
editable.

That is the shipped configuration behind objectstack#5077: the reporter writes
`{ name: 'supply_share', type: 'number' }` purely to fix formatting (a
workaround for objectstack#5066) over a hook-maintained ROLLUP. The header chip
became writable, the rollup was overwritten by hand and stayed corrupted until
an unrelated child-row touch re-fired it (yinlianghui/hotcrm-heimao#61).

The gate now takes the UNION of the two types: non-editable if the authored
entry type OR the object field's type is computed. Renderer/editor selection
keeps the old precedence, so the display is unchanged — only who may write.

- fieldEnrichment: new `isComputedFieldType(viewFieldType, objectFieldType)`,
  the ONE definition both gates call; `TEXTUAL_REF_FALLBACK_TYPES` moved here
  beside it (still re-exported from InlineFieldInput, public name unchanged) so
  the renderer fallback and the two gates read one set.
- HeaderHighlight / DetailSection: gates pass the authored type and the object
  type separately instead of the collapsed `resolvedType`.
- tests: 23 cases across both surfaces — the reporter's exact config named,
  every computed object type under an authored `number`, narrowing still
  working, plain-field controls, and the objectstack-ai#3356 `readonly` regression guard.
  13 of them fail against the pre-objectstack-ai#3355 precedence logic.


Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants