From 89e113cccb6d67b0cf0d3dc7cd629296d29d8479 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Fri, 12 Jun 2026 11:57:18 +0500 Subject: [PATCH] feat(console): send back for revision in the approvals inbox (ADR-0044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Console counterpart of framework #1769 (issue #1744): - approver action "Send back" (violet) with explanation dialog → POST /approvals/requests/:id/revise; auto-reject toast when the server reports the maxRevisions budget exceeded - submitter revision panel on a returned request: unlocked-record hint, edit-record link, optional comment, Resubmit (POST /resubmit, opens the next round) and Recall (abandon the revision window) - `returned` status: violet badge, status filter option, drawer strip - Round-N chips on the list row and drawer (row.round from the server) - timeline entries for revise (violet) / resubmit (blue) audit actions - approvalsApi.sendBack/resubmit + row typings (returned, round) - approvalsInbox i18n: 18 new keys across all ten locales (parity green) Browser-verified against the framework showcase stack: send back → record unlocks → resubmit → Round 2 chip → approve; third send-back auto-rejects. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 --- .changeset/approvals-send-back-revision.md | 6 + .../src/pages/system/ApprovalsInboxPage.tsx | 171 +++++++++++++++++- apps/console/src/services/approvalsApi.ts | 30 ++- packages/i18n/src/locales/ar.ts | 18 ++ packages/i18n/src/locales/de.ts | 18 ++ packages/i18n/src/locales/en.ts | 18 ++ packages/i18n/src/locales/es.ts | 18 ++ packages/i18n/src/locales/fr.ts | 18 ++ packages/i18n/src/locales/ja.ts | 18 ++ packages/i18n/src/locales/ko.ts | 18 ++ packages/i18n/src/locales/pt.ts | 18 ++ packages/i18n/src/locales/ru.ts | 18 ++ packages/i18n/src/locales/zh.ts | 18 ++ 13 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 .changeset/approvals-send-back-revision.md diff --git a/.changeset/approvals-send-back-revision.md b/.changeset/approvals-send-back-revision.md new file mode 100644 index 000000000..714cacb2f --- /dev/null +++ b/.changeset/approvals-send-back-revision.md @@ -0,0 +1,6 @@ +--- +"@object-ui/console": minor +"@object-ui/i18n": minor +--- + +ADR-0044 send-back-for-revision in the approvals inbox (framework #1744/#1769). Approvers get a "Send back" action (violet, with its own dialog) that ends the round as `returned` and unlocks the record; the submitter sees a revision panel on the returned request — edit-record link, optional comment, Resubmit (opens round N+1) and Recall (abandons the revision). New `returned` status badge/filter, Round-N chips (list + drawer), timeline rendering for `revise`/`resubmit` actions, `approvalsApi.sendBack/resubmit`, and ten-locale `approvalsInbox` strings. diff --git a/apps/console/src/pages/system/ApprovalsInboxPage.tsx b/apps/console/src/pages/system/ApprovalsInboxPage.tsx index c1f3b4fde..562b0d9f9 100644 --- a/apps/console/src/pages/system/ApprovalsInboxPage.tsx +++ b/apps/console/src/pages/system/ApprovalsInboxPage.tsx @@ -94,6 +94,7 @@ import { HelpCircle, Send, Check, + CornerUpLeft, } from 'lucide-react'; import { approvalsApi, @@ -109,14 +110,15 @@ const PAGE_SIZE = 50; /** * Semantic status colors (green = approved, amber = waiting, red = rejected, - * slate = recalled) — variant-based Badge colors read as monochrome chrome, - * not as state. + * slate = recalled, violet = returned for revision) — variant-based Badge + * colors read as monochrome chrome, not as state. */ const STATUS_CLASSES: Record = { pending: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-400', approved: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-400', rejected: 'border-red-200 bg-red-50 text-red-700 dark:border-red-500/30 dark:bg-red-500/10 dark:text-red-400', recalled: 'border-border bg-muted text-muted-foreground', + returned: 'border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-500/30 dark:bg-violet-500/10 dark:text-violet-400', }; function formatDate(s: string | null | undefined): string { @@ -282,6 +284,7 @@ export function ApprovalsInboxPage() { case 'approved': return tr('statusApproved', 'Approved'); case 'rejected': return tr('statusRejected', 'Rejected'); case 'recalled': return tr('statusRecalled', 'Recalled'); + case 'returned': return tr('statusReturned', 'Returned for revision'); default: return status; } }, [tr]); @@ -355,6 +358,10 @@ export function ApprovalsInboxPage() { const [reassignTo, setReassignTo] = useState(''); const [requestInfoOpen, setRequestInfoOpen] = useState(false); const [requestInfoText, setRequestInfoText] = useState(''); + // Send back for revision (ADR-0044) — a flow movement, unlike request-info. + const [sendBackOpen, setSendBackOpen] = useState(false); + const [sendBackText, setSendBackText] = useState(''); + const [resubmitting, setResubmitting] = useState(false); const [reply, setReply] = useState(''); const [threadBusy, setThreadBusy] = useState(false); const [userOptions, setUserOptions] = useState>([]); @@ -608,6 +615,54 @@ export function ApprovalsInboxPage() { } }, [selected, requestInfoText, resolveActor, refreshThread, humanizeError, tr]); + /** + * Send back for revision (ADR-0044): finalizes this round as `returned`, + * unlocks the record, and parks the flow until the submitter resubmits. + * Past the node's revision budget the server auto-rejects instead. + */ + const doSendBack = useCallback(async () => { + if (!selected) return; + setThreadBusy(true); + try { + const res = await approvalsApi.sendBack(selected.id, { + actor_id: resolveActor(selected), comment: sendBackText.trim() || undefined, + }); + toast.success(res.autoRejected + ? tr('sendBackAutoRejected', 'Revision limit reached — the request was auto-rejected') + : tr('sendBackSuccess', 'Sent back for revision — the requester can now edit and resubmit')); + setSendBackOpen(false); + setSendBackText(''); + await refreshThread(selected.id); + refreshBadge(); + } catch (err: any) { + toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); + } finally { + setThreadBusy(false); + } + }, [selected, sendBackText, resolveActor, refreshThread, refreshBadge, humanizeError, tr]); + + /** + * Resubmit after rework (ADR-0044, submitter): the flow re-enters the + * approval node and opens the next round's request. + */ + const doResubmit = useCallback(async () => { + if (!selected) return; + setResubmitting(true); + try { + await approvalsApi.resubmit(selected.id, { + actor_id: user?.id, comment: comment.trim() || undefined, + }); + toast.success(tr('resubmitSuccess', 'Resubmitted — a new approval round has opened')); + setComment(''); + await refreshThread(selected.id); + refreshBadge(); + } catch (err: any) { + toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); + } finally { + setResubmitting(false); + } + }, [selected, comment, user?.id, refreshThread, refreshBadge, humanizeError, tr]); + const doReply = useCallback(async () => { if (!selected || !reply.trim()) return; setThreadBusy(true); @@ -646,6 +701,12 @@ export function ApprovalsInboxPage() { return selected.submitter_id === user?.id || actorOverride.trim().length > 0; }, [selected, user?.id, actorOverride]); + /** ADR-0044: the submitter may resubmit (or abandon) a returned request. */ + const canResubmit = useMemo(() => { + if (!selected || selected.status !== 'returned') return false; + return selected.submitter_id === user?.id || actorOverride.trim().length > 0; + }, [selected, user?.id, actorOverride]); + /** Unique process labels present in current rows (for filter dropdown). */ const processOptions = useMemo(() => { @@ -898,6 +959,11 @@ export function ApprovalsInboxPage() {
{processLabel(r)}
{stepLabel(r) || '—'} + {(r.round ?? 1) > 1 && ( + + {tr('roundChip', 'Round {{n}}', { n: r.round })} + + )}
); @@ -1023,6 +1089,7 @@ export function ApprovalsInboxPage() { {statusLabel('approved')} {statusLabel('rejected')} {statusLabel('recalled')} + {statusLabel('returned')} )} @@ -1364,6 +1431,30 @@ export function ApprovalsInboxPage() { + {/* Send back for revision dialog (ADR-0044) */} + { if (!open) { setSendBackOpen(false); setSendBackText(''); } }}> + + + {tr('sendBackTitle', 'Send this request back for revision?')} + + {tr('sendBackBody', 'This round ends and the record unlocks so the requester can fix the data. When they resubmit, a fresh approval round opens for all approvers.')} + + +