Skip to content

Commit 0de6f72

Browse files
committed
fix(showcase): head the invoice print page's line-items columns with labels
The invoice print page's `record:line_items` columns were identity-only `{ name }` entries. That block draws a column exactly as declared and does not fill one from the child field, so the printed table was headed `description` / `quantity` / `unit_price` / `amount`. Each column now declares the invoice line field's own label. A column `label` is a plain string and no translation bundle key reaches a line-items column, so the headings read in English in every locale. The new pin holds each column to its child field's label, never the name. Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP Co-authored-by: Claude <noreply@anthropic.com>
1 parent c1c0a90 commit 0de6f72

2 files changed

Lines changed: 75 additions & 1 deletion

File tree

‎examples/app-showcase/src/ui/pages/invoice-document.page.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,19 @@ export const InvoiceDocumentPage = definePage({
5757
properties: {
5858
childObject: 'showcase_invoice_line',
5959
relationshipField: 'invoice',
60-
columns: [{ name: 'description' }, { name: 'quantity' }, { name: 'unit_price' }, { name: 'amount' }],
60+
// Each column declares its heading. Unlike the master-detail grid
61+
// (`showcase_invoice.lines.inlineColumns`), this block does not
62+
// fill a column from the child field, so an identity-only
63+
// `{ name }` column prints its raw field name. The labels are the
64+
// invoice line fields' own. They read in English in every locale:
65+
// a column `label` is a plain string, and no translation bundle
66+
// key reaches a `record:line_items` column.
67+
columns: [
68+
{ name: 'description', label: 'Description' },
69+
{ name: 'quantity', label: 'Qty' },
70+
{ name: 'unit_price', label: 'Unit Price' },
71+
{ name: 'amount', label: 'Amount' },
72+
],
6173
readonly: true,
6274
},
6375
},
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { RecordLineItemsProps } from '@objectstack/spec/ui';
5+
6+
import { InvoiceDocumentPage } from '../src/ui/pages/index.js';
7+
import { InvoiceLine } from '../src/data/objects/invoice.object.js';
8+
9+
/**
10+
* The invoice print page heads its line-items table with labels.
11+
*
12+
* `record:line_items` draws a column exactly as declared. Unlike the
13+
* master-detail grid, it does not fill a column from the child object's
14+
* field, so a column with no `label` is headed by its raw field name: the
15+
* printed invoice read `description` / `quantity` / `unit_price` / `amount`.
16+
*
17+
* The pin holds each column to the label its own child field declares, and
18+
* never to the field name. Deriving the expected heading from the field keeps
19+
* the print in step with the master-detail grid, which hydrates the same
20+
* labels for the same fields.
21+
*/
22+
23+
type Column = Record<string, unknown>;
24+
type Component = { type?: unknown; properties?: Record<string, unknown> };
25+
26+
const page = InvoiceDocumentPage as unknown as { regions?: { components?: Component[] }[] };
27+
28+
const lineItemBlocks = (page.regions ?? [])
29+
.flatMap((region) => region.components ?? [])
30+
.filter((component) => component.type === 'record:line_items');
31+
32+
const columnsOf = (block: Component | undefined): Column[] =>
33+
(Array.isArray(block?.properties?.columns) ? block!.properties!.columns : []) as Column[];
34+
35+
const childFields = (InvoiceLine.fields ?? {}) as Record<string, { label?: unknown }>;
36+
37+
describe('Invoice print page — the line-items table is headed by labels, not field names', () => {
38+
const [block, ...rest] = lineItemBlocks;
39+
40+
it('carries one line-items block over the invoice lines', () => {
41+
expect(block, 'the invoice print page must carry its line-items table').toBeTruthy();
42+
expect(rest).toHaveLength(0);
43+
expect(block.properties?.childObject).toBe(InvoiceLine.name);
44+
expect(columnsOf(block).map((c) => c.name)).toEqual(['description', 'quantity', 'unit_price', 'amount']);
45+
});
46+
47+
it('heads every column with its child field\'s own label, never the field name', () => {
48+
for (const column of columnsOf(block)) {
49+
const name = column.name as string;
50+
const fieldLabel = childFields[name]?.label;
51+
expect(typeof fieldLabel, `showcase_invoice_line.${name} must declare a label`).toBe('string');
52+
expect(column.label, `column '${name}' must declare its heading`).toBe(fieldLabel);
53+
expect(column.label, `column '${name}' must not be headed by its field name`).not.toBe(name);
54+
}
55+
});
56+
57+
it('parses against the spec\'s `record:line_items` row with the labels intact', () => {
58+
const result = RecordLineItemsProps.safeParse(block.properties);
59+
expect(result.success, JSON.stringify(result.error?.issues)).toBe(true);
60+
expect(result.data).toEqual(block.properties);
61+
});
62+
});

0 commit comments

Comments
 (0)