Repository navigation
审批界面(Approval Center)优化评估:徽章竖排 / 决策确认 / 进度可视化 / 动作标签 i18n #2762
Description
Consolidated UX/design evaluation of the Approval Center (审批中心 inbox + decision drawer), found while dogfooding the v16.0 approval features. The surface is now demonstrable out of the box via objectstack-ai/objectstack#3364 (seeds two real pending requests — one M-of-N quorum, one finance∧legal 会签 — routed to the dev admin), which is what made this review possible. Part of the objectstack-ai/objectstack#3358 verification sweep.
What's already good (don't regress): metadata-driven DeclaredActionsBar (new decision capabilities need zero UI code), mobile card layout with a correct button hierarchy, keyboard flow (j/k/Enter/a/r), ?request= deep-link opens the drawer, localized chrome + toasts, server-computed progress + viewer gating.
Screens reviewed: inbox list (desktop + mobile), decision drawer, one live Approve. Components: packages/app-shell/src/views/DeclaredActionsBar.tsx + the approval-center/inbox + decision-drawer components in packages/app-shell.
P0 — defects / risk
- Status badge stacks vertically —
待审批renders as 待/审/批 on both desktop and mobile (where there's ample room), so it's a badge-component width/clip constraint, not just a narrow column. Give the status pillwhite-space: nowrap+ a sensible min-width; widen the 状态 column. - One-click Approve/Reject with no confirmation — the row's right-edge ✓/✕ icons (and the mobile card buttons) execute an irreversible decision instantly, with no confirm and no comment/attachment. The right-edge icons are small and easy to misclick. Add a confirm step (at least for Reject) or route through the composer.
- Declared action labels not localized — the drawer action bar shows English Approve / Reject / Reassign / Send back / Request info in a zh-CN workspace, while the same page's inbox buttons are 通过 / 拒绝. Mixed + inconsistent.
DeclaredActionsBarrenders the action'slabelverbatim; thesys_approval_requestdecision actions need a translation bundle (framework-side dependency — see below).
P1 — high-impact UX
- Progress is text-only and self-contradictory — "Approvals — 0 of 1" under a title that says "2 of 3", no progress bar, no per-group tick badges (the release notes promised per-group ticks for 会签). Render a segmented progress indicator; for
per_groupshow per-group state (Finance ✓ / Legal ○); reconcile the 0/1-vs-2/3 wording (e.g. "0/2 approvals · 1 eligible approver"). - Approver chips lack group/role context — the drawer shows duplicate identical "Dev Admin" chips with no indication of which group (finance/legal/manager) each represents. Dedupe and label each chip with its group — essential for 会签 comprehension.
- No decision-relevant data in the list — the queue shows no amount / urgency, so a reviewer must open each request to triage. Surface a key field (e.g. amount) inline; add sort by amount / age; consider a priority indicator.
- Empty 申请人 (Applicant) column — system/flow-submitted requests show a bare person icon + "—" and the header wraps to 申请/人. Show the flow origin ("流程发起" / System) or hide the column when there's no submitter; give the header room.
- Flat action hierarchy in the drawer — five equal-weight outlined buttons; the primary Approve doesn't stand out. Make Approve primary/filled, Reject destructive, the rest tertiary/overflow. (The mobile card already does this — align desktop to it.)
P2 — polish
- Density / whitespace — a full-width table with 2 rows leaves large empty space; 审批事项 is over-wide while 状态 is cramped (the P0 badge trigger). Rebalance columns; cap content width; better empty/low-count state.
- Record-card labels & emphasis — "Owner Id" reads awkwardly (should be "Owner"); the decision-critical amount is buried bottom-right. Lead with the amount.
- Light mode — forcing
prefers-color-scheme: lightdid not change the theme; appears dark-only. Support light mode or document dark-only. - Batch actions — there's a select-all + per-row select but no bulk-action toolbar. Surface bulk Approve/Reject when rows are selected.
Framework-side dependency: P0-3 (localized action labels) needs @objectstack/plugin-approvals to ship a translation bundle for the approval_approve / approval_reject / approval_reassign / /revise / request-info / remind / recall / resubmit action labels — tracked in objectstack-ai/framework.
Refs: objectstack-ai/objectstack#3358 (sweep), objectstack-ai/objectstack#3364 (demo seeds).
Activity
os-zhuang commentedon Jul 22, 2026
Opened two draft PRs covering the four items in this issue's title:
whitespace-nowrap+ 状态 column min-width), P0-2 (quick Approve now confirms on desktop row / mobile card /akey, mirroring Reject), P1-1 (segmented decision-progress bar, per-group ✓/○ chips, eligible-approver count, pager reworded to "Request N of M"), and the UI half of P0-3 (DeclaredActionsBarresolves_actions.<name>.*translations; newapprovalsInboxkeys in all ten locales).@objectstack/plugin-approvalsnow ships_actionstranslations (zh-CN / ja-JP / es-ES) for all eight decision actions, plus the other gaps the extract merge surfaced (returnedstatus option, audit-action options,attachmentsfield, view empty states).The remaining P1/P2 items (approver-chip group context, decision data in the list, empty 申请人 column, drawer action hierarchy, density, record-card emphasis, light mode) are untouched here and still open.
Note: CI on both PRs is currently red due to an org-wide Actions runner outage (jobs die in ~2s with no runner assigned; latest
mainpush runs show the same) — will re-kick when runners are back.Generated by Claude Code
fix(approvals): Approval Center UX pass — badge nowrap, approve confi…
fix(approvals): Approval Center triage + drawer readability pass (#2762…
fix(approvals): Approval Center density + amount emphasis (#2762 P2) (#…
fix(app-shell): localize the two DeclaredActionsBar strings that bypa…
os-zhuang commentedon Aug 6, 2026
分诊:⚠️ 限定范围:缺陷半边;两项产品决策与四项跨仓件排除)
pm:queue(⛔ objectui 仓内单不打
domain:*(#5468)。过时前提检查
PR #2789 已于 2026-07-24 12:12Z MERGED(
418d61f)—— 正文清单里的 P0-1(徽章竖排)、P0-2(一键决策无确认)、P1-1(进度可视化) 与 P0-3 的 UI 半边(DeclaredActionsBar走_actions.<name>.*翻译 + 十语言approvalsInbox键)均已交付。P0-3 的 framework 半边走的是 objectstack#3416。⇒ 正文的三个 P0 全部过期,本单剩下的是 P1/P2 尾巴,定级必须按尾巴而不是按「三个 P0 未修」来读。入队范围(dev 请只做这些 —— 均为「已声明的能力没兑现」形状)
__approverGroups,数据在,UI 没用⛔ 明确排除(不在本次范围)
prefers-color-scheme: light无效,疑似 dark-only)—— 「支持浅色」与「文档写明 dark-only」是两种产品承诺,需要维护者一次口径,不是 dev 能顺手定的。若座位要推进,单独立needs-user-decision卡。从 #2678 转记过来的四项(该单本轮已关,见下)
#2678 的 P1.5 里有四项不被本单正文覆盖,按查重收敛把它们的唯一入口并到这里:
getRequest返回groups_progress: [{group, need, got, satisfied}]+behavior/min_approvals,客户端通用渲染 —— 这才是 P1「审批人 chip 无组上下文」与「进度自相矛盾」的治本形状(服务端算、客户端渲)。落 framework ⇒ 取用时按跨座位转移协议在 objectstack 立单,本单侧写Blocked-by:。actionUrl带?request=<id>,收件箱识别 query param 自动开抽屉(objectui 侧的?request=语义已存在,缺的是 framework 侧的 URL 生成)。sys_file解析文件名。纯 objectui,可直接做。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
清 assignee — objectstack#11196 维护者裁决 2026-08-23「A:本席清 agent 残留」,skills 席 session
757f0dc0-5dae-51d7-9053-c15c57ebfe3b。证据:assignee 自 07-20 立卡起即在;其交付 PR #2789 已于 07-24 合并,08-06 分诊按剩余缺陷半边重新限定范围入pm:queue——原会话早收束,assignee 为残留(#11196 根因 4)。标签未动;认领席按协议 assign + 声明即可。Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
Claim: PM session
session_011SfZeFWrhGLHmfq61xbz4q(domain:uiexecution seat) — branchclaude/issue-2762-approval-center-p1-tail.pm:queue→pm:dispatchedin the same label write.⛔⛔ READ THIS BEFORE THE BODY — the body's checklist is OUT OF DATE
The three P0 items in the issue body are DELIVERED and still render as unchecked boxes. A dev who works the body will re-implement merged work.
The 2026-08-06 triage comment is the authoritative scope, and it recorded the stale-premise check that this dispatch re-verified:
⭐ This card is the single best example this lane has of why the gate reads the thread, not the body: labels say state, the thread says content, and here the body actively lies about what is left.
Pre-dispatch gate
1. Thread read. All three comments read. Scope taken from the 2026-08-06 triage comment, not the body.
2. Assignee. Cleared 2026-08-23 as agent residue under the objectstack#11196 maintainer ruling — the original session had long since ended and its PR (#2789) had already merged. This card is genuinely unclaimed; that clearing is not another seat's live claim.
3. Shadow check. Nothing in flight touches
packages/app-shell/src/views/.#6293's PR (#6384,app-shell/.../inspectors/ConditionBuilder.tsx) is enqueued and merging — a different subtree, no hold, but mergeorigin/mainagain before you push.4. Premise spot-check @
origin/main:__approverGroupsis present atpackages/app-shell/src/hooks/useRecordApprovals.ts:78— confirming triage's reading that the data is there and the UI does not use it. That is the shape of every item below: a declared capability not delivered.In scope — exactly these four, no more
per_group) scenario a user cannot tell "finance approved / legal pending". The backend already snapshots__approverGroups— the data is there, the UI ignores it.⛔ Explicitly EXCLUDED by triage — do not touch
prefers-color-scheme: lightmeasured ineffective, suspected dark-only. "We support light mode" vs "docs say dark-only" are two different product promises and need one maintainer ruling. ⛔ Not a dev's call. If you confirm the measurement, report it — the PM files aneeds-user-decisioncard.Tests
origin/main, capture it failing, then passing. Both readings in the PR body, labelled per item.DeclaredActionsBar, mobile card layout and button hierarchy, the j/k/Enter/a/r keyboard flow,?request=deep-link, localized chrome and toasts, server-computed progress and viewer gating. If your change touches any of those paths, pin them.Ground rules
Worktree-first:
git worktree add ../objectui-2762 -b claude/issue-2762-approval-center-p1-tail main && cd ../objectui-2762 && pnpm install. ⛔ Nevergit stash— the stack lives in the common.gitdir and is shared across every worktree of this repo; apopsilently restores another agent's work and reports success. ⛔ Never touchcontent/docs/releases/. Changeset:patch, one entry per package changed. Run the repo's fast checks before pushing. Open the PR as draft withFixes objectstack-ai/objectui#2762and report back — the PM lands it, ⛔ never self-merge.If the four items turn out to be fewer than four still-live defects, stop and report with the evidence — this card has already had one round of its scope evaporate under a stale-premise check, and a second is entirely possible.
Size: M. Model: opus.
Generated by Claude Code
claude commentedon Aug 25, 2026
os-dev-report
{ "issue": 2762, "status": "done", "branch": "claude/issue-2762-approval-center-p1-tail", "pr": null, "premise_still_valid": false, "summary": "Measured zero-implementation stop: all four dispatched items are already delivered on origin/main (591bf2791). The dispatch and the 2026-08-06 triage both stopped their stale-premise check at PR objectstack-ai/objectui#2789 (merged 2026-07-24T12:12Z) and missed PR objectstack-ai/objectui#2803 'Approval Center triage + drawer readability pass (#2762 P1-2/3/4/5, P2)' (1a03af69e, merged 2026-07-24T14:17Z — two hours later, same day) plus PR objectstack-ai/objectui#2811 (7cb199b1c, 2026-07-24T16:24Z). Between them those two PRs delivered every in-scope item: (1) approver chips keyed by (name, group) and rendered as 'Dev Admin, dot, Finance' — server side attaches pending_approver_groups for per_group nodes at packages/plugins/plugin-approvals/src/approval-service.ts:4736, citing objectui#2807, so the item is live end to end, not just client-side; (2) isSystemSubmitter() plus a 'Flow-initiated' requester cell replacing the bare avatar + em dash; (3) drawer action hierarchy, delivered contract-first as a cross-repo pair — the framework declares variant 'primary' on approval_approve and 'danger' on approval_reject (sys-approval-request.object.ts:368/401, both comments citing objectui#2762 P1-5) and DeclaredActionsBar maps primary to the filled default and danger to destructive; (4) prettifyKey() drops a trailing 'id' token so owner_id reads 'Owner', and decisionAmountEntry() promotes the amount to a lead figure excluded from the field grid. The two triage-EXCLUDED items are also already implemented on main (in-list amount + sort as objectstack-ai/objectui#2762 P1-3, and a bulk-reject toolbar), which is further evidence the tail closed. No code was written, no changeset, no PR. The branch was pushed empty as the write-route probe and carries zero commits; the worktree is clean and removed.", "tests": "Method: rather than reason from source, a four-case scope probe was written asserting the FIXED behaviour of each item and run against UNMODIFIED origin/main at 591bf2791 — the inverse of the dispatch's ghost-assertion guard. Under that guard an assertion that passes on unmodified main proves the defect is dead, so a green reading here IS the measurement. Degenerate-control guard honoured: the fixture is two DIFFERENT approver ids (u_fin, u_leg) sharing ONE display name ('Dev Admin', the reported symptom) filling TWO DIFFERENT groups (Finance, Legal), plus a 'flow:budget_flow' submitter id and a payload carrying both owner_id and amount; a single-approver or single-group fixture would pass against the pre-fix code and prove nothing. Command (repo root, via the shared verify lock): pnpm exec vitest run apps/console/src/pages/system/ApprovalsInboxPage.scopeProbe2762.test.tsx — verdict line 'os-verify-lock: VERDICT command-exit 0', vitest 'Test Files 1 passed (1) / Tests 4 passed (4)'. Items 1, 2, 4a (Owner copy) and 4b (amount lead, asserted as the text-xl lead figure rendered exactly once) all PASS on unmodified main. Item 3 is pinned by an existing test rather than the probe: packages/app-shell/src/views/__tests__/DeclaredActionsBar.test.tsx 'maps the spec action variant onto Button variants' — run with -t, verdict 'command-exit 0', 'Tests 1 passed | 29 skipped (30)'. One honest correction on the way: the probe's first item-2 assertion failed, and the failure was the PROBE's, not the page's — it searched the whole table row for an em dash and hit RequestCell's unrelated step-label fallback. Rescoped to the requester cell it passes. The probe was NOT committed: assertions that pass on main are characterization tests, not this card's fix. It was deleted from the worktree; its shape is recorded in the finding below.", "open_questions": [ { "question": "The EXCLUDED P2 light-mode item: the dispatch asked me to report if I confirmed the 'prefers-color-scheme: light is ineffective, suspected dark-only' measurement. I did NOT confirm it, and a code read (no browser run) suggests the reading is a measurement-method artifact rather than a product-promise question. Light mode is real and class-driven: packages/components/src/index.css and packages/app-shell/src/styles.css both bind Tailwind's dark variant to a .dark class on the html element INSTEAD of prefers-color-scheme, deliberately, so the in-app toggle owns the theme; ConsoleShell.tsx:153 mounts ThemeProvider with defaultTheme 'system' and storageKey 'object-ui-theme'; the command palette offers light / dark / system. With theme='system' the provider DOES read prefers-color-scheme — but only inside an effect keyed on [theme], with no matchMedia change listener, so forcing the media query in devtools after load changes nothing until a reload, and any previously stored explicit 'dark' choice pins it regardless. A related already-fixed bug (#2942) is cited in that file: spec's 'auto' spelling fell through to classList.add('auto'), a class no variant matches. So: does this still need a maintainer ruling?", "options": [ "A — Do not file the needs-user-decision card. Light mode is supported and toggle-driven; re-measure via the command palette (theme light) or an OS-level change plus reload before escalating a product promise.", "B — File the needs-user-decision card as planned, on the narrower question of whether the app should FOLLOW live OS theme changes (add a matchMedia change listener while theme is system/auto) rather than whether light mode exists at all.", "C — Treat the missing matchMedia listener as an ordinary small defect card, no maintainer ruling needed." ], "recommendation": "A, then C if it still reproduces. The 'we support light mode vs docs say dark-only' framing rests on the original measurement, and that measurement is explained by a mechanism the repo chose on purpose and documents in two CSS files — so the premise for a maintainer ruling is weak. I did not run a browser, so this is a code-read reading, not a refutation; one re-measure through the command palette settles it. If the theme still will not switch, the live-follow gap (C) is a plain defect and needs no product ruling." } ], "out_of_scope_findings": [ "filed as objectstack-ai/objectui#6395: [finding] three of the four objectstack-ai/objectui#2762 P1/P2 behaviours delivered in objectstack-ai/objectui#2803/#2811 (approver-chip group, flow-initiated requester cell, Owner copy + amount lead) have no assertion anywhere in apps/console or packages/app-shell — the scope probe above was the only thing that ever measured them; label `finding`, unassigned, no `pm:queue`, related to the objectstack-ai/objectstack#25950 SDUI rebuild.", "not filed, reported here instead — pending_approver_groups is attached only for behavior === 'per_group' AND only for NAMED groups (synthetic `#N` keys are dropped on purpose at approval-service.ts, 'a dot-#0 sub-tag is noise'). A 会签 flow whose groups were never named therefore still shows context-free duplicate chips. That is the framework's deliberate design, cross-repo, and outside this card." ] }Generated by Claude Code
Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
CLOSING — measured zero-implementation stop. All four in-scope items are already delivered. PM session
session_011SfZeFWrhGLHmfq61xbz4q,domain:uiexecution seat.⛔ The stale-premise check that found this was MINE to run, and I ran it wrong
My dispatch order (comment
5415472770) leaned on the 2026-08-06 triage comment's finding that PR #2789 merged 2026-07-24T12:12Z, and scoped this card to the four-item tail. Both that triage comment and my dispatch stopped there.They should not have. The dev found:
1a03af69e, merged 2026-07-24T14:17Z.7cb199b1c, merged 2026-07-24T16:24Z.Between them, every item I dispatched was already shipped.
⭐ The error, stated plainly: I verified that a PR had merged and never asked whether there were others. A stale-premise check that finds one delivering PR and stops is not a stale-premise check — it is a single lookup wearing the name. I even wrote in the dispatch order that "this card has already had one round of its scope evaporate under a stale-premise check, and a second is entirely possible" — and then reproduced the exact miss that caused the first one.
What is already on
origin/main(591bf2791), measuredDev Admin · Finance. ⭐ Live end to end, not client-only: the server attachespending_approver_groupsforper_groupnodes atplugin-approvals/src/approval-service.ts:4736, citing objectui#2807isSystemSubmitter()+ a "Flow-initiated" requester cell replacing the bare avatar + em dashvariant: 'primary'onapproval_approveand'danger'onapproval_reject(sys-approval-request.object.ts:368/401, both comments citing objectui#2762 P1-5), andDeclaredActionsBarmaps them onto the filled/destructive Button variantsprettifyKey()drops a trailingidtoken soowner_idreads "Owner";decisionAmountEntry()promotes the amount to a lead figure excluded from the field gridThe two items triage excluded (in-list amount + sort, bulk-reject toolbar) are also on main — further evidence the tail closed.
⭐ The method is worth keeping
The dev inverted the ghost-assertion guard: it wrote a four-case probe asserting the fixed behaviour of each item and ran it against unmodified
origin/main.All four pass. The degenerate-control discipline was honoured in the same breath — the fixture is two different approver ids sharing one display name ("Dev Admin", the reported symptom) in two different groups, because a single-approver or single-group fixture would pass against the pre-fix code and prove nothing.
And it was not committed: "assertions that pass on main are characterization tests, not this card's fix." Correct — and the gap that leaves is filed as #6395 (three of the four behaviours have no assertion anywhere).
The dev also caught its own instrument: its first item-2 assertion failed, and it identified the failure as the probe's (searching the whole row for an em dash, hitting
RequestCell's unrelated step-label fallback), not the page's.Closing as completed
Nothing in scope remains. No code, no changeset, no PR — the branch was pushed empty as a write-route probe and carries zero commits.
⭐ A measured zero-implementation stop is a good outcome. The cost here was one dev run; the cost of the alternative was re-implementing four shipped behaviours and a PR that would have looked like progress.
Reopening is free if any item is found still live.
Generated by Claude Code
os-support-ai commentedon Aug 25, 2026
PM ruling on the excluded P2 light-mode item — do NOT file the
needs-user-decisioncard.session_011SfZeFWrhGLHmfq61xbz4q,domain:uiexecution seat.My dispatch order said: "If you confirm the measurement, report it — the PM files a
needs-user-decisioncard." The dev did not confirm it, and reported why. That is the branch I am ruling on.The premise for the maintainer question does not hold up
The escalation was framed as "we support light mode" vs "docs say dark-only" — two different product promises needing one ruling. That framing rests entirely on the original reading that
prefers-color-scheme: lightis ineffective. The dev's code read explains that reading as a mechanism the repo chose on purpose:packages/components/src/index.cssandpackages/app-shell/src/styles.cssboth bind Tailwind'sdarkvariant to a.darkclass onhtmlinstead ofprefers-color-scheme— deliberately, so the in-app toggle owns the theme.ConsoleShell.tsx:153mountsThemeProviderwithdefaultTheme: 'system',storageKey: 'object-ui-theme'; the command palette offers light / dark / system.theme === 'system'the provider does readprefers-color-scheme— but only inside an effect keyed on[theme], with nomatchMediachange listener. So forcing the media query in devtools after load changes nothing until a reload, and any previously stored explicitdarkchoice pins it regardless.⇒ Light mode is supported and toggle-driven. The original measurement is consistent with a measurement-method artifact, not with a missing product capability.
⭐ A decision card built on a doubtful premise is worse than no card: it puts a question in the maintainer's inbox that the code already answers, and whatever gets ruled becomes binding on a premise nobody re-checked. ⛔ Not filing it.
The dev wrote: "I did not run a browser, so this is a code-read reading, not a refutation." I am adopting that limit rather than rounding it up. The premise is in doubt, not disproven.
Next step if anyone picks this up: re-measure through the command palette (theme → light), or change the OS theme and reload, before escalating anything. One re-measure settles it.
If it still will not switch after that, the live-follow gap — no
matchMediachange listener whilethemeissystem— is an ordinary small defect card, not a product ruling. File it as a plainfinding; ⛔ it needs no maintainer floor.Related prior art recorded in the same file: #2942, where spec's
'auto'spelling fell through toclassList.add('auto'), a class no variant matches.Also from this run, recorded so it is not lost
⛔ Not filed, cross-repo, outside this lane:
pending_approver_groupsis attached only whenbehavior === 'per_group'and only for named groups — synthetic#Nkeys are dropped on purpose (approval-service.ts: "a dot-#0 sub-tag is noise"). So a 会签 flow whose groups were never named still shows context-free duplicate chips. That is the framework's deliberate design and belongs to whoever ownsplugin-approvals, not here. Recorded rather than filed, since filing it in objectui would put it in the wrong queue.Generated by Claude Code
test(console): pin the three unpinned #2762 P1/P2 approvals behaviours
test(console): pin the three unpinned objectstack-ai#2762 P1/P2 appro…