Skip to content

Commit c7f6aa8

Browse files
baozhoutaoclaude
andcommitted
feat(approvals): enrich inbox rows with payload_labels (snapshot field labels)
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 d75edb9 commit c7f6aa8

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
@@ -598,6 +598,27 @@ describe('ApprovalService (node era)', () => {
598598
expect(rows[0].payload_display).toEqual({ account: 'Acme Corp' });
599599
});
600600

601+
it('enrichment maps snapshot field keys to the object field labels', async () => {
602+
(engine as any).getSchema = (name: string) =>
603+
name === 'opportunity'
604+
? {
605+
label: 'Opportunity',
606+
fields: {
607+
id: {}, // no label → excluded from payload_labels
608+
name: { label: 'Deal Name' },
609+
amount: { label: 'Deal Amount' },
610+
},
611+
}
612+
: undefined;
613+
await svc.openNodeRequest(
614+
openInput(['u9'], { record: { id: 'opp1', name: 'Acme Renewal', amount: 100 } }), CTX,
615+
);
616+
const rows = await svc.listRequests({ status: 'pending' }, SYS);
617+
// Only keys present in the snapshot AND carrying a schema label are mapped;
618+
// `id` (unlabeled) is dropped.
619+
expect(rows[0].payload_labels).toEqual({ name: 'Deal Name', amount: 'Deal Amount' });
620+
});
621+
601622
it('enrichment maps user-id approvers to display names', async () => {
602623
engine._tables['sys_user'] = [{ id: 'u9', name: 'Grace Hopper', email: 'grace@example.com' }];
603624
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
@@ -1968,11 +1968,31 @@ export class ApprovalService implements IApprovalService {
19681968
} catch { return []; }
19691969
}
19701970

1971+
/**
1972+
* Field key → display label for an object's schema. Lets the inbox summary
1973+
* show a human field name ("考核状态") instead of a title-cased machine key
1974+
* ("Assessment Status"). For a single-locale project the schema label already
1975+
* IS the localized string; symmetric with `resolveDisplayField`/lookup
1976+
* resolution that power `payload_display`.
1977+
*/
1978+
private resolveFieldLabels(object: string): Record<string, string> {
1979+
try {
1980+
const schema: any = (this.engine as any).getSchema?.(object);
1981+
const fields = schema?.fields ?? {};
1982+
const out: Record<string, string> = {};
1983+
for (const [key, f] of Object.entries<any>(fields)) {
1984+
if (f?.label) out[key] = String(f.label);
1985+
}
1986+
return out;
1987+
} catch { return {}; }
1988+
}
1989+
19711990
/**
19721991
* Attach inbox display fields to rows so clients never render a raw
19731992
* identifier: `record_title`, `submitter_name`, `object_label`,
1974-
* `pending_approver_names` (user-id approvers), and `payload_display`
1975-
* (lookup foreign keys in the snapshot → referenced record titles).
1993+
* `pending_approver_names` (user-id approvers), `payload_display`
1994+
* (lookup foreign keys in the snapshot → referenced record titles), and
1995+
* `payload_labels` (snapshot field keys → the target object's field labels).
19761996
* Batched: one query per distinct object (target + referenced) plus one
19771997
* `sys_user` lookup. Best-effort — a deleted record falls back to the
19781998
* payload snapshot, and any failure leaves the field unset rather than
@@ -2011,9 +2031,13 @@ export class ApprovalService implements IApprovalService {
20112031

20122032
// Lookup foreign keys inside payload snapshots → referenced record titles.
20132033
const lookupFieldsByObject = new Map<string, Array<{ key: string; reference: string }>>();
2034+
// Field key → label per object, for the snapshot summary's field names.
2035+
const fieldLabelsByObject = new Map<string, Record<string, string>>();
20142036
for (const object of byObject.keys()) {
20152037
const lookups = this.resolveLookupFields(object);
20162038
if (lookups.length) lookupFieldsByObject.set(object, lookups);
2039+
const labels = this.resolveFieldLabels(object);
2040+
if (Object.keys(labels).length) fieldLabelsByObject.set(object, labels);
20172041
}
20182042
const refIds = new Map<string, Set<string>>();
20192043
for (const r of rows) {
@@ -2081,6 +2105,18 @@ export class ApprovalService implements IApprovalService {
20812105
}
20822106
if (Object.keys(display).length) r.payload_display = display;
20832107
}
2108+
2109+
// Field labels for the snapshot keys the summary renders (only keys
2110+
// actually present in the payload — a deleted field's label is noise).
2111+
const fieldLabels = fieldLabelsByObject.get(r.object_name);
2112+
if (fieldLabels && r.payload && typeof r.payload === 'object') {
2113+
const labels: Record<string, string> = {};
2114+
for (const key of Object.keys(r.payload as Record<string, unknown>)) {
2115+
const l = fieldLabels[key];
2116+
if (l) labels[key] = l;
2117+
}
2118+
if (Object.keys(labels).length) r.payload_labels = labels;
2119+
}
20842120
}
20852121
}
20862122

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ export interface ApprovalRequestRow {
9090
* record's display name), so inbox summaries never show foreign-key ids.
9191
*/
9292
payload_display?: Record<string, string>;
93+
/**
94+
* Display labels for `payload` fields (field key → target object's field
95+
* label), so inbox summaries show the human field name (e.g. "考核状态")
96+
* instead of a title-cased machine key ("Assessment Status"). Resolved from
97+
* the target object's schema — for a single-locale project the schema label
98+
* IS the localized string; symmetric with `payload_display` (which resolves
99+
* the values). Absent keys fall back to the client's prettified key.
100+
*/
101+
payload_labels?: Record<string, string>;
93102
/**
94103
* SLA deadline, when the node config carries `escalation.timeoutHours`:
95104
* `created_at + timeoutHours`. Display-only for now — automatic escalation

0 commit comments

Comments
 (0)