Skip to content

Commit 33e3614

Browse files
os-zhuangclaude
andcommitted
feat(approvals): surface decisionOutputs keys on the request row (#3447 P2 UI enablement)
The decision UI renders one input per author-declared output key; the set varies per request (each node declares its own), so it rides the request read (decision_outputs) rather than the object's static action params — the params array cannot enumerate a per-request field set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d1cabaa commit 33e3614

4 files changed

Lines changed: 35 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/plugin-approvals": patch
4+
---
5+
6+
Surface the approval node's author-declared `decisionOutputs` keys on the request read as `ApprovalRequestRow.decision_outputs` (#3447 P2 UI enablement). The set varies per request (each node declares its own), so it rides the row rather than the object's static action params — a decision UI renders one input per key and POSTs `outputs` with the decision.

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,21 @@ describe('ApprovalService (node era)', () => {
441441
}, SYS)).rejects.toThrow(/VALIDATION_FAILED.*reserved/s);
442442
});
443443

444+
it('decision outputs: declared keys surface on the request row as decision_outputs (#3447 P2 UI)', async () => {
445+
// The decision UI renders one input per declared key — the keys ride the
446+
// request read (per-request; the static action params can't carry them).
447+
const req = await svc.openNodeRequest(
448+
openInput(['u9'], {}, { decisionOutputs: ['next_reviewers', 'note'] }), CTX,
449+
) as any;
450+
expect(req.decision_outputs).toEqual(['next_reviewers', 'note']);
451+
const listed = await svc.listRequests({ status: 'pending' }, SYS);
452+
expect((listed[0] as any).decision_outputs).toEqual(['next_reviewers', 'note']);
453+
// A node declaring none omits the field entirely.
454+
engine._tables['sys_approval_request'] = [];
455+
const plain = await svc.openNodeRequest(openInput(['u9']), CTX) as any;
456+
expect(plain.decision_outputs).toBeUndefined();
457+
});
458+
444459
it('decision outputs: accepted keys return from decideNode and snapshot as __decisionOutputs', async () => {
445460
const req = await svc.openNodeRequest(
446461
openInput(['u9'], {}, { decisionOutputs: ['next_reviewers', 'note'] }), CTX,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ function rowFromRequest(row: any): ApprovalRequestRow {
229229
sla_due_at: slaDueAt(row.created_at, cfg),
230230
// ADR-0044 revision round (rides the config snapshot; absent ⇒ round 1).
231231
round: typeof cfg?.__round === 'number' ? cfg.__round : undefined,
232+
// #3447 P2: the node's author-declared decision-output keys, surfaced so a
233+
// decision UI can render input fields for them and POST `outputs` on
234+
// approve/reject. Per-request (each node declares its own), which is why
235+
// this rides the row instead of the static action params.
236+
decision_outputs: Array.isArray(cfg?.decisionOutputs) && cfg.decisionOutputs.length
237+
? cfg.decisionOutputs.map(String)
238+
: undefined,
232239
} as any;
233240
}
234241

packages/spec/src/contracts/approval-service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export interface ApprovalRequestRow {
4848
/** ADR-0019 correlation: the suspended flow run this request belongs to. */
4949
flow_run_id?: string;
5050
flow_node_id?: string;
51+
/**
52+
* #3447 P2: the node's author-declared decision-output keys
53+
* (`config.decisionOutputs`), surfaced from the config snapshot so a
54+
* decision UI can render one input per key and POST `outputs` with the
55+
* decision. Absent when the node declares none.
56+
*/
57+
decision_outputs?: string[];
5158
completed_at?: string;
5259
created_at?: string;
5360
updated_at?: string;

0 commit comments

Comments
 (0)