Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions skills/objectstack-data/rules/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions skills/objectstack-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<field>` 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`
Expand Down