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
12 changes: 12 additions & 0 deletions .changeset/28256-data-skill-percent-display-lookup-autofill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@objectstack/skills': patch
---

docs(skills): the objectstack-data field-types rule teaches `percentDisplay` and `lookupAutofill`

Clause-②: no
Metadata-protocol: none

- The `percent` row of the field-type table lists `percentDisplay`: unset or `'bar'` draws the progress bar, `'plain'` prints the number alone, for a rate or a probability.
- The Relational section teaches `lookupAutofill: [{ target, source, overwrite? }]` on a single-value `lookup` / `master_detail`: which targets a pick may fill, which sources it may copy, the overwrite rule, that the fill is client-side only, and how it relates to a line-item grid column's `autofill`.
- Paid inside the file's token ceiling: the `Select with Default`, `Email with Uniqueness` and `Lookup (Many-to-Many)` examples go (each restated a rule the skill carries elsewhere), and the `vectorConfig` trap folds into the invented-key list at the top of the file.
41 changes: 41 additions & 0 deletions content/docs/data-modeling/fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ primary_contact: Field.lookup('contact', {
],
}),

// A pick fills sibling fields: the picked product's list_price lands in unit_price
product: Field.lookup('product', {
label: 'Product',
required: true,
lookupAutofill: [{ target: 'unit_price', source: 'list_price' }],
}),

