From 30f08ef0e6f5854f36999fe313b72a536db71090 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 16:16:22 +0000 Subject: [PATCH] feat(approvals): label pending-approver chips with their group (objectui#2807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drawer's 'waiting on' chips now key their collapse by (name, group) using the framework's pending_approver_groups: the same person filling two groups stays two labeled chips (Dev Admin · finance / · legal), one group filled twice collapses to a single chip with a count. Group renders as a muted · sub-tag. Degrades to plain dedupe + count when no group data present. Rebased onto latest main (resolves overlap with the #2810 admin-override change in the same file). Closes objectui#2807 (UI half) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Cu48mLFUdRBmMh8Z8R3CVz --- .changeset/approval-approver-group-chips.md | 20 ++++++++++ .../src/pages/system/ApprovalsInboxPage.tsx | 40 +++++++++++++------ apps/console/src/services/approvalsApi.ts | 8 ++++ 3 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 .changeset/approval-approver-group-chips.md diff --git a/.changeset/approval-approver-group-chips.md b/.changeset/approval-approver-group-chips.md new file mode 100644 index 000000000..c03c3c2d1 --- /dev/null +++ b/.changeset/approval-approver-group-chips.md @@ -0,0 +1,20 @@ +--- +"@object-ui/console": patch +--- + +feat(approvals): label pending-approver chips with their group (objectui#2807) + +Follow-up to #2762 P1-2. The dedupe pass collapsed repeated "waiting on" +chips to one with a `×N` count, but couldn't say *which* group (finance / +legal / …) each pending approver represented in a 会签 (per_group) request — +the data wasn't there. With the framework now emitting +`pending_approver_groups` (`@objectstack/plugin-approvals`), the drawer: + +- keys the chip collapse by **(name, group)** — the same person filling two + different groups stays two labeled chips (`Dev Admin · finance`, + `Dev Admin · legal`), while one group filled twice collapses to a single + chip with a count; +- renders the group as a muted `· ` sub-tag on the chip. + +Degrades cleanly: with no group data (non-`per_group`, or an older backend) +the key is the name alone, keeping the plain dedupe + `×N` behavior. diff --git a/apps/console/src/pages/system/ApprovalsInboxPage.tsx b/apps/console/src/pages/system/ApprovalsInboxPage.tsx index bc370c91d..1d27d65f7 100644 --- a/apps/console/src/pages/system/ApprovalsInboxPage.tsx +++ b/apps/console/src/pages/system/ApprovalsInboxPage.tsx @@ -161,27 +161,39 @@ function submitterDisplay(r: ApprovalRequestRow): string { function approverDisplay(a: string, r: ApprovalRequestRow): string { return r.pending_approver_names?.[a] || formatIdentity(a); } +/** The group(s) a pending approver represents (会签), joined for display. */ +function approverGroup(a: string, r: ApprovalRequestRow): string | undefined { + const gs = r.pending_approver_groups?.[a]; + return gs && gs.length ? gs.join(' / ') : undefined; +} /** - * Dedupe the pending-approver chips by display label (#2762 P1-2): a person - * who fills more than one approver slot showed up as N identical chips. Collapse - * them to one chip carrying a count, preserving first-seen order; the tooltip - * keeps every underlying id so the raw slots stay inspectable. + * Collapse the pending-approver chips (#2762 P1-2), now keyed by (name, group) + * so 会签 comprehension is preserved (objectui#2807): the same person filling + * two *different* groups stays two labeled chips (finance / legal), while a + * person filling one group twice collapses to a single chip with a count. When + * no group data is present (non-`per_group`, or an older backend) the key is the + * name alone, degrading to the plain dedupe + count. First-seen order is kept; + * the tooltip keeps every underlying id so the raw slots stay inspectable. */ -function approverChips(r: ApprovalRequestRow): Array<{ label: string; count: number; title: string }> { +function approverChips( + r: ApprovalRequestRow, +): Array<{ label: string; group?: string; count: number; title: string }> { const order: string[] = []; - const byLabel = new Map(); + const byKey = new Map(); for (const a of r.pending_approvers || []) { const label = approverDisplay(a, r); - const seen = byLabel.get(label); + const group = approverGroup(a, r); + const key = group ? `${label} ${group}` : label; + const seen = byKey.get(key); if (seen) { seen.count += 1; if (a && !seen.title.split(', ').includes(a)) seen.title += `, ${a}`; } else { - byLabel.set(label, { label, count: 1, title: a || label }); - order.push(label); + byKey.set(key, { label, group, count: 1, title: a || label }); + order.push(key); } } - return order.map((l) => byLabel.get(l)!); + return order.map((k) => byKey.get(k)!); } /** * A request with no human submitter — flow- or system-initiated (#2762 P1-4). @@ -1707,9 +1719,13 @@ export function ApprovalsInboxPage() { {tr('waitingOn', 'Waiting on')}
- {approverChips(selected).map((chip) => ( - + {approverChips(selected).map((chip, i) => ( + {chip.label} + {chip.group && ( + // 会签 group this slot represents (objectui#2807). + · {chip.group} + )} {chip.count > 1 && ( ×{chip.count} )} diff --git a/apps/console/src/services/approvalsApi.ts b/apps/console/src/services/approvalsApi.ts index 86251d55d..2227e6c60 100644 --- a/apps/console/src/services/approvalsApi.ts +++ b/apps/console/src/services/approvalsApi.ts @@ -54,6 +54,14 @@ export interface ApprovalRequestRow { object_label?: string; /** Display names for user-id entries in `pending_approvers` (id → name). */ pending_approver_names?: Record; + /** + * Group membership of each still-pending approver, `per_group` (会签) requests + * only (objectui#2807): approver id → the group key(s) it fills, e.g. + * `{ u_devadmin: ['finance', 'legal'] }`. Lets the drawer label each "waiting + * on" chip with its group. Absent for non-`per_group` behaviors and for slots + * whose group was unnamed. + */ + pending_approver_groups?: Record; /** Display values for lookup fields in `payload` (field key → record title). */ payload_display?: Record; /** SLA deadline (`created_at + escalation.timeoutHours`), display-only. */