Skip to content

feat(record-detail): 记录详情页头部手动刷新按钮(原地刷新详情/相关/计数,不 reload 浏览器) - #3771

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-3460-record-header-refresh
Aug 8, 2026
Merged

feat(record-detail): 记录详情页头部手动刷新按钮(原地刷新详情/相关/计数,不 reload 浏览器)#3771
yinlianghui merged 2 commits into
mainfrom
claude/issue-3460-record-header-refresh

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3460

背景

MES 派工单场景:A 操作员停留在详情页期间,B 操作员对同一单据做了开工/报工,A 只能靠 F5 才能看到最新状态 —— 而 F5 会把当前页签、滚动位置、正在编辑的行内草稿一起丢掉。

管道本来就齐:#2269 的失效总线已实现 "refresh data, don't rebuild UI",RecordContext.refresh 也早已声明 —— 声明了、消费方就位、类型与 lint 全绿,却既没有生产者也没有任何 UI 去读它(declared ≠ enforced 的教科书形状)。本 PR 给它补上两端。

改动(3 个源文件 + 3 个测试文件 + 文档 + changeset)

  1. packages/app-shell/src/views/RecordDetailView.tsx —— 生产者
    提供 RecordContext.refresh = () => notifyDataChanged({ objectName: '*' })。用 '*' 而非本记录是唯一诚实的作用域:用户点刷新的动机就是"别人改了我看不见的数据",这次写入本客户端从未见过,因此不可能知道动了哪些对象。'*' 让主记录、每个相关子列表、页签计数徽标全部原地重拉。

  2. packages/components/src/renderers/layout/containers.tsx —— 消费者(page:header)
    宿主提供 refresh 时,在头部操作区最右端渲染 ⟳(维护者裁决的位置)。样式取裁决的 B 案:与「⋯」溢出触发器同款 outline 胶囊(同 variant / 同 size / 同内边距),整排 [开工][催办][编辑][⋯][⟳] 读作一个按钮家族。
    它是页面 chrome 而非动作管线成员,这一点有测试钉住,不靠约定:位置恒定(不随对象声明的动作数变化)、不占 maxVisible 预算因此永不会被折进「⋯」、不过 requiredPermissions 能力门控(重读一条已经在看的记录不是受限操作)。可访问名与 tooltip 复用现成的 common.refresh(十语言均有,零 i18n 改动),避免图标按钮在另外九种语言下仍是英文(与 objectstack#5407 修掉的是同一类问题)。总线是 fire-and-forget、没有完成信号,所以图标至少转 650ms,否则热后端下点击看起来"什么都没发生"。

  3. packages/plugin-detail/src/RelatedList.tsx —— 一行
    旧事件 objectui:related-changed 的监听器放行 '*' 通配,与 dataChangeMatches 语义对齐。此前它只比对自己的对象名,于是一次通配失效能到达页面上所有读者,唯独到不了相关列表 —— 这正是让 ⟳ 变成"半次刷新"的缺口。具体的外部对象名依旧被忽略(两个方向都钉了测试)。

宿主不提供 refresh 时,渲染结果与改动前完全一致。

范围(按 2026-08-06 维护者裁决)

  • 样式 B ✅ / 位置最右端 ✅ / 首期不含 DetailView ✅(plugin-detail/src/DetailView.tsx 一行未动)/ 自动刷新(WebSocket/轮询)挂起 ✅。
  • ⚠️ 裁决里"抽屉/分屏不加 ⟳"这一条,落地方式与 issue 正文的前提不同:正文称抽屉/分屏详情走 DetailView,所以"不改 DetailView"自动等于"抽屉里没有 ⟳"。实读代码并非如此 —— ObjectView.tsx(分屏预览 + 抽屉两处)、ObjectDataPage.tsxInterfaceListPage.tsx 都是把 RecordDetailView 本体带 embedded 挂进覆盖层,渲染的是同一个 page:header。若无条件提供 refresh,被裁决排除的那些 surface 反而会长出按钮。因此生产者按 embedded 收口:refresh={embedded ? undefined : handleManualRefresh},与该视图既有的 embedded 抑制(自动"返回列表"、breadcrumb)同一套逻辑。这条有专门测试,并做了反向验证。

验证

三包 type-check 全过,lint 0 error。新增 22 个测试,受影响三包全量:components + plugin-detail 149 文件 1159 passed,app-shell 296 文件 2645 passed(1 skipped,既有)。i18n 两个门(check:i18n-keys / check:i18n-drift)、check:control-bytescheck:changeset-*、新落地的 check-changeset-presence 全绿。

反向验证(先预测方向,再跑) —— 四次拆卸,方向全部与预测一致:

拆掉什么 预测 实测
RelatedList 恢复旧一行守卫 '*' 一例红,另 3 例绿 1 failed / 3 passed
page:header 去掉 hostRefresh 门(无条件渲染) 两条 host-opt-in 守卫红,其余 71 条 page-header 测试绿 2 failed / 71 passed(expected button to be null;expected 'DIV' to be 'HEADER')
生产者去掉 embedded 收口 抽屉抑制一例红;"embedded 不发总线"一例仍绿(它只挡挂载期误发,不是这个门的守卫) 1 failed / 4 passed,与预测一致
生产者整体去掉(回到无生产者) 三条 ⟳ 用例红,两条 embedded 用例绿 3 failed / 2 passed

未做浏览器复跑,如实说明取舍:issue 正文的浏览器结论是提单人的;仓内现成的 #3391 harness(apps/console/src/dev/DevRecordHeaderActions.tsx)自身是带 embedded 挂载的,不改它就照不到本路径,而改它超出本单声明的文件面。按钮的出现条件、位置、可访问名、旋转下限、回调、以及"点击后 findOne 再跑一次且 header DOM 节点不变(即原地刷新而非 remount)"都已由单测在真实渲染树上钉住,判断浏览器复跑不划算。


Generated by Claude Code

claude added 2 commits August 8, 2026 13:24
Operator A sits on a record detail page while operator B writes to the same
record; A's only way to see it was F5, which discards the open tab, the scroll
position and any in-progress inline edit along with the stale data.

The objectui#2269 invalidation bus already refetches every mounted reader in
place, and `RecordContext.refresh` was already declared for exactly this — with
no producer and no consumer. Give it both:

- RecordDetailView produces `refresh`, publishing
  `notifyDataChanged({ objectName: '*' })`. The wildcard is the honest scope: a
  user clicks refresh because SOMEONE ELSE wrote data this client never saw, so
  it cannot know which objects were touched. Standalone record route only —
  embedded hosts (drawer, split-pane) are unchanged in this phase.
- page:header renders the button at the far end of the row when the host
  provides `refresh`. Page chrome, not a header action: constant position, no
  `maxVisible` overflow budget, no capability gate. Same outline pill as the
  `...` overflow trigger so the row reads as one button family; accessible name
  from the existing `common.refresh` key; spins for a floor of 650ms because the
  bus is fire-and-forget.
- RelatedList accepts the `'*'` wildcard on the legacy
  `objectui:related-changed` event, matching `dataChangeMatches`. A concrete
  foreign object name is still ignored.

Hosts that provide no `refresh` render exactly as before.

Fixes #3460
@vercel

vercel Bot commented Aug 8, 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 8, 2026 1:37pm

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CvqJ0f_p.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.22KB 106.21KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.85KB 3.18KB
data-objectstack (index.js) 138.22KB 35.46KB
fields (index.js) 230.90KB 56.84KB
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.09KB 42.72KB
plugin-dashboard (index.js) 117.06KB 30.24KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.81KB 57.42KB
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.71KB 49.68KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 8, 2026 13:46
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 54233b1 Aug 8, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3460-record-header-refresh branch August 8, 2026 13:46
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.

feat(record-detail): 记录详情页头部增加手动刷新按钮(不 reload 浏览器即可刷新详情/相关/计数)

2 participants