// Master-detail (cascade delete)
project: Field.masterDetail('project', {
label: 'Project',
Expand All @@ -236,6 +243,40 @@ order: Field.masterDetail('order', {
| `inlineEdit` | `boolean \| 'grid' \| 'form'` | Render child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) |
| `inlineColumns` | `array` | Optional explicit columns for the inline grid |
| `inlineAmountField` | `string` | Optional numeric child field for the inline running total |
| `lookupAutofill` | `{ target, source, overwrite? }[]` | Fields of this object a pick fills from the picked record (single-value `lookup` / `master_detail` only) — see below |

#### Lookup-pick autofill: `lookupAutofill`

A single-value `lookup` / `master_detail` can declare `lookupAutofill: [{ target,
source, overwrite? }]`. When a user picks a record in that field, each pair copies
the picked record's `source` field (a field of the `reference` object) into this
object's `target` field — in the record form, and in a line-item grid column over
the field. The example above fills a quote line's `unit_price` from the picked
product's `list_price`.

- **`target`** is a field of this object that an author writes: not the lookup
itself, not `readonly`, and not computed (`formula`, `summary`, `autonumber`, or
a stored field with an `expression`). Each target is named once per field. The
object's parse refuses anything else.
- **`source`** is a field the referenced object declares and the record carries
(not `internal`), and its value must be one the target can hold: the same type
and multiplicity (a list only into a list),
a string-valued field into a text target, `number` and `currency` into each
other, a relationship only into a relationship over the same object. `os
validate` / `os build` judge the source against the referenced object.
- **Overwrite rule.** By default a pick never replaces a value somebody wrote: the
target is filled only while it is empty or still holds the value an earlier pick
filled in. A pair with `overwrite: true` replaces the target on every pick.
- **Client-side only.** The fill edits the form, and the filled value is saved as an
ordinary write. Nothing on the write path reads the key: an import or an API
write that sets the lookup fills nothing.
- **In a line-item grid**, the column key `autofill` still governs only the grid's
same-named copy; a declared pair applies under its own overwrite rule and takes
its target out of that copy. A master-detail collection whose `relationshipField`
is set and whose every column carries its `type` loads no child fields, so it
sees no pairs and keeps only the same-named copy.

The key's full contract is in the [Field reference](/docs/references/data/field).

#### Referential integrity

Expand Down
60 changes: 14 additions & 46 deletions skills/objectstack-data/rules/field-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ fields: {

> **Config columns list only real `FieldSchema` keys.** Per-type display knobs
> beyond these do **not** exist — an unknown field key is REFUSED at parse
> (`unrecognized_keys`), so don't invent `theme`, `rows`, or
> `fileAttachmentConfig`. Source of truth:
> (`unrecognized_keys`), so don't invent `theme`, `rows`,
> `vectorConfig` or `fileAttachmentConfig`. Source of truth:
> `node_modules/@objectstack/spec/src/data/field.zod.ts`.

## Text & Content
Expand All @@ -51,7 +51,7 @@ fields: {
|:-----|:------------|:-------|
| `number` | Generic numeric value | `min`, `max`, `precision`, `scale` |
| `currency` | Monetary amounts | `currencyConfig` (currencyMode, defaultCurrency, currencyField, homeAmount) |
| `percent` | Percentage values (0-100) | `min`, `max`, `precision` |
| `percent` | Percentage values (0-100) | `min`, `max`, `precision`, `percentDisplay` — the read-only face: unset or `'bar'` draws a progress bar, `'plain'` the number alone, for a rate or a probability |

## Date & Time

Expand Down Expand Up @@ -90,11 +90,20 @@ options: [

| Type | When to Use | Key Config |
|:-----|:------------|:-----------|
| `lookup` | Reference another object (independent) | `reference`, `lookupFilters`, `multiple`, `deleteBehavior` |
| `master_detail` | Parent–child with lifecycle control | `reference`, `deleteBehavior` (`cascade`/`restrict` — `set_null` is refused) |
| `lookup` | Reference another object (independent) | `reference`, `lookupFilters`, `multiple`, `deleteBehavior`, `lookupAutofill` |
| `master_detail` | Parent–child with lifecycle control | `reference`, `deleteBehavior` (`cascade`/`restrict` — `set_null` is refused), `lookupAutofill` |
| `tree` | Hierarchical self-reference | `reference` — optional; when written, this object's own name (any other object is refused at parse) |
| `user` | Person picker — a lookup specialized to `sys_user` (assignee, watchers). Stored identically to `lookup` | `multiple` (collaborators), `defaultValue: 'current_user'` |

**`lookupAutofill: [{ target, source, overwrite? }]`** (single-value `lookup` /
`master_detail`): a pick copies the picked record's `source` into this object's
`target` (`[{ target: 'unit_price', source: 'list_price' }]` on a product lookup),
in the form and the line-item grid. `target`: a field of this object an author
writes (not the lookup, `readonly` or computed), named once. `source`: a declared,
non-`internal` field of `reference` the target can hold. A pick fills an empty
target or an earlier pick's fill; `overwrite: true` always replaces. Client-side
only; in a grid a declared target leaves the same-named `autofill` copy.

## Media

| Type | When to Use | Config |
Expand Down Expand Up @@ -142,19 +151,6 @@ Stored as JSON on the parent row — no separate table / FK:

## Common Field Configurations

### Email with Uniqueness

```typescript
{
type: 'email',
required: true,
// State the scope (ADR-0120): 'organization' = one holder per organization
// (NULL-safe), 'global' = one across the whole installation. Bare `true` at
// field level still means 'organization' and stays valid.
unique: 'organization',
}
```

### Currency

```typescript
Expand Down Expand Up @@ -182,20 +178,6 @@ home-currency amount in `field` (a `'dynamic'` `currency`) at the `rateDate`
(`date`/`datetime`) rate, and that rate in `rateField` (`number`), both
`readonly: true`. Cross-currency totals sum `field`.

### Select with Default

```typescript
{
type: 'select',
required: true,
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium', default: true },
{ label: 'High', value: 'high', color: '#e74c3c' },
],
}
```

### Lookup (One-to-Many)

```typescript
Expand All @@ -209,17 +191,6 @@ home-currency amount in `field` (a `'dynamic'` `currency`) at the `rateDate`
}
```

### Lookup (Many-to-Many)

```typescript
{
type: 'lookup',
reference: 'tag',
multiple: true,
max: 10,
}
```

### Formula

```typescript
Expand Down Expand Up @@ -281,9 +252,6 @@ The `format` is literal text interleaved with `{...}` tokens:
}
```

There is **no** `vectorConfig` block — an authored `vectorConfig` is refused at
parse. `dimensions` is the flat field-level key.

## Incorrect vs Correct

### ❌ Incorrect — Autonumber interpolating an optional / adjacent field
Expand Down
Loading