Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/reject-decision-one-dialog.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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?');
});
});

Expand Down
10 changes: 9 additions & 1 deletion packages/app-shell/src/views/RecordDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }),
},
Expand Down
Loading