Skip to content

fix(app-shell,plugin-detail): 记录页审批带渲染法定人数进度与会签分组 chips - #3163

Merged
os-zhuang merged 1 commit into
mainfrom
claude/v17-verification-defects-gnf9e6-approvals
Aug 1, 2026
Merged

fix(app-shell,plugin-detail): 记录页审批带渲染法定人数进度与会签分组 chips#3163
os-zhuang merged 1 commit into
mainfrom
claude/v17-verification-defects-gnf9e6-approvals

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

缺陷

在业务记录页(showcase_expense_report/AyG40_bAHSP_gi8T)上,审批面板的其它部分都是对的 —— 表头的 批准 / 驳回、Draft → Submitted → Approved → Reimbursed 管线、lockRecord审批中已锁定 徽标、撤回审批 —— 唯独法定人数进度按组分片一个都不渲染。抓页面拿到的信号是:

{"lockBadge": true, "approveBtn": true, "quorumHint": false, "groupChips": false}

后果:quorum 节点上审批人看不到还差几票,per_group(会签)节点上看不到哪些组已签、哪些还欠 —— 恰恰是决定"我这一票是不是就把这一步走完了"的那个事实。

根因(与 issue 的归因部分不符,以实测为准)

issue 说「服务端已经把需要的数据都发过来了,前端没消费」。结论方向是对的(framework 侧确实不需要改动),但"已经发过来了"这半句不准确 —— 对记录页这条链路,数据根本不在响应里。

实测追下来是这样的:

  1. framework 确实会把节点的 node_config_jsonbehavior / minApprovals / __approverGroups 名单)算成一个 decision_progress 块 —— { behavior, got, need, groups? },见 plugin-approvals/src/approval-service.tsattachDecisionProgress这是服务端算好的、和引擎实际裁决规则同源的计数,客户端不该自己再推一遍。
  2. 但这个块只在 getRequest 里挂approval-service.ts:3634)。listRequests故意跳过它的:每行都要额外查一次 sys_approval_action 做统计,而列表读可能返回几百行。
  3. 记录页表头走的是 useRecordApprovals,它读的是列表路由 GET /approvals/requests?object=…&recordId=…所以这条链路上 decision_progress 从来就没进过 payload —— 组件不是"忘了读",是拿到的对象里压根没有这个字段。

