diff --git a/.changeset/reject-decision-one-dialog.md b/.changeset/reject-decision-one-dialog.md new file mode 100644 index 0000000000..9b62f3cb9b --- /dev/null +++ b/.changeset/reject-decision-one-dialog.md @@ -0,0 +1,22 @@ +--- +"@object-ui/app-shell": patch +--- + +fix(approvals): record-header Reject fires after ONE dialog again (objectui#3126) + +Since #2961 made the record header's decision inputs live (`actionParams`), +the Reject action carried BOTH `confirmText` and a collectable comment param. +The ActionRunner chains confirmation before param collection, so rejecting +queued two dialogs: the approver answered "Reject this approval request? → +Continue", the alertdialog closed — and no request fired, because it was +waiting on a second, unexpected "Action parameters / Comment (optional)" +dialog. Anyone on the rc.0 contract (one confirm → request) read that as a +silent no-op: zero network traffic, no toast, the flow stuck pending. Approve +never declared `confirmText`, which is why it kept working on the same node. + +The Reject action no longer declares `confirmText`. The param dialog is the +confirmation surface: it is titled by the action ("Reject"), carries the old +confirm question as its description (same `approvals.rejectConfirm` i18n key, +so every locale keeps its translation), collects the optional comment and any +declared decision outputs, and nothing is sent until its own Confirm — one +decision, one dialog, matching Approve and the Approval Center. diff --git a/packages/app-shell/src/views/RecordDetailView.approvalDecisionActions.test.tsx b/packages/app-shell/src/views/RecordDetailView.approvalDecisionActions.test.tsx index 164299c4a1..d57210d87b 100644 --- a/packages/app-shell/src/views/RecordDetailView.approvalDecisionActions.test.tsx +++ b/packages/app-shell/src/views/RecordDetailView.approvalDecisionActions.test.tsx @@ -56,7 +56,22 @@ describe('buildApprovalDecisionActions — param key (objectui#2955)', () => { const actions = byName(buildApprovalDecisionActions(pending(), t) as any[]); expect(actions.approve_request.order).toBe(-100); expect(actions.reject_request.order).toBe(-99); - expect(actions.reject_request.confirmText).toBeDefined(); + }); +}); + +describe('buildApprovalDecisionActions — one dialog per decision (objectui#3126)', () => { + it('never carries `confirmText` — the param dialog is the confirmation', () => { + // The runner chains confirm THEN param collection, so `confirmText` + + // `actionParams` queued TWO dialogs: after "Continue" nothing was sent + // until a second, unexpected comment dialog was also confirmed — the + // #3126 "reject silently does nothing" report. One decision, one dialog. + const actions = buildApprovalDecisionActions(pending(), t) as any[]; + for (const a of actions) expect(a.confirmText).toBeUndefined(); + }); + + it('keeps the reject confirm question as the dialog description', () => { + const actions = byName(buildApprovalDecisionActions(pending(), t) as any[]); + expect(actions.reject_request.description).toBe('Reject this approval request?'); }); }); diff --git a/packages/app-shell/src/views/RecordDetailView.tsx b/packages/app-shell/src/views/RecordDetailView.tsx index 308b396c7c..c7d77a519b 100644 --- a/packages/app-shell/src/views/RecordDetailView.tsx +++ b/packages/app-shell/src/views/RecordDetailView.tsx @@ -228,7 +228,15 @@ export function buildApprovalDecisionActions( order: -99, locations: ['record_header'], refreshAfter: true, - confirmText: t('approvals.rejectConfirm', { defaultValue: 'Reject this approval request?' }), + // NO `confirmText` here (objectui#3126). The runner chains confirm THEN + // param collection, so carrying both queued two dialogs: the approver + // answered "Reject this approval request? → Continue" and the request + // still didn't fire — it waited on a second, unexpected comment dialog + // (opened since #2961 made `actionParams` live on this surface), which + // reads as a silent no-op. The param dialog IS the confirmation: it is + // titled by this label, carries the confirm question as its description, + // and nothing is sent until its own Confirm. + description: t('approvals.rejectConfirm', { defaultValue: 'Reject this approval request?' }), actionParams: decisionParams('reject'), successMessage: t('approvals.rejectSuccess', { defaultValue: 'Rejected' }), },