diff --git a/skills/objectstack-data/rules/relationships.md b/skills/objectstack-data/rules/relationships.md index f855fc899..beaf184aa 100644 --- a/skills/objectstack-data/rules/relationships.md +++ b/skills/objectstack-data/rules/relationships.md @@ -249,6 +249,44 @@ inlineEdit: 'form' // read-only list; "Add" / per-row edit opens the FULL fo rich/form-only fields (textarea, file, image, json, location…) or more than ~8 editable fields, else `grid`. Set the string to override. +**Modeling a `'grid'` line item for the full editor.** The grid lights up extra +behaviors purely from how you model the child + parent (no UI config — details +in the objectstack-ui skill → Master-Detail Forms): + +```typescript +// Parent +Invoice = { + fields: { + tax_rate: Field.number({ label: 'Tax Rate (%)' }), // → live Subtotal/Tax/Total stack + total: Field.summary({ summaryOperations: { // server roll-up of the line subtotal + object: 'invoice_line', field: 'amount', function: 'sum' } }), + }, +} +// Child line +InvoiceLine = { + fields: { + invoice: Field.masterDetail('invoice', { inlineEdit: 'grid' }), + position: Field.number({ defaultValue: 0 }), // → drag-reorder, persisted (auto-hidden col) + product: Field.lookup('product', { required: true }), // → catalog typeahead + description: Field.text(), // ← auto-filled from product.description + quantity: Field.number({ required: true, defaultValue: 1 }), + unit_price: Field.currency(), // ← auto-filled from product.unit_price + amount: Field.currency({ expression: 'record.quantity * record.unit_price' }), // computed, read-only, live + }, +} +``` + +- **Computed column** — a *stored* `currency`/`number` field with an `expression` + renders read-only and recomputes live in the grid, then persists. Keep it + stored (NOT a `formula` field) so the parent `summary` can roll it up; the + server only treats `type: 'formula'` as computed, so on a stored field the + `expression` is a client compute hint and the sent value is stored verbatim. +- **Catalog auto-fill** — a `lookup` line field + sibling columns whose names + match fields on the referenced record (e.g. `unit_price`, `description`) → + picking a record fills those cells. +- **Sort field** — a numeric `position` / `sort_order` / `sequence` field is + auto-detected, hidden from the grid, and stamped on drag-reorder. + ### Detail-page related lists (the read-side mirror) Where `inlineEdit` is the **write** side (child pulled into the parent's entry diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index aefa6a1ab..a4df4bfc0 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -103,6 +103,39 @@ The relationship FK and grid columns are derived from the child object's metadata in every case; select options and lookups carry through. A parent `summary` field rolls child values up server-side (see objectstack-data). +**Line-item grid behaviors (`grid` mode).** The editable grid is a real +spreadsheet-style line editor (the QuickBooks / Stripe / NetSuite pattern). All +of the following come from the DATA MODEL — no UI config — so they apply to any +inline grid, not just invoices: + +- **Computed columns.** A child field with an arithmetic `expression` + (e.g. `amount: Field.currency({ expression: 'record.quantity * record.unit_price' })`) + renders **read-only** and is recomputed **live** client-side as its inputs + change, then persisted. Keep it a *stored* field (`currency`/`number`), NOT a + `formula` field, so a parent `summary` can still roll it up — the server only + treats `type: 'formula'` as computed, so a stored field's `expression` is a + client-side display/compute hint and the sent value is stored as-is. The + evaluator supports `+ - * / %`, parens and `record.` refs only. +- **Trailing "ghost" row.** The grid always shows one empty line at the bottom; + typing in it materialises a real row and a fresh ghost appears — users never + click "Add line", and an untouched ghost is never persisted. +- **Item typeahead auto-fill.** When a `lookup` cell's record is picked, the grid + copies the chosen record's fields into any **same-named** sibling columns + (e.g. a product's `unit_price` / `description` drop into the line). Model it by + giving the line a `lookup` to the catalog plus columns whose names match the + catalog fields. Opt out per column with `autofill: false`. +- **Persisted drag-reorder.** Add a numeric sort field to the child named + `position` (or `sort_order` / `sequence` / `line_no`). The grid auto-detects + it, hides it from the editable columns, and stamps `row[position] = index` on + reorder so line order survives a reload. +- **Totals stack.** Give the PARENT a tax-rate field named `tax_rate` (percent + number). The master-detail form then renders a live **Subtotal → Tax → Total** + block under the lines (override the field name with the form's `taxRateField`). + The parent `summary` persists the line subtotal; the tax-inclusive grand total + is a live entry-time aid. +- Per-cell **inline validation** (required-empty cells flag red in place) and a + hover **duplicate** action come for free. + **Read side — detail-page related lists.** The mirror of `inlineEdit` is the related list on the parent's record DETAIL page. You don't author it: every child relationship is shown as a related list by default (owned `master_detail`