Skip to content

Commit 7d324d3

Browse files
committed
feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807)
per_group (会签) requests now carry pending_approver_groups on the enriched row — each still-pending approver id mapped to the group key(s) it fills — so a client can label each 'waiting on' chip with its group instead of showing duplicate, context-free names. - Resolved in attachDecisionProgress from the same open-time __approverGroups snapshot decision_progress already uses, so the two never disagree - Only pending slots; synthetic (unnamed, #N) group keys dropped - Absent for non-per_group behaviors; display-only - Added to the ApprovalRequestRow contract in @objectstack/spec Closes objectui#2807 (framework half) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cu48mLFUdRBmMh8Z8R3CVz
1 parent 6f55c63 commit 7d324d3

4 files changed

Lines changed: 75 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-approvals": patch
4+
---
5+
6+
feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807)
7+
8+
`per_group` (会签) requests now carry `pending_approver_groups` on the
9+
enriched row — a map from each still-pending approver id to the group key(s)
10+
it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label
11+
each "waiting on" chip with the group it represents instead of showing
12+
duplicate, context-free names.
13+
14+
- Resolved in `attachDecisionProgress` from the same open-time
15+
`__approverGroups` snapshot the `decision_progress` groups already use, so
16+
the two never disagree.
17+
- Only the **pending** slots are mapped (a resolved approver has left
18+
`pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are
19+
dropped — a `· #0` sub-tag would be noise.
20+
- Absent for non-`per_group` behaviors. Display-only; the engine's
21+
finalization tally stays authoritative.
22+
- Added to the `ApprovalRequestRow` contract in `@objectstack/spec`.

packages/plugins/plugin-approvals/src/approval-service.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,33 @@ describe('ApprovalService — decision_progress & deep links (#2678 P1.5)', () =
14511451
expect(row.decision_progress.groups.find((g: any) => g.group === 'finance')).toMatchObject({ got: 0, satisfied: false });
14521452
});
14531453

1454+
it('per_group: pending_approver_groups maps each pending approver to its group (objectui#2807)', async () => {
1455+
const req = await svc.openNodeRequest(cfg([U('l1', 'legal'), U('f1', 'finance')], 'per_group'), CTX);
1456+
let row: any = await svc.getRequest(req.id, SYS);
1457+
// Every pending slot is labeled with the group it fills.
1458+
expect(row.pending_approver_groups).toEqual({ l1: ['legal'], f1: ['finance'] });
1459+
// Once legal signs off, l1 drops out of pending — and out of the map.
1460+
await svc.decideNode(req.id, { decision: 'approve', actorId: 'l1' }, SYS);
1461+
row = await svc.getRequest(req.id, SYS);
1462+
expect(row.pending_approver_groups).toEqual({ f1: ['finance'] });
1463+
});
1464+
1465+
it('per_group with unnamed groups omits synthetic keys; non-per_group omits the map (objectui#2807)', async () => {
1466+
// Distinct (record, run) so the two opens aren't a duplicate-pending clash.
1467+
const mk = (approvers: any[], behavior: string, recordId: string, runId: string, extra: Record<string, any> = {}) => ({
1468+
...openInput([], { recordId, runId }),
1469+
config: { approvers, behavior, lockRecord: true, ...extra },
1470+
});
1471+
// Unnamed approvers → synthetic `#N` group keys, which are not surfaced.
1472+
const unnamed = await svc.openNodeRequest(mk([U('u1'), U('u2')], 'per_group', 'opp_u', 'run_u'), CTX);
1473+
const uRow: any = await svc.getRequest(unnamed.id, SYS);
1474+
expect(uRow.pending_approver_groups).toBeUndefined();
1475+
// Quorum aggregates approvals, not groups — no approver→group map.
1476+
const q = await svc.openNodeRequest(mk([U('a1'), U('a2')], 'quorum', 'opp_q', 'run_q', { minApprovals: 2 }), CTX);
1477+
const qRow: any = await svc.getRequest(q.id, SYS);
1478+
expect(qRow.pending_approver_groups).toBeUndefined();
1479+
});
1480+
14541481
it('quorum: progress reports approvals against the clamped threshold', async () => {
14551482
const req = await svc.openNodeRequest(cfg([U('u1'), U('u2'), U('u3')], 'quorum', { minApprovals: 2 }), CTX);
14561483
await svc.decideNode(req.id, { decision: 'approve', actorId: 'u1' }, SYS);

packages/plugins/plugin-approvals/src/approval-service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,21 @@ export class ApprovalService implements IApprovalService {
22672267
});
22682268
progress.got = progress.groups.filter((g: any) => g.satisfied).length;
22692269
progress.need = progress.groups.length;
2270+
2271+
// Approver→group(s) for the STILL-PENDING slots (objectui#2807), so the
2272+
// console can label each "waiting on" chip with the group it represents
2273+
// rather than showing duplicate, context-free names. Only pending slots
2274+
// matter — a resolved approver has dropped out of `pending_approvers`.
2275+
// Synthetic (unnamed, `#N`) group keys are dropped: a `· #0` sub-tag is
2276+
// noise, and the client would have to filter it anyway.
2277+
const pendingGroups: Record<string, string[]> = {};
2278+
for (const a of (row.pending_approvers ?? [])) {
2279+
const named = (snapshot[a] ?? []).filter((g) => !/^#\d+$/.test(g));
2280+
if (named.length) pendingGroups[a] = named;
2281+
}
2282+
if (Object.keys(pendingGroups).length) {
2283+
(row as any).pending_approver_groups = pendingGroups;
2284+
}
22702285
}
22712286
(row as any).decision_progress = progress;
22722287
} catch { /* display-only enrichment */ }

packages/spec/src/contracts/approval-service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ export interface ApprovalRequestRow {
7474
* they are already human-readable.
7575
*/
7676
pending_approver_names?: Record<string, string>;
77+
/**
78+
* Group membership of each STILL-PENDING approver, for `per_group` (会签)
79+
* requests only (objectui#2807). Maps an approver id in `pending_approvers`
80+
* to the group key(s) it fills — e.g. `{ "u_devadmin": ["finance", "legal"] }`
81+
* — so a client can label each "waiting on" chip with the group it represents
82+
* instead of showing duplicate, context-free names. Resolved from the same
83+
* open-time `__approverGroups` snapshot the `decision_progress` groups use, so
84+
* the two never disagree. Absent for non-`per_group` behaviors and for slots
85+
* whose group was synthetic (unnamed). Display-only.
86+
*/
87+
pending_approver_groups?: Record<string, string[]>;
7788
/**
7889
* Display values for lookup fields in `payload` (field key → referenced
7990
* record's display name), so inbox summaries never show foreign-key ids.

0 commit comments

Comments
 (0)