From 0aa9d333f25e0cc674dffe73a8a7e852ddf9ecda Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 8 Jun 2026 21:52:14 +0800 Subject: [PATCH] feat(showcase): lock invoice lines when the invoice is paid (#1581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add parent-scoped readonlyWhen rules to showcase_invoice_line's product, quantity and unit_price: readonlyWhen "parent.status == 'paid'". The inline line-item grid evaluates these per row against the live header invoice, so a paid invoice's lines render read-only — the "paid invoice → lock lines" case. Pairs with objectui#1607 (MasterDetailForm binds the header as `parent`). Co-Authored-By: Claude Opus 4.8 --- .../src/objects/invoice.object.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/examples/app-showcase/src/objects/invoice.object.ts b/examples/app-showcase/src/objects/invoice.object.ts index 7d014283f..4c5395488 100644 --- a/examples/app-showcase/src/objects/invoice.object.ts +++ b/examples/app-showcase/src/objects/invoice.object.ts @@ -116,20 +116,39 @@ export const InvoiceLine = ObjectSchema.create({ // Line sort position — stamped by the grid on drag-reorder so the order // persists. Excluded from the editable columns (it's not hand-entered). position: Field.number({ label: 'Position', defaultValue: 0 }), - product: Field.lookup('showcase_product', { label: 'Product', required: true }), + // Conditional rule (B2 in grids, PARENT-scoped): once the header invoice is + // Paid, its lines are frozen. `readonlyWhen` here references the header as + // `parent`, so the inline grid evaluates it per row against the live invoice + // record and locks the cell — the "paid invoice → lock lines" case (#1581). + product: Field.lookup('showcase_product', { + label: 'Product', + required: true, + readonlyWhen: "parent.status == 'paid'", + }), // Conditional rule (B2 in grids): a bulk line (large quantity) must carry a // description note. `requiredWhen` here is ROW-scoped — it references the // line's own `record`, so the inline grid flags this cell required per row - // as the quantity crosses the threshold. (Row-scoped rules need no header - // context; a header-driven lock like "paid invoice → lock lines" is a - // separate, deferred capability — see ADR-0036.) + // as the quantity crosses the threshold. (A header-driven lock referencing + // `parent` — see `product`/`quantity`/`unit_price` — is the parent-scoped + // counterpart; both are evaluated by the inline grid. See ADR-0036 / #1581.) description: Field.text({ label: 'Description', maxLength: 200, requiredWhen: 'record.quantity >= 100', }), - quantity: Field.number({ label: 'Qty', required: true, min: 0, defaultValue: 1 }), - unit_price: Field.currency({ label: 'Unit Price', scale: 2, min: 0 }), + quantity: Field.number({ + label: 'Qty', + required: true, + min: 0, + defaultValue: 1, + readonlyWhen: "parent.status == 'paid'", + }), + unit_price: Field.currency({ + label: 'Unit Price', + scale: 2, + min: 0, + readonlyWhen: "parent.status == 'paid'", + }), // Amount = Qty × Unit Price. Kept as a *stored* currency column (so the // parent Invoice.total summary can roll it up — summary aggregation reads // stored columns, not on-read formula fields), but the `expression` makes