Skip to content

Commit 73046d2

Browse files
committed
fix(plugin-approvals): every request action's success copy names the request, in all four locales
Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4d7a1cd commit 73046d2

8 files changed

Lines changed: 206 additions & 124 deletions

‎packages/plugins/plugin-approvals/src/approval-service.test.ts‎

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,16 +1288,15 @@ describe('ApprovalService (node era)', () => {
12881288
expect(rows[0].record_title).toBe('Snapshot Title');
12891289
});
12901290

1291-
// [#23157] The approve action is `type: 'api'` against the decide route,
1292-
// which answers decide()'s result verbatim; its success copy interpolates
1293-
// `${result.*}` over that answer (the scope `ActionSchema.successMessage`
1294-
// declares). So the toast names what was approved only while decide()'s
1295-
// answer carries the title the copy reads, finalized or not.
1296-
describe('the approve action\'s success copy, filled from decide()\'s answer', () => {
1297-
const approveCopy = (): string => {
1298-
const action = (SysApprovalRequest as any).actions.find((a: any) => a.name === 'approval_approve');
1299-
return action.successMessage;
1300-
};
1291+
// [#23157] Every request action is `type: 'api'` against a route that
1292+
// answers its service method's result verbatim, and each of those results
1293+
// carries `request`, the read-back row with its display enrichment. Each
1294+
// action's success copy interpolates `${result.*}` over that answer (the
1295+
// scope `ActionSchema.successMessage` declares), so the toast names the
1296+
// request only while the answer carries the title the copy reads.
1297+
describe('each request action\'s success copy, filled from its route\'s answer', () => {
1298+
const copyOf = (name: string): string =>
1299+
((SysApprovalRequest as any).actions as any[]).find((a) => a.name === name).successMessage;
13011300
/** The copy's `${result.<path>}` tokens, each filled from the answer; an unresolved path reads as ''. */
13021301
const fill = (copy: string, result: unknown): { text: string; unresolved: string[] } => {
13031302
const unresolved: string[] = [];
@@ -1308,43 +1307,110 @@ describe('ApprovalService (node era)', () => {
13081307
});
13091308
return { text, unresolved };
13101309
};
1310+
const named = (action: string, answer: unknown): string => {
1311+
const { text, unresolved } = fill(copyOf(action), answer);
1312+
expect(unresolved, `${action}: every token resolves on the answer`).toEqual([]);
1313+
return text;
1314+
};
13111315
/** The id of the request an ordinary open answers (never an auto-outcome in these cells). */
13121316
const openRequestId = async (...args: Parameters<ApprovalService['openNodeRequest']>): Promise<string> =>
13131317
((await svc.openNodeRequest(...args)) as { id: string }).id;
1318+
/** A flow whose approval node declares the `revise` out-edge send-back needs (#3823). */
1319+
const REVISE_FLOW = {
1320+
name: 'deal_approval',
1321+
nodes: [
1322+
{ id: 'approve_step', type: 'approval' },
1323+
{ id: 'wait_revision', type: APPROVAL_REVISE_NODE_TYPE },
1324+
],
1325+
edges: [{ id: 'e_rev', source: 'approve_step', target: 'wait_revision', label: 'revise' }],
1326+
};
13141327

1315-
it('reads the answer: the copy carries at least one `${result.*}` token', () => {
1316-
expect(approveCopy()).toMatch(/\$\{result\.[\w.]+\}/);
1328+
beforeEach(() => {
1329+
engine._tables['opportunity'] = [{ id: 'opp1', name: 'Acme Renewal', amount: 100 }];
1330+
svc.attachAutomation({
1331+
async resume() { /* resumed */ },
1332+
async cancelRun() { /* handed back */ },
1333+
async getFlow() { return REVISE_FLOW; },
1334+
} as any);
13171335
});
13181336

1319-
it('a finalizing approve: the toast names the record, the way the inbox row\'s inline Approve does', async () => {
1320-
engine._tables['opportunity'] = [{ id: 'opp1', name: 'Acme Renewal', amount: 100 }];
1337+
it('reads the answer: every action\'s copy carries a `${result.*}` token', () => {
1338+
for (const a of (SysApprovalRequest as any).actions as any[]) {
1339+
expect(a.successMessage, `${a.name}.successMessage`).toMatch(/\$\{result\.[\w.]+\}/);
1340+
}
1341+
});
1342+
1343+
it('approve, finalizing: the toast names the record, the way the inbox row\'s inline Approve does', async () => {
13211344
const id = await openRequestId(openInput(['u9']), CTX);
13221345
const out = await svc.decide(id, { decision: 'approve', actorId: 'u9' }, SYS);
13231346
expect(out.finalized).toBe(true);
1324-
const { text, unresolved } = fill(approveCopy(), out);
1325-
expect(unresolved).toEqual([]);
1326-
expect(text).toBe('Approved "Acme Renewal"');
1347+
expect(named('approval_approve', out)).toBe('Approved "Acme Renewal"');
13271348
});
13281349

1329-
it('an approve that leaves the step waiting on others still names the record', async () => {
1330-
engine._tables['opportunity'] = [{ id: 'opp1', name: 'Acme Renewal', amount: 100 }];
1350+
it('approve, leaving the step waiting on others: still names the record', async () => {
13311351
const id = await openRequestId(openInput(['u1', 'u2'], {}, { behavior: 'unanimous' }), CTX);
13321352
const out = await svc.decide(id, { decision: 'approve', actorId: 'u1' }, SYS);
13331353
expect(out.finalized).toBe(false);
1334-
const { text, unresolved } = fill(approveCopy(), out);
1335-
expect(unresolved).toEqual([]);
1336-
expect(text).toContain('Acme Renewal');
1337-
expect(text).not.toBe('Approved.');
1354+
expect(named('approval_approve', out)).toBe('Approved "Acme Renewal"');
13381355
});
13391356

1340-
it('a record whose row is gone is named from the payload snapshot, as the inbox names it', async () => {
1341-
const id = await openRequestId(
1342-
openInput(['u9'], { record: { id: 'opp1', name: 'Snapshot Title', amount: 1 } }), CTX,
1343-
);
1357+
it('approve, on a record whose row is gone: named from the payload snapshot, as the inbox names it', async () => {
1358+
engine._tables['opportunity'] = [];
1359+
const id = await openRequestId(openInput(['u9'], { record: { id: 'opp1', name: 'Snapshot Title', amount: 1 } }), CTX);
13441360
const out = await svc.decide(id, { decision: 'approve', actorId: 'u9' }, SYS);
1345-
const { text, unresolved } = fill(approveCopy(), out);
1346-
expect(unresolved).toEqual([]);
1347-
expect(text).toBe('Approved "Snapshot Title"');
1361+
expect(named('approval_approve', out)).toBe('Approved "Snapshot Title"');
1362+
});
1363+
1364+
it('reject: names the record, the way the inbox row\'s inline Reject does', async () => {
1365+
const id = await openRequestId(openInput(['u9']), CTX);
1366+
const out = await svc.decide(id, { decision: 'reject', actorId: 'u9' }, SYS);
1367+
expect(out.request.status).toBe('rejected');
1368+
expect(named('approval_reject', out)).toBe('Rejected "Acme Renewal"');
1369+
});
1370+
1371+
it('reassign: names the record', async () => {
1372+
const id = await openRequestId(openInput(['u9']), CTX);
1373+
const out = await svc.reassign(id, { actorId: 'u9', to: 'u7' }, asUser('u9'));
1374+
expect(named('approval_reassign', out)).toBe('Reassigned "Acme Renewal"');
1375+
});
1376+
1377+
it('send back for revision: names the record', async () => {
1378+
const id = await openRequestId(openInput(['u9']), CTX);
1379+
const out = await svc.sendBack(id, { actorId: 'u9', comment: 'split the lines' }, asUser('u9'));
1380+
expect(out.request.status).toBe('returned');
1381+
expect(named('approval_send_back', out)).toBe('Sent "Acme Renewal" back for revision');
1382+
});
1383+
1384+
it('request information: names the record', async () => {
1385+
const id = await openRequestId(openInput(['u9']), CTX);
1386+
const out = await svc.requestInfo(id, { actorId: 'u9', comment: 'which cost centre?' }, asUser('u9'));
1387+
expect(named('approval_request_info', out)).toBe('Information requested on "Acme Renewal"');
1388+
});
1389+
1390+
it('remind: names the record', async () => {
1391+
const id = await openRequestId(openInput(['u9']), CTX);
1392+
const out = await svc.remind(id, { actorId: 'u1' }, CTX);
1393+
expect(named('approval_remind', out)).toBe('Reminder sent for "Acme Renewal"');
1394+
});
1395+
1396+
it('recall: names the record', async () => {
1397+
const id = await openRequestId(openInput(['u9']), CTX);
1398+
const out = await svc.recall(id, { actorId: 'u1' }, CTX);
1399+
expect(out.request.status).toBe('recalled');
1400+
expect(named('approval_recall', out)).toBe('Recalled "Acme Renewal"');
1401+
});
1402+
1403+
it('resubmit: names the record', async () => {
1404+
const id = await openRequestId(openInput(['u9']), CTX);
1405+
await svc.sendBack(id, { actorId: 'u9', comment: 'split the lines' }, asUser('u9'));
1406+
const out = await svc.resubmit(id, { actorId: 'u1', comment: 'split' }, CTX);
1407+
expect(named('approval_resubmit', out)).toBe('Resubmitted "Acme Renewal"');
1408+
});
1409+
1410+
it('reply: names the record', async () => {
1411+
const id = await openRequestId(openInput(['u9']), CTX);
1412+
const out = await svc.comment(id, { actorId: 'u1', comment: 'attached the receipt' }, CTX);
1413+
expect(named('approval_comment', out)).toBe('Reply posted on "Acme Renewal"');
13481414
});
13491415
});
13501416

‎packages/plugins/plugin-approvals/src/sys-approval-request.object.ts‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ export const SysApprovalRequest = ObjectSchema.create({
477477
// same title the inbox row shows (the target record's display field,
478478
// else its payload snapshot). `${result.*}` is the success copy's
479479
// declared scope on a `type: 'api'` action: the route's own response.
480+
// Every action below names the request the same way: each of their
481+
// routes answers `{ request, … }` with that same read-back row.
480482
successMessage: 'Approved "${result.request.record_title}"',
481483
refreshAfter: true,
482484
},
@@ -507,7 +509,7 @@ export const SysApprovalRequest = ObjectSchema.create({
507509
'has(record.viewer) && has(record.viewer.can_act) && record.viewer.can_act == true' +
508510
' || has(record.viewer) && has(record.viewer.can_override) && record.viewer.can_override == true',
509511
locations: ['record_section', 'list_item'],
510-
successMessage: 'Rejected.',
512+
successMessage: 'Rejected "${result.request.record_title}"',
511513
refreshAfter: true,
512514
},
513515
{
@@ -530,7 +532,7 @@ export const SysApprovalRequest = ObjectSchema.create({
530532
'has(record.viewer) && has(record.viewer.can_act) && record.viewer.can_act == true' +
531533
' || has(record.viewer) && has(record.viewer.can_override) && record.viewer.can_override == true',
532534
locations: ['record_section'],
533-
successMessage: 'Reassigned.',
535+
successMessage: 'Reassigned "${result.request.record_title}"',
534536
refreshAfter: true,
535537
},
536538

@@ -550,7 +552,7 @@ export const SysApprovalRequest = ObjectSchema.create({
550552
],
551553
visible: 'has(record.viewer) && has(record.viewer.can_act) && record.viewer.can_act == true',
552554
locations: ['record_section'],
553-
successMessage: 'Sent back for revision.',
555+
successMessage: 'Sent "${result.request.record_title}" back for revision',
554556
refreshAfter: true,
555557
},
556558
{
@@ -565,7 +567,7 @@ export const SysApprovalRequest = ObjectSchema.create({
565567
],
566568
visible: 'has(record.viewer) && has(record.viewer.can_act) && record.viewer.can_act == true',
567569
locations: ['record_section'],
568-
successMessage: 'Information requested.',
570+
successMessage: 'Information requested on "${result.request.record_title}"',
569571
refreshAfter: true,
570572
},
571573

@@ -592,7 +594,7 @@ export const SysApprovalRequest = ObjectSchema.create({
592594
],
593595
visible: 'has(record.status) && record.status == "pending" && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true',
594596
locations: ['record_section'],
595-
successMessage: 'Reminder sent.',
597+
successMessage: 'Reminder sent for "${result.request.record_title}"',
596598
refreshAfter: true,
597599
},
598600
{
@@ -634,7 +636,7 @@ export const SysApprovalRequest = ObjectSchema.create({
634636
' && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true' +
635637
' || has(record.viewer) && has(record.viewer.can_override) && record.viewer.can_override == true',
636638
locations: ['record_section'],
637-
successMessage: 'Recalled.',
639+
successMessage: 'Recalled "${result.request.record_title}"',
638640
refreshAfter: true,
639641
},
640642
{
@@ -649,7 +651,7 @@ export const SysApprovalRequest = ObjectSchema.create({
649651
],
650652
visible: 'has(record.status) && record.status == "returned" && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true',
651653
locations: ['record_section'],
652-
successMessage: 'Resubmitted.',
654+
successMessage: 'Resubmitted "${result.request.record_title}"',
653655
refreshAfter: true,
654656
},
655657

@@ -697,7 +699,7 @@ export const SysApprovalRequest = ObjectSchema.create({
697699
' || has(record.status) && record.status == "pending"' +
698700
' && has(record.viewer) && has(record.viewer.is_submitter) && record.viewer.is_submitter == true',
699701
locations: ['record_section'],
700-
successMessage: 'Reply posted.',
702+
successMessage: 'Reply posted on "${result.request.record_title}"',
701703
refreshAfter: true,
702704
},
703705
],

‎packages/plugins/plugin-approvals/src/translations/approve-success-copy-i18n.test.ts‎

Lines changed: 0 additions & 54 deletions
This file was deleted.

‎packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
138138
approval_reject: {
139139
label: "Reject",
140140
description: "Reject this request? A rejection is final for every approver.",
141-
successMessage: "Rejected.",
141+
successMessage: "Rejected \"${result.request.record_title}\"",
142142
params: {
143143
comment: {
144144
label: "Comment"
@@ -150,7 +150,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
150150
},
151151
approval_reassign: {
152152
label: "Reassign",
153-
successMessage: "Reassigned.",
153+
successMessage: "Reassigned \"${result.request.record_title}\"",
154154
params: {
155155
to: {
156156
label: "New approver",
@@ -163,7 +163,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
163163
},
164164
approval_send_back: {
165165
label: "Send back",
166-
successMessage: "Sent back for revision.",
166+
successMessage: "Sent \"${result.request.record_title}\" back for revision",
167167
params: {
168168
comment: {
169169
label: "Reason"
@@ -172,7 +172,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
172172
},
173173
approval_request_info: {
174174
label: "Request info",
175-
successMessage: "Information requested.",
175+
successMessage: "Information requested on \"${result.request.record_title}\"",
176176
params: {
177177
comment: {
178178
label: "What do you need?"
@@ -181,7 +181,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
181181
},
182182
approval_remind: {
183183
label: "Send reminder",
184-
successMessage: "Reminder sent.",
184+
successMessage: "Reminder sent for \"${result.request.record_title}\"",
185185
params: {
186186
comment: {
187187
label: "Note"
@@ -191,7 +191,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
191191
approval_recall: {
192192
label: "Recall",
193193
description: "Recall this request? Approvers can no longer act on it and the record is unlocked.",
194-
successMessage: "Recalled.",
194+
successMessage: "Recalled \"${result.request.record_title}\"",
195195
params: {
196196
comment: {
197197
label: "Comment"
@@ -200,7 +200,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
200200
},
201201
approval_resubmit: {
202202
label: "Resubmit",
203-
successMessage: "Resubmitted.",
203+
successMessage: "Resubmitted \"${result.request.record_title}\"",
204204
params: {
205205
comment: {
206206
label: "What changed?"
@@ -209,7 +209,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
209209
},
210210
approval_comment: {
211211
label: "Reply",
212-
successMessage: "Reply posted.",
212+
successMessage: "Reply posted on \"${result.request.record_title}\"",
213213
params: {
214214
comment: {
215215
label: "Comment"

0 commit comments

Comments
 (0)