diff --git a/examples/app-showcase/src/data/objects/invoice.object.ts b/examples/app-showcase/src/data/objects/invoice.object.ts index 2b531ec904..480be2eac8 100644 --- a/examples/app-showcase/src/data/objects/invoice.object.ts +++ b/examples/app-showcase/src/data/objects/invoice.object.ts @@ -185,6 +185,40 @@ export const InvoiceLine = ObjectSchema.create({ // Thin, high-volume line items → the editable grid form factor. inlineEdit: 'grid', inlineTitle: 'Line Items', + /** + * The inline grid's columns, declared EXPLICITLY rather than auto-derived + * — and the declaration is load-bearing, not decoration. + * + * Auto-derivation curates: past `DEFAULT_MAX_INLINE_COLUMNS` (6) it keeps + * the primary + every required column, fills the rest by type usefulness, + * and marks the overflow `defaultHidden` (the column chooser reveals it). + * This line had exactly 6 editable fields, so nothing was curated. Adding + * `service_start` below makes 7, and the one that loses the tie-break is + * `receipt` — a `file` column, which the fill-priority table does not + * rank, so it sorts last. That column is objectui#2360's upload-in-grid + * fixture: demoting it out of the default view is precisely the cost that + * kept `time` out of objectui#3569, and it is not a cost worth paying. + * + * Declaring the set opts out of curation entirely (`deriveDetail` routes + * an author-supplied set through `hydrateColumns`, which never sets + * `defaultHidden`), so all seven stay default-visible and `receipt`'s + * visibility stops depending on a tie-break it happens to be losing. + * + * Bare `{ field }` entries on purpose: `hydrateColumns` fills label, type, + * options, lookup target, `readonlyWhen`/`requiredWhen` and the computed + * `expression` from the schema, so labels stay translatable and the + * columns cannot drift from the field definitions above. `position` is + * absent because it is the grid's drag-reorder sort field, never a cell. + */ + inlineColumns: [ + { field: 'product' }, + { field: 'description' }, + { field: 'service_start' }, + { field: 'quantity' }, + { field: 'unit_price' }, + { field: 'receipt' }, + { field: 'amount' }, + ], }), // Catalog lookup. Picking a product auto-fills `description` + `unit_price` // (the grid copies same-named fields from the selected product record). @@ -211,6 +245,29 @@ export const InvoiceLine = ObjectSchema.create({ maxLength: 200, requiredWhen: P`record.quantity >= 100`, }), + /** + * Clock time the billed work started — the inline grid's `time` fixture + * (objectui#3569). ⛔ Do not "tidy" this field away: it is the ONLY place in + * showcase where a `time` field sits inside an inline-edit grid, and the + * grid's time control has no other real-machine coverage. `field_zoo.f_time` + * seeds a `time` value but is not a master-detail child, so it never reaches + * this rendering path. + * + * objectui#3569 split `date` / `datetime` / `time` into three grid controls; + * a renderer that folds `time` back onto the `date` control feeds `HH:mm` + * into an ``, which shows nothing and writes the clock + * out of the record on the next save. Only a running grid can catch that. + * + * Honest on a T&M line, and deliberately scoped: a services line bills hours + * (`quantity`) at a rate (`unit_price`), so a start clock plus that duration + * describes the whole billed window — no second `service_end` field that + * would only restate it. Goods lines leave it empty, which is also the + * fixture's empty-cell case. + * + * Authored as a literal `{ type: 'time' }` because there is no `Field.time` + * builder — the same form `showcase_field_zoo.f_time` uses. + */ + service_start: { type: 'time', label: 'Service Start' }, quantity: Field.number({ label: 'Qty', required: true, diff --git a/examples/app-showcase/src/data/seed/index.ts b/examples/app-showcase/src/data/seed/index.ts index 176169fb3f..d038914cbe 100644 --- a/examples/app-showcase/src/data/seed/index.ts +++ b/examples/app-showcase/src/data/seed/index.ts @@ -383,15 +383,25 @@ const invoices = defineSeed(Invoice, { // Line items — `product` resolves by SKU (the Product seed's externalId), and // `invoice` by invoice number. A contributor reaches a line only through its // master invoice, so these inherit the invoice's owner scoping. +// +// `service_start` is the inline grid's `time` FIXTURE (objectui#3569) - the one +// place in showcase where a `time` field is rendered by an inline-edit grid. +// DO NOT blank these clocks in a seed tidy-up: a zeroed or missing value leaves +// the fixture unable to show the defect it exists to catch (a `time` column +// folded onto the `date` control renders empty and writes the clock out of the +// record on the next save), so the seeded values are deliberately NON-ZERO and +// recognisable. Only the T&M service lines carry one - hours billed from a +// start clock; the goods lines are empty on purpose, which is the fixture's +// empty-cell case. const invoiceLines = defineSeed(InvoiceLine, { mode: 'upsert', externalId: 'description', records: [ - { description: 'INV-1001 \u00b7 Consulting hours', invoice: 'INV-1001', product: 'SERVICE-HR', position: 0, quantity: 10, unit_price: 150, amount: 1500 }, + { description: 'INV-1001 \u00b7 Consulting hours', invoice: 'INV-1001', product: 'SERVICE-HR', position: 0, quantity: 10, unit_price: 150, amount: 1500, service_start: '09:15' }, { description: 'INV-1001 \u00b7 Widget A units', invoice: 'INV-1001', product: 'WIDGET-A', position: 1, quantity: 4, unit_price: 29.99, amount: 119.96 }, { description: 'INV-1002 \u00b7 Gadget X units', invoice: 'INV-1002', product: 'GADGET-X', position: 0, quantity: 2, unit_price: 99, amount: 198 }, { description: 'INV-1003 \u00b7 Widget B units', invoice: 'INV-1003', product: 'WIDGET-B', position: 0, quantity: 6, unit_price: 49.99, amount: 299.94 }, - { description: 'INV-1004 \u00b7 Consulting hours', invoice: 'INV-1004', product: 'SERVICE-HR', position: 0, quantity: 3, unit_price: 150, amount: 450 }, + { description: 'INV-1004 \u00b7 Consulting hours', invoice: 'INV-1004', product: 'SERVICE-HR', position: 0, quantity: 3, unit_price: 150, amount: 450, service_start: '13:40' }, ], }); diff --git a/examples/app-showcase/src/system/translations/index.ts b/examples/app-showcase/src/system/translations/index.ts index bc0d377b8e..4f7db872b1 100644 --- a/examples/app-showcase/src/system/translations/index.ts +++ b/examples/app-showcase/src/system/translations/index.ts @@ -140,6 +140,19 @@ export const ShowcaseTranslationBundle = { incurred_at: { label: 'Incurred At' }, }, }, + // Same rule, same reason as `showcase_expense_line` above: `service_start` + // is a NEW declared label (objectui#3569's inline-grid TIME fixture), so + // it must be translated at birth or check-i18n-coverage sees the example's + // untranslated count grow and fails. And DELIBERATELY only this one field + // — `product`, `description`, `quantity`, `unit_price`, `receipt` and + // `amount` predate the ratchet and are part of the frozen baseline; + // translating them here would push the count BELOW the baseline, which the + // same gate rejects as an un-ratcheted improvement. + showcase_invoice_line: { + fields: { + service_start: { label: 'Service Start' }, + }, + }, showcase_preference: { label: 'Setting', pluralLabel: 'Settings', @@ -444,6 +457,13 @@ export const ShowcaseTranslationBundle = { incurred_at: { label: '发生时间' }, }, }, + // See the `en` side for why this entry translates exactly ONE field and + // no more (check-i18n-coverage is a two-sided ratchet). + showcase_invoice_line: { + fields: { + service_start: { label: '服务开始时间' }, + }, + }, showcase_preference: { label: '设置', pluralLabel: '设置',