Skip to content

Commit debc23a

Browse files
baozhoutaoclaude
andauthored
feat(approvals): enrich inbox rows with payload_labels (snapshot field labels) (#3501)
The approvals inbox summary title-cased raw snapshot machine keys ("assessment_status" → "Assessment Status") because the API sent no field labels. Add `payload_labels` (snapshot field key → the target object's field label) to the inbox enrichment, symmetric with the existing `payload_display` (which resolves the values). For a single-locale project the schema label is already the localized string, so the client can render "考核状态" instead of a prettified English key; the client falls back to the prettified key when a label is absent. - ApprovalRequestRow contract: add `payload_labels?`. - ApprovalService.enrichRows: resolve per-object field labels and attach the ones whose keys are present in the snapshot. - Add a service test. Action/param i18n (the `_actions` translation bundles) is intentionally NOT included here — framework main already ships complete action translations for sys_approval_request across en/zh-CN/ja-JP/es-ES. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 879ea13 commit debc23a

4 files changed

Lines changed: 83 additions & 2 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
"@objectstack/spec": patch
4+
---
5+
6+
feat(approvals): enrich inbox rows with `payload_labels` (snapshot field labels)
7+
8+
The approvals inbox summary title-cased raw snapshot machine keys
9+
(`assessment_status` → "Assessment Status") because the API sent no field
10+
labels. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot
11+
field key → the target object's field label), symmetric with the existing
12+
`payload_display` (which resolves the values), and `ApprovalRequestRow` gains
13+
the field. For a single-locale project the schema label is already the
14+
localized string, so a client can render the human field name (e.g. "考核状态")
15+
instead of a prettified English key.

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,27 @@ describe('ApprovalService (node era)', () => {
858858
expect(rows[0].payload_display).toEqual({ account: 'Acme Corp' });
859859
});
860860

861+
it('enrichment maps snapshot field keys to the object field labels', async () => {
862+
(engine as any).getSchema = (name: string) =>
863+
name === 'opportunity'
864+
? {
865+
label: 'Opportunity',
866+
fields: {
867+
id: {}, // no label → excluded from payload_labels
868+
name: { label: 'Deal Name' },
869+
amount: { label: 'Deal Amount' },
870+
},
871+
}
872+
: undefined;
873+
await svc.openNodeRequest(
874+
openInput(['u9'], { record: { id: 'opp1', name: 'Acme Renewal', amount: 100 } }), CTX,
875+
);
876+
const rows = await svc.listRequests({ status: 'pending' }, SYS);
877+
// Only keys present in the snapshot AND carrying a schema label are mapped;
878+
// `id` (unlabeled) is dropped.
879+
expect(rows[0].payload_labels).toEqual({ name: 'Deal Name', amount: 'Deal Amount' });
880+
});
881+
861882
it('enrichment maps user-id approvers to display names', async () => {
862883
engine._tables['sys_user'] = [{ id: 'u9', name: 'Grace Hopper', email: 'grace@example.com' }];
863884
await svc.openNodeRequest(openInput(['u9']), CTX);

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,11 +2292,31 @@ export class ApprovalService implements IApprovalService {
22922292
} catch { return []; }
22932293
}
22942294

2295+
/**
2296+
* Field key → display label for an object's schema. Lets the inbox summary
2297+
* show a human field name ("考核状态") instead of a title-cased machine key
2298+
* ("Assessment Status"). For a single-locale project the schema label already
2299+
* IS the localized string; symmetric with `resolveDisplayField`/lookup
2300+
* resolution that power `payload_display`.
2301+
*/
2302+
private resolveFieldLabels(object: string): Record<string, string> {
2303+
try {
2304+
const schema: any = (this.engine as any).getSchema?.(object);
2305+
const fields = schema?.fields ?? {};
2306+
const out: Record<string, string> = {};
2307+
for (const [key, f] of Object.entries<any>(fields)) {
2308+
if (f?.label) out[key] = String(f.label);
2309+
}
2310+
return out;
2311+
} catch { return {}; }
2312+
}
2313+
22952314
/**
22962315
* Attach inbox display fields to rows so clients never render a raw
22972316
* identifier: `record_title`, `submitter_name`, `object_label`,
2298-
* `pending_approver_names` (user-id approvers), and `payload_display`
2299-
* (lookup foreign keys in the snapshot → referenced record titles).
2317+
* `pending_approver_names` (user-id approvers), `payload_display`
2318+
* (lookup foreign keys in the snapshot → referenced record titles), and
2319+
* `payload_labels` (snapshot field keys → the target object's field labels).
23002320
* Batched: one query per distinct object (target + referenced) plus one
23012321
* `sys_user` lookup. Best-effort — a deleted record falls back to the
23022322
* payload snapshot, and any failure leaves the field unset rather than
@@ -2335,9 +2355,13 @@ export class ApprovalService implements IApprovalService {
23352355

23362356
// Lookup foreign keys inside payload snapshots → referenced record titles.
23372357
const lookupFieldsByObject = new Map<string, Array<{ key: string; reference: string }>>();
2358+
// Field key → label per object, for the snapshot summary's field names.
2359+
const fieldLabelsByObject = new Map<string, Record<string, string>>();
23382360
for (const object of byObject.keys()) {
23392361
const lookups = this.resolveLookupFields(object);
23402362
if (lookups.length) lookupFieldsByObject.set(object, lookups);
2363+
const labels = this.resolveFieldLabels(object);
2364+
if (Object.keys(labels).length) fieldLabelsByObject.set(object, labels);
23412365
}
23422366
const refIds = new Map<string, Set<string>>();
23432367
for (const r of rows) {
@@ -2405,6 +2429,18 @@ export class ApprovalService implements IApprovalService {
24052429
}
24062430
if (Object.keys(display).length) r.payload_display = display;
24072431
}
2432+
2433+
// Field labels for the snapshot keys the summary renders (only keys
2434+
// actually present in the payload — a deleted field's label is noise).
2435+
const fieldLabels = fieldLabelsByObject.get(r.object_name);
2436+
if (fieldLabels && r.payload && typeof r.payload === 'object') {
2437+
const labels: Record<string, string> = {};
2438+
for (const key of Object.keys(r.payload as Record<string, unknown>)) {
2439+
const l = fieldLabels[key];
2440+
if (l) labels[key] = l;
2441+
}
2442+
if (Object.keys(labels).length) r.payload_labels = labels;
2443+
}
24082444
}
24092445
}
24102446

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ export interface ApprovalRequestRow {
112112
* record's display name), so inbox summaries never show foreign-key ids.
113113
*/
114114
payload_display?: Record<string, string>;
115+
/**
116+
* Display labels for `payload` fields (field key → target object's field
117+
* label), so inbox summaries show the human field name (e.g. "考核状态")
118+
* instead of a title-cased machine key ("Assessment Status"). Resolved from
119+
* the target object's schema — for a single-locale project the schema label
120+
* IS the localized string; symmetric with `payload_display` (which resolves
121+
* the values). Absent keys fall back to the client's prettified key.
122+
*/
123+
payload_labels?: Record<string, string>;
115124
/**
116125
* SLA deadline, when the node config carries `escalation.timeoutHours`:
117126
* `created_at + timeoutHours`. Display-only for now — automatic escalation

0 commit comments

Comments
 (0)