From c7f6aa8464e4b237fcfa6b016d2b038291ed47f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Sun, 26 Jul 2026 00:12:38 -0700 Subject: [PATCH] feat(approvals): enrich inbox rows with payload_labels (snapshot field labels) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/approvals-payload-labels.md | 15 +++++++ .../src/approval-service.test.ts | 21 ++++++++++ .../plugin-approvals/src/approval-service.ts | 40 ++++++++++++++++++- .../spec/src/contracts/approval-service.ts | 9 +++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 .changeset/approvals-payload-labels.md diff --git a/.changeset/approvals-payload-labels.md b/.changeset/approvals-payload-labels.md new file mode 100644 index 0000000000..f0b1cc3b14 --- /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 9ae1085cd9..77986e1d58 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 40c63af663..76468eb951 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 296bc5ace8..ecd90a129f 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