Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/approvals-payload-labels.md
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions packages/plugins/plugin-approvals/src/approval-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
40 changes: 38 additions & 2 deletions packages/plugins/plugin-approvals/src/approval-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
try {
const schema: any = (this.engine as any).getSchema?.(object);
const fields = schema?.fields ?? {};
const out: Record<string, string> = {};
for (const [key, f] of Object.entries<any>(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
Expand Down Expand Up @@ -2011,9 +2031,13 @@ export class ApprovalService implements IApprovalService {

// Lookup foreign keys inside payload snapshots → referenced record titles.
const lookupFieldsByObject = new Map<string, Array<{ key: string; reference: string }>>();
// Field key → label per object, for the snapshot summary's field names.
const fieldLabelsByObject = new Map<string, Record<string, string>>();
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<string, Set<string>>();
for (const r of rows) {
Expand Down Expand Up @@ -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<string, string> = {};
for (const key of Object.keys(r.payload as Record<string, unknown>)) {
const l = fieldLabels[key];
if (l) labels[key] = l;
}
if (Object.keys(labels).length) r.payload_labels = labels;
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/spec/src/contracts/approval-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ export interface ApprovalRequestRow {
* record's display name), so inbox summaries never show foreign-key ids.
*/
payload_display?: Record<string, string>;
/**
* 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<string, string>;
/**
* SLA deadline, when the node config carries `escalation.timeoutHours`:
* `created_at + timeoutHours`. Display-only for now — automatic escalation
Expand Down