Skip to content

Commit 6b114a1

Browse files
os-zhuangclaude
andauthored
fix(approvals-inbox): align participant gating with the server-computed viewer block (#2719)
Consume framework#3310's per-viewer capability. `ApprovalRequestRow` gains an optional `viewer: { can_act, is_submitter }`, and the inbox's participant checks (the reply box + the "why disabled" hint) prefer it over the client-side identity heuristic when present — so the hint never contradicts the declared decision buttons (whose `visible` CEL now gates on `record.viewer.*`), and position/team-addressed approvers the heuristic couldn't resolve are recognized. The heuristic stays as a fallback for a backend that predates `viewer`. Verified in-browser: a submitter viewing their own pending request no longer sees approve/reject/reassign/send-back/request-info (all hidden), only remind and recall; `getRequest` returns `viewer: {can_act:false, is_submitter:true}`. Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6f804b2 commit 6b114a1

3 files changed

Lines changed: 36 additions & 2 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/console": patch
3+
---
4+
5+
fix(approvals-inbox): align participant gating with the server-computed `viewer` block
6+
7+
Consume framework#3310's per-viewer capability: `ApprovalRequestRow` gains an
8+
optional `viewer: { can_act, is_submitter }`, and the approvals inbox's
9+
participant checks (the reply box + the "why disabled" hint) prefer it over the
10+
client-side identity heuristic when present. This keeps the hint from ever
11+
contradicting the declared decision buttons — whose `visible` CEL now gates on
12+
`record.viewer.*` — and correctly recognizes position/team-addressed approvers
13+
that the client heuristic couldn't resolve. The heuristic remains as a fallback
14+
for a backend that predates `viewer`.

apps/console/src/pages/system/ApprovalsInboxPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,22 +602,29 @@ export function ApprovalsInboxPage() {
602602

603603
// Participant checks — drive the reply box + the "why disabled" hint (the
604604
// decision buttons themselves are server-declared and gate via their own
605-
// `visible` CEL). No actor-override branch: the admin "act as" escape hatch
606-
// retired with the composer (cloud#861 enterprise act-as is the successor).
605+
// `visible` CEL). Prefer the server-computed `viewer` block (framework#3310)
606+
// so the hint never contradicts the buttons — it already reflects position/
607+
// team approver resolution, which the client identity heuristic below can't.
608+
// The heuristic stays as a fallback for a backend that predates `viewer`.
609+
// No actor-override branch: the admin "act as" escape hatch retired with the
610+
// composer (cloud#861 enterprise act-as is the successor).
607611
const canApproveReject = useMemo(() => {
608612
if (!selected || selected.status !== 'pending') return false;
613+
if (selected.viewer) return selected.viewer.can_act;
609614
const pending = new Set(selected.pending_approvers || []);
610615
return identities.some(id => pending.has(id));
611616
}, [selected, identities]);
612617

613618
const canRecall = useMemo(() => {
614619
if (!selected || selected.status !== 'pending') return false;
620+
if (selected.viewer) return selected.viewer.is_submitter;
615621
return selected.submitter_id === user?.id;
616622
}, [selected, user?.id]);
617623

618624
/** ADR-0044: the submitter may resubmit (or abandon) a returned request. */
619625
const canResubmit = useMemo(() => {
620626
if (!selected || selected.status !== 'returned') return false;
627+
if (selected.viewer) return selected.viewer.is_submitter;
621628
return selected.submitter_id === user?.id;
622629
}, [selected, user?.id]);
623630

apps/console/src/services/approvalsApi.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ export interface ApprovalRequestRow {
7171
need: number;
7272
groups?: Array<{ group: string; got: number; need: number; satisfied: boolean }>;
7373
};
74+
/**
75+
* Server-computed capability for the current viewer (framework#3310), attached
76+
* by getRequest/listRequests. Declared decision actions gate their `visible`
77+
* CEL on it — approver actions on `viewer.can_act`, submitter levers on
78+
* `viewer.is_submitter` — so the drawer never offers a decision the caller
79+
* can't make. Absent on rows not read through the approvals service.
80+
*/
81+
viewer?: {
82+
/** The caller is a current pending approver (mirrors the service's authz). */
83+
can_act: boolean;
84+
/** The caller submitted this request. */
85+
is_submitter: boolean;
86+
};
7487
/** ADR-0044 revision round on this (run, node): absent/1 = first round. */
7588
round?: number;
7689
}

0 commit comments

Comments
 (0)