diff --git a/.changeset/approvals-payload-labels.md b/.changeset/approvals-payload-labels.md new file mode 100644 index 000000000..f0b1cc3b1 --- /dev/null +++ b/.changeset/approvals-payload-labels.md @@ -0,0 +1,15 @@ +--- +"@objectstack/plugin-approvals": patch +"@objectstack/spec": patch +--- + +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. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot +field key → the target object's field label), symmetric with the existing +`payload_display` (which resolves the values), and `ApprovalRequestRow` gains +the field. For a single-locale project the schema label is already the +localized string, so a client can render the human field name (e.g. "考核状态") +instead of a prettified English key. diff --git a/packages/plugins/plugin-approvals/src/approval-service.test.ts b/packages/plugins/plugin-approvals/src/approval-service.test.ts index 9ae1085cd..77986e1d5 100644 --- a/packages/plugins/plugin-approvals/src/approval-service.test.ts +++ b/packages/plugins/plugin-approvals/src/approval-service.test.ts @@ -598,6 +598,27 @@ describe('ApprovalService (node era)', () => { expect(rows[0].payload_display).toEqual({ account: 'Acme Corp' }); }); + it('enrichment maps snapshot field keys to the object field labels', async () => { + (engine as any).getSchema = (name: string) => + name === 'opportunity' + ? { + label: 'Opportunity', + fields: { + id: {}, // no label → excluded from payload_labels + name: { label: 'Deal Name' }, + amount: { label: 'Deal Amount' }, + }, + } + : undefined; + await svc.openNodeRequest( + openInput(['u9'], { record: { id: 'opp1', name: 'Acme Renewal', amount: 100 } }), CTX, + ); + const rows = await svc.listRequests({ status: 'pending' }, SYS); + // Only keys present in the snapshot AND carrying a schema label are mapped; + // `id` (unlabeled) is dropped. + expect(rows[0].payload_labels).toEqual({ name: 'Deal Name', amount: 'Deal Amount' }); + }); + it('enrichment maps user-id approvers to display names', async () => { engine._tables['sys_user'] = [{ id: 'u9', name: 'Grace Hopper', email: 'grace@example.com' }]; await svc.openNodeRequest(openInput(['u9']), CTX); diff --git a/packages/plugins/plugin-approvals/src/approval-service.ts b/packages/plugins/plugin-approvals/src/approval-service.ts index 40c63af66..76468eb95 100644 --- a/packages/plugins/plugin-approvals/src/approval-service.ts +++ b/packages/plugins/plugin-approvals/src/approval-service.ts @@ -1968,11 +1968,31 @@ export class ApprovalService implements IApprovalService { } catch { return []; } } + /** + * Field key → display label for an object's schema. Lets the inbox summary + * show a human field name ("考核状态") instead of a title-cased machine key + * ("Assessment Status"). For a single-locale project the schema label already + * IS the localized string; symmetric with `resolveDisplayField`/lookup + * resolution that power `payload_display`. + */ + private resolveFieldLabels(object: string): Record { + try { + const schema: any = (this.engine as any).getSchema?.(object); + const fields = schema?.fields ?? {}; + const out: Record = {}; + for (const [key, f] of Object.entries(fields)) { + if (f?.label) out[key] = String(f.label); + } + return out; + } catch { return {}; } + } + /** * Attach inbox display fields to rows so clients never render a raw * identifier: `record_title`, `submitter_name`, `object_label`, - * `pending_approver_names` (user-id approvers), and `payload_display` - * (lookup foreign keys in the snapshot → referenced record titles). + * `pending_approver_names` (user-id approvers), `payload_display` + * (lookup foreign keys in the snapshot → referenced record titles), and + * `payload_labels` (snapshot field keys → the target object's field labels). * Batched: one query per distinct object (target + referenced) plus one * `sys_user` lookup. Best-effort — a deleted record falls back to the * payload snapshot, and any failure leaves the field unset rather than @@ -2011,9 +2031,13 @@ export class ApprovalService implements IApprovalService { // Lookup foreign keys inside payload snapshots → referenced record titles. const lookupFieldsByObject = new Map>(); + // Field key → label per object, for the snapshot summary's field names. + const fieldLabelsByObject = new Map>(); for (const object of byObject.keys()) { const lookups = this.resolveLookupFields(object); if (lookups.length) lookupFieldsByObject.set(object, lookups); + const labels = this.resolveFieldLabels(object); + if (Object.keys(labels).length) fieldLabelsByObject.set(object, labels); } const refIds = new Map>(); for (const r of rows) { @@ -2081,6 +2105,18 @@ export class ApprovalService implements IApprovalService { } if (Object.keys(display).length) r.payload_display = display; } + + // Field labels for the snapshot keys the summary renders (only keys + // actually present in the payload — a deleted field's label is noise). + const fieldLabels = fieldLabelsByObject.get(r.object_name); + if (fieldLabels && r.payload && typeof r.payload === 'object') { + const labels: Record = {}; + for (const key of Object.keys(r.payload as Record)) { + const l = fieldLabels[key]; + if (l) labels[key] = l; + } + if (Object.keys(labels).length) r.payload_labels = labels; + } } } diff --git a/packages/spec/src/contracts/approval-service.ts b/packages/spec/src/contracts/approval-service.ts index 296bc5ace..ecd90a129 100644 --- a/packages/spec/src/contracts/approval-service.ts +++ b/packages/spec/src/contracts/approval-service.ts @@ -90,6 +90,15 @@ export interface ApprovalRequestRow { * record's display name), so inbox summaries never show foreign-key ids. */ payload_display?: Record; + /** + * Display labels for `payload` fields (field key → target object's field + * label), so inbox summaries show the human field name (e.g. "考核状态") + * instead of a title-cased machine key ("Assessment Status"). Resolved from + * the target object's schema — for a single-locale project the schema label + * IS the localized string; symmetric with `payload_display` (which resolves + * the values). Absent keys fall back to the client's prettified key. + */ + payload_labels?: Record; /** * SLA deadline, when the node config carries `escalation.timeoutHours`: * `created_at + timeoutHours`. Display-only for now — automatic escalation