顺带确认:审批中心(Approval Center 抽屉,ApprovalsInboxPage渲染这两样的(objectui#2811 那次做的),因为它走 getRequest。缺的只有记录页这一面 —— 这也解释了为什么同一条 checklist 里 lockRecord 那一半(#2906/#2914)好好的:lock_record 是列表读就带的字段。

所以这不是"服务端契约错了",也不需要在消费端加兜底掩盖上游 —— 服务端契约是对的,只是把这份富化放在了单条读上。正确的修法是让消费端去服务端实际发布它的地方把它读回来。

改动

1. useRecordApprovals 补一次单条读(packages/app-shell/src/hooks/useRecordApprovals.ts

列表读之后,只针对唯一那条 pending 记录再发一次 GET /approvals/requests/:id,把单条读的富化合并回该行。只有 pending 行才有活的计数、也只有它驱动表头,所以恒定一次追加请求,不会随行数放大。

失败必须是无害的:请求出错、或返回的 id 和请求的对不上,就原样保留列表行 —— 这是纯展示富化,一个 500 不能把审批面板带下去;更不能凭空编一个计数,错的 "1 of 2" 比没有更糟。

2. 把它穿到审批带上(@object-ui/react@object-ui/plugin-detail

InlineEditProvider 新增 approvalProgress(类型 ApprovalProgress),DetailView 的审批带在原徽标旁边渲染:

  • quorum / unanimous:带 role="progressbar" 的标签 + 每一票一格的刻度条(Approvals — 1 of 2);
  • per_group(会签):Sign-off — 1 of 2 groups,外加每组一个 chip 标出谁已签(finance 1/1 ✓ / manager 0/1)。

渲染器保持 DataSource 无关 —— 计数由 host 从审批读里穿进来,组件不自己推导引擎的裁决规则。组名来自流程作者自己的配置(是数据不是文案),所以不需要为组名加任何 locale 字符串;新增的 3 个标签 key 已补齐全部 10 个语言包。

first_response 节点不带 decision_progress,行为完全不变 —— 那种节点一票即终局,"1 of 1" 的进度条是噪音不是信息。

验证证据

新增回归测试,给定真实服务端载荷断言两样东西确实渲染 / 确实被读到:

packages/app-shell/src/hooks/useRecordApprovals.quorum.test.tsx(6 例)—— 载荷用 showcase_committee_quorumquorum,3 人名单 minApprovals: 2)与 showcase_expense_signoffper_group,manager + finance)的真实形状:

  • 列表读不带的计数,hook 现在能拿到({ behavior: 'quorum', got: 1, need: 2 });
  • 断言它确实打了单条端点 /approvals/requests/req_committee_1
  • 单条读只做加法,lock_record / pending_approvers 等列表字段原样保留;
  • 会签的 groups[] 与每个待审批人的 pending_approver_groups 都被暴露;
  • 单条读失败时行原样保留、decision_progressundefined(不编造),且 canDecide 等既有能力不受影响;
  • 没有 pending 行时不发追加请求。

packages/plugin-detail/src/__tests__/DetailView.approvalBand.test.tsx 新增 7 例 —— 断言渲染输出本身:

  • Approvals — 1 of 2 与锁定徽标并存;
  • 计数以 role="progressbar"aria-valuenow=1 / aria-valuemax=2)暴露,是可访问的语义而非装饰;
  • lockRecord: false 的可编辑节点同样渲染计数(进度关乎决策,不关乎锁);
  • 会签渲染 Sign-off — 1 of 2 groups + finance 1/1 / manager 0/1 两个 chip;
  • 会签数的是不是票;
  • first_response(无 decision_progress)不渲染进度条 —— 老行为不变;
  • 审批结束后整条带子都不在,自然也没有残留的进度条。

闸门:

  • pnpm vitest run packages/plugin-detail packages/react → 64 files / 682 tests 全绿
  • pnpm vitest run packages/app-shell/src/hooks packages/app-shell/src/views/RecordDetailView → 23 files / 210 tests 全绿
  • pnpm vitest run packages/i18n → 17 files / 157 tests 全绿(含 10 语言包全量 key parity)
  • pnpm type-check → 78/78 tasks 全绿
  • 触碰文件 eslint 0 error(仅存量 no-explicit-any warning)

未覆盖(另案)

issue 里还提到 sys_approval_request 详情页把 待审批人 渲染成一串重复 user id 的逗号拼接。那是通用记录页渲染系统对象的问题(字段本身是 id 数组,没有 display 解析),和本 PR 的审批带是两条链路,未在此处一并处理。

定级

changeset 定 minor:这是可观察的渲染输出变化,且在 @object-ui/react 上新增了公开的 approvalProgress prop 与 ApprovalProgress 类型,不是既有面内的行为纠正。

备注:提交历史

容器重启中断了本任务,协调方把当时未提交的 18 个文件抢救成了一条 wip: rescue … 提交并首次推送了分支。按仓库纪律不做 force-push,故该提交信息保留在分支上;合并走 squash,落到 main 的是本 PR 标题与正文。

Fixes objectstack-ai/objectstack#4478


Generated by Claude Code

@vercel

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

Request Review

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.45KB 3.08KB
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.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
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) 475.95KB 104.41KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
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.44KB 10.66KB
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.90KB 12.35KB
plugin-charts (index.js) 60.53KB 17.12KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.86KB 28.81KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 224.52KB 54.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 184.69KB 48.91KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.82KB 25.30KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.47KB 10.57KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
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
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

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 13:38
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit a889e31 Aug 1, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/v17-verification-defects-gnf9e6-approvals branch August 1, 2026 13:39
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.

Console approvals: quorum progress and per-group chips do not render, though the server sends minApprovals / group / __nodeLabel

2 participants