Skip to content

Commit 84e7be9

Browse files
os-zhuangclaude
andauthored
feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807) (#3458)
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) Claude-Session: https://claude.ai/code/session_01Cu48mLFUdRBmMh8Z8R3CVz Co-authored-by: Claude <noreply@anthropic.com>
1 parent be1c52c commit 84e7be9

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
@@ -1562,6 +1562,33 @@ describe('ApprovalService — decision_progress & deep links (#2678 P1.5)', () =
15621562
expect(row.decision_progress.groups.find((g: any) => g.group === 'finance')).toMatchObject({ got: 0, satisfied: false });
15631563
});
15641564

1565+
it('per_group: pending_approver_groups maps each pending approver to its group (objectui#2807)', async () => {
1566+
const req = await svc.openNodeRequest(cfg([U('l1', 'legal'), U('f1', 'finance')], 'per_group'), CTX);
1567+
let row: any = await svc.getRequest(req.id, SYS);
1568+
// Every pending slot is labeled with the group it fills.
1569+
expect(row.pending_approver_groups).toEqual({ l1: ['legal'], f1: ['finance'] });
1570+
// Once legal signs off, l1 drops out of pending — and out of the map.
1571+
await svc.decideNode(req.id, { decision: 'approve', actorId: 'l1' }, SYS);
1572+
row = await svc.getRequest(req.id, SYS);
1573+
expect(row.pending_approver_groups).toEqual({ f1: ['finance'] });
1574+
});
1575+
1576+
it('per_group with unnamed groups omits synthetic keys; non-per_group omits the map (objectui#2807)', async () => {
1577+
// Distinct (record, run) so the two opens aren't a duplicate-pending clash.
1578+
const mk = (approvers: any[], behavior: string, recordId: string, runId: string, extra: Record<string, any> = {}) => ({
1579+
...openInput([], { recordId, runId }),
1580+
config: { approvers, behavior, lockRecord: true, ...extra },
1581+
});
1582+
// Unnamed approvers → synthetic `#N` group keys, which are not surfaced.
1583+
const unnamed = await svc.openNodeRequest(mk([U('u1'), U('u2')], 'per_group', 'opp_u', 'run_u'), CTX);
1584+
const uRow: any = await svc.getRequest(unnamed.id, SYS);
1585+
expect(uRow.pending_approver_groups).toBeUndefined();
1586+
// Quorum aggregates approvals, not groups — no approver→group map.
1587+
const q = await svc.openNodeRequest(mk([U('a1'), U('a2')], 'quorum', 'opp_q', 'run_q', { minApprovals: 2 }), CTX);
1588+
const qRow: any = await svc.getRequest(q.id, SYS);
1589+
expect(qRow.pending_approver_groups).toBeUndefined();
1590+
});
1591+
15651592
it('quorum: progress reports approvals against the clamped threshold', async () => {
15661593
const req = await svc.openNodeRequest(cfg([U('u1'), U('u2'), U('u3')], 'quorum', { minApprovals: 2 }), CTX);
15671594
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
@@ -2352,6 +2352,21 @@ export class ApprovalService implements IApprovalService {
23522352
});
23532353
progress.got = progress.groups.filter((g: any) => g.satisfied).length;
23542354
progress.need = progress.groups.length;
2355+
2356+
// Approver→group(s) for the STILL-PENDING slots (objectui#2807), so the
2357+
// console can label each "waiting on" chip with the group it represents
2358+
// rather than showing duplicate, context-free names. Only pending slots
2359+
// matter — a resolved approver has dropped out of `pending_approvers`.
2360+
// Synthetic (unnamed, `#N`) group keys are dropped: a `· #0` sub-tag is
2361+
// noise, and the client would have to filter it anyway.
2362+
const pendingGroups: Record<string, string[]> = {};
2363+
for (const a of (row.pending_approvers ?? [])) {
2364+
const named = (snapshot[a] ?? []).filter((g) => !/^#\d+$/.test(g));
2365+
if (named.length) pendingGroups[a] = named;
2366+
}
2367+
if (Object.keys(pendingGroups).length) {
2368+
(row as any).pending_approver_groups = pendingGroups;
2369+
}
23552370
}
23562371
(row as any).decision_progress = progress;
23572372
} 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)