Skip to content

Commit 1afade2

Browse files
committed
fix(plugin-approvals): approval request title reads the flow's label
display_title now reads process_label + ' · ' + record_id, falling back to process_name when no label is stored, so the record page heading, its breadcrumb and the action history request cell show the flow's label instead of flow:NAME. titleFormat keeps {process_name}: a template cannot fall back, and process_label can be null after the boot fill. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
1 parent b6805ac commit 1afade2

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
Clause-②: no
6+
7+
fix(plugin-approvals): an approval request's record title reads the flow's label, not `flow:NAME`
8+
9+
- **`sys_approval_request.display_title` reads `process_label`.** The title is now the flow's label and the record id (`Discount Sign-off · opp_45`), the same label the request lists show. A row with no label stored falls back to `process_name` (`flow:deal_desk · opp_42`), so the title never reads `null · …`. Every surface that draws the request's title through `nameField` shows it: the record page's heading and breadcrumb, and the request cell of the action history list.
10+
- **`titleFormat` still names `{process_name}`.** A placeholder template cannot fall back, and `process_label` can be null after the boot fill (a row it could not label, or a failed fill run). A renderer that reads `titleFormat` ahead of `nameField`, such as the global search palette, still shows the machine source.
11+
12+
A patch: one formula on one platform object's computed title. No export, type, accepted value or stored column changes.

‎packages/plugins/plugin-approvals/src/sys-approval-display-title.test.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
*
2323
* Where a source column is nullable, a row without it is titled by the other
2424
* column rather than failing to evaluate.
25+
*
26+
* `sys_approval_request` is the one object whose formula and `titleFormat`
27+
* name different columns (#24007): the formula's flow half is the flow's label
28+
* (`process_label`, nullable), with `process_name` as its fallback, while
29+
* `titleFormat` keeps `{process_name}` because a template cannot express that
30+
* fallback. So for that object point 2 is asserted where they agree — a row
31+
* with no stored label, where both draw `process_name` — and a row with a
32+
* label pins the formula's own text and the template's difference from it.
2533
*/
2634

2735
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
@@ -90,7 +98,26 @@ describe('[#20015] approval objects resolve a real record title under ADR-0079 o
9098
}
9199
});
92100

93-
it('sys_approval_request: the H1 is "{process_name} · {record_id}", not the id', async () => {
101+
it('sys_approval_request: with a flow label stored, the H1 is "{process_label} · {record_id}"', async () => {
102+
// An authored label that is not the humanized machine name ("Deal Desk"),
103+
// so the title can only have come from the stored label.
104+
const created = await engine.insert('sys_approval_request', {
105+
process_name: 'flow:deal_desk', process_label: 'Discount Sign-off',
106+
object_name: 'opportunity', record_id: 'opp_45', status: 'pending',
107+
}, SYS);
108+
expect(created.display_title).toBe('Discount Sign-off · opp_45');
109+
const row = await engine.findOne('sys_approval_request', { where: { id: created.id } }, SYS);
110+
expect(row).not.toBeNull();
111+
112+
expect(h1Of(SysApprovalRequest, row!)).toBe('Discount Sign-off · opp_45');
113+
expect(h1Of(SysApprovalRequest, row!)).not.toBe(row!.id);
114+
expect(resolveRecordTitle(SysApprovalRequest, storedOnly(row!))).toBe('Discount Sign-off · opp_45');
115+
// The pairing, restated: `titleFormat` keeps the machine source, so a
116+
// renderer that reads it first draws a different text for this row.
117+
expect(renderTitleFormat(SysApprovalRequest.titleFormat, row!)).toBe('flow:deal_desk · opp_45');
118+
});
119+
120+
it('sys_approval_request: with no flow label stored, the H1 is "{process_name} · {record_id}", never "null · …"', async () => {
94121
const created = await engine.insert('sys_approval_request', {
95122
process_name: 'flow:deal_desk', object_name: 'opportunity', record_id: 'opp_42',
96123
status: 'pending',
@@ -99,6 +126,7 @@ describe('[#20015] approval objects resolve a real record title under ADR-0079 o
99126
expect(created.display_title).toBe('flow:deal_desk · opp_42');
100127
const row = await engine.findOne('sys_approval_request', { where: { id: created.id } }, SYS);
101128
expect(row).not.toBeNull();
129+
expect(row!.process_label ?? null).toBeNull();
102130

103131
expect(h1Of(SysApprovalRequest, row!)).toBe('flow:deal_desk · opp_42');
104132
expect(h1Of(SysApprovalRequest, row!)).not.toBe(row!.id);

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,19 @@ export const SysApprovalRequest = ObjectSchema.create({
4545
isSystem: true,
4646
managedBy: 'engine-owned',
4747
description: 'Live approval instance tracked per submission',
48-
// [ADR-0079] The record title is `display_title`, a text formula over the
49-
// same two columns `titleFormat` names. The pointer used to be `id`: once a
50-
// renderer honours ADR-0079's order (an explicit `nameField` wins over
51-
// `titleFormat`), that made the record page's H1 the raw id. `titleFormat`
52-
// stays for renderers that still read it first;
53-
// `sys-approval-display-title.test.ts` holds the two to the same text.
48+
// [ADR-0079] The record title is `display_title`, a text formula. The
49+
// pointer used to be `id`: once a renderer honours ADR-0079's order (an
50+
// explicit `nameField` wins over `titleFormat`), that made the record page's
51+
// H1 the raw id. The formula reads the flow's label (`process_label`) and
52+
// falls back to the machine source (`process_name`) where no label is
53+
// stored. `titleFormat` stays for renderers that still read it first — the
54+
// global search palette (`searchAll` in `@objectstack/metadata-protocol`)
55+
// ranks it above `nameField` — and keeps `{process_name}`: a placeholder
56+
// template cannot say "else", and `process_label` can be null after the boot
57+
// fill (a row the fill could not label, or a failed fill run), where a
58+
// `{process_label}` template draws a title with no source at all.
59+
// `sys-approval-display-title.test.ts` holds the two to the same text where
60+
// no label is stored, and pins where they differ.
5461
displayNameField: 'display_title',
5562
nameField: 'display_title', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
5663
titleFormat: '{process_name} · {record_id}',
@@ -128,12 +135,15 @@ export const SysApprovalRequest = ObjectSchema.create({
128135
id: Field.text({ label: 'Request ID', required: true, readonly: true, group: 'System' }),
129136

130137
// [ADR-0079] The record title (`nameField` above). A formula is computed on
131-
// read and has no stored column. Both source columns are required, so the
132-
// expression needs no null guard.
138+
// read and has no stored column. Its flow half is the flow's label,
139+
// `process_label`, the same column the request lists show. That column is
140+
// nullable — a row the boot fill could not label, or one written before
141+
// the fill ran — so the expression guards it and falls back to
142+
// `process_name`, which is required, as is `record_id`.
133143
display_title: Field.formula({
134144
label: 'Title',
135145
returnType: 'text',
136-
expression: F`record.process_name + ' · ' + record.record_id`,
146+
expression: F`record.process_label != null ? record.process_label + ' · ' + record.record_id : record.process_name + ' · ' + record.record_id`,
137147
description: 'Record title: the request source and the record it is about (computed on read)',
138148
group: 'Target',
139149
}),

0 commit comments

Comments
 (0)