Skip to content

Commit 2b50bbd

Browse files
qq9340100claude
andauthored
feat(example-showcase): plant the inline-grid time real-machine fixture on showcase_invoice_line (#6533) (#8655)
objectui#3569 split `date` / `datetime` / `time` into three distinct inline-grid controls. The `datetime` half got a long-lived showcase fixture (`showcase_expense_line.incurred_at`); the `time` half got only unit tests, so no running app has ever exercised the grid's time control. Adds `showcase_invoice_line.service_start` (type `time`, seeded `09:15` on the T&M service lines) and — crucially — declares the grid's `inlineColumns` explicitly. The explicit declaration is the point, not decoration. This line had exactly six editable fields, i.e. exactly `DEFAULT_MAX_INLINE_COLUMNS`. A seventh column makes auto-derivation curate, and the column that loses the tie-break is `receipt` (a `file` column, unranked in the fill-priority table, so it sorts last) — objectui#2360's upload-in-grid fixture. Demoting it into the column chooser is exactly the cost that kept `time` out of objectui#3569. Declaring the set routes through `hydrateColumns`, which never sets `defaultHidden`, so all seven columns stay default-visible and `receipt` keeps its visibility. Measured in the running app (showcase on a private port + DB, console built at the pinned .objectui-sha): before: Product | Description | Qty | Unit Price | Receipt | Amount after: Product | Description | Service Start | Qty | Unit Price | Receipt | Amount `Service Start` renders as a native time control; the seeded 09:15 reads back as 09:15 AM, and a value typed into an empty cell survived save + reopen. `service_start` is translated at birth (en + zh-CN) and only that field, because check-i18n-coverage freezes the example's untranslated count in both directions — the same rule the `incurred_at` fixture documented. Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91d09be commit 2b50bbd

3 files changed

Lines changed: 89 additions & 2 deletions

File tree

examples/app-showcase/src/data/objects/invoice.object.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,40 @@ export const InvoiceLine = ObjectSchema.create({
185185
// Thin, high-volume line items → the editable grid form factor.
186186
inlineEdit: 'grid',
187187
inlineTitle: 'Line Items',
188+
/**
189+
* The inline grid's columns, declared EXPLICITLY rather than auto-derived
190+
* — and the declaration is load-bearing, not decoration.
191+
*
192+
* Auto-derivation curates: past `DEFAULT_MAX_INLINE_COLUMNS` (6) it keeps
193+
* the primary + every required column, fills the rest by type usefulness,
194+
* and marks the overflow `defaultHidden` (the column chooser reveals it).
195+
* This line had exactly 6 editable fields, so nothing was curated. Adding
196+
* `service_start` below makes 7, and the one that loses the tie-break is
197+
* `receipt` — a `file` column, which the fill-priority table does not
198+
* rank, so it sorts last. That column is objectui#2360's upload-in-grid
199+
* fixture: demoting it out of the default view is precisely the cost that
200+
* kept `time` out of objectui#3569, and it is not a cost worth paying.
201+
*
202+
* Declaring the set opts out of curation entirely (`deriveDetail` routes
203+
* an author-supplied set through `hydrateColumns`, which never sets
204+
* `defaultHidden`), so all seven stay default-visible and `receipt`'s
205+
* visibility stops depending on a tie-break it happens to be losing.
206+
*
207+
* Bare `{ field }` entries on purpose: `hydrateColumns` fills label, type,
208+
* options, lookup target, `readonlyWhen`/`requiredWhen` and the computed
209+
* `expression` from the schema, so labels stay translatable and the
210+
* columns cannot drift from the field definitions above. `position` is
211+
* absent because it is the grid's drag-reorder sort field, never a cell.
212+
*/
213+
inlineColumns: [
214+
{ field: 'product' },
215+
{ field: 'description' },
216+
{ field: 'service_start' },
217+
{ field: 'quantity' },
218+
{ field: 'unit_price' },
219+
{ field: 'receipt' },
220+
{ field: 'amount' },
221+
],
188222
}),
189223
// Catalog lookup. Picking a product auto-fills `description` + `unit_price`
190224
// (the grid copies same-named fields from the selected product record).
@@ -211,6 +245,29 @@ export const InvoiceLine = ObjectSchema.create({
211245
maxLength: 200,
212246
requiredWhen: P`record.quantity >= 100`,
213247
}),
248+
/**
249+
* Clock time the billed work started — the inline grid's `time` fixture
250+
* (objectui#3569). ⛔ Do not "tidy" this field away: it is the ONLY place in
251+
* showcase where a `time` field sits inside an inline-edit grid, and the
252+
* grid's time control has no other real-machine coverage. `field_zoo.f_time`
253+
* seeds a `time` value but is not a master-detail child, so it never reaches
254+
* this rendering path.
255+
*
256+
* objectui#3569 split `date` / `datetime` / `time` into three grid controls;
257+
* a renderer that folds `time` back onto the `date` control feeds `HH:mm`
258+
* into an `<input type="date">`, which shows nothing and writes the clock
259+
* out of the record on the next save. Only a running grid can catch that.
260+
*
261+
* Honest on a T&M line, and deliberately scoped: a services line bills hours
262+
* (`quantity`) at a rate (`unit_price`), so a start clock plus that duration
263+
* describes the whole billed window — no second `service_end` field that
264+
* would only restate it. Goods lines leave it empty, which is also the
265+
* fixture's empty-cell case.
266+
*
267+
* Authored as a literal `{ type: 'time' }` because there is no `Field.time`
268+
* builder — the same form `showcase_field_zoo.f_time` uses.
269+
*/
270+
service_start: { type: 'time', label: 'Service Start' },
214271
quantity: Field.number({
215272
label: 'Qty',
216273
required: true,

examples/app-showcase/src/data/seed/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,15 +383,25 @@ const invoices = defineSeed(Invoice, {
383383
// Line items — `product` resolves by SKU (the Product seed's externalId), and
384384
// `invoice` by invoice number. A contributor reaches a line only through its
385385
// master invoice, so these inherit the invoice's owner scoping.
386+
//
387+
// `service_start` is the inline grid's `time` FIXTURE (objectui#3569) - the one
388+
// place in showcase where a `time` field is rendered by an inline-edit grid.
389+
// DO NOT blank these clocks in a seed tidy-up: a zeroed or missing value leaves
390+
// the fixture unable to show the defect it exists to catch (a `time` column
391+
// folded onto the `date` control renders empty and writes the clock out of the
392+
// record on the next save), so the seeded values are deliberately NON-ZERO and
393+
// recognisable. Only the T&M service lines carry one - hours billed from a
394+
// start clock; the goods lines are empty on purpose, which is the fixture's
395+
// empty-cell case.
386396
const invoiceLines = defineSeed(InvoiceLine, {
387397
mode: 'upsert',
388398
externalId: 'description',
389399
records: [
390-
{ description: 'INV-1001 \u00b7 Consulting hours', invoice: 'INV-1001', product: 'SERVICE-HR', position: 0, quantity: 10, unit_price: 150, amount: 1500 },
400+
{ 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' },
391401
{ description: 'INV-1001 \u00b7 Widget A units', invoice: 'INV-1001', product: 'WIDGET-A', position: 1, quantity: 4, unit_price: 29.99, amount: 119.96 },
392402
{ description: 'INV-1002 \u00b7 Gadget X units', invoice: 'INV-1002', product: 'GADGET-X', position: 0, quantity: 2, unit_price: 99, amount: 198 },
393403
{ description: 'INV-1003 \u00b7 Widget B units', invoice: 'INV-1003', product: 'WIDGET-B', position: 0, quantity: 6, unit_price: 49.99, amount: 299.94 },
394-
{ description: 'INV-1004 \u00b7 Consulting hours', invoice: 'INV-1004', product: 'SERVICE-HR', position: 0, quantity: 3, unit_price: 150, amount: 450 },
404+
{ 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' },
395405
],
396406
});
397407

examples/app-showcase/src/system/translations/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ export const ShowcaseTranslationBundle = {
140140
incurred_at: { label: 'Incurred At' },
141141
},
142142
},
143+
// Same rule, same reason as `showcase_expense_line` above: `service_start`
144+
// is a NEW declared label (objectui#3569's inline-grid TIME fixture), so
145+
// it must be translated at birth or check-i18n-coverage sees the example's
146+
// untranslated count grow and fails. And DELIBERATELY only this one field
147+
// — `product`, `description`, `quantity`, `unit_price`, `receipt` and
148+
// `amount` predate the ratchet and are part of the frozen baseline;
149+
// translating them here would push the count BELOW the baseline, which the
150+
// same gate rejects as an un-ratcheted improvement.
151+
showcase_invoice_line: {
152+
fields: {
153+
service_start: { label: 'Service Start' },
154+
},
155+
},
143156
showcase_preference: {
144157
label: 'Setting',
145158
pluralLabel: 'Settings',
@@ -444,6 +457,13 @@ export const ShowcaseTranslationBundle = {
444457
incurred_at: { label: '发生时间' },
445458
},
446459
},
460+
// See the `en` side for why this entry translates exactly ONE field and
461+
// no more (check-i18n-coverage is a two-sided ratchet).
462+
showcase_invoice_line: {
463+
fields: {
464+
service_start: { label: '服务开始时间' },
465+
},
466+
},
447467
showcase_preference: {
448468
label: '设置',
449469
pluralLabel: '设置',

0 commit comments

Comments
 (0)