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
16 changes: 16 additions & 0 deletions .changeset/11070-field-metadata-spec-spellings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@object-ui/types': minor
'@object-ui/fields': minor
---

The formula and summary field widgets read `@objectstack/spec`'s own spellings, `returnType` and `summaryOperations`, and the snake_case spellings they used to read are retired at once (objectui#11070).

**Visible change.** Object metadata carries the spec spellings: the platform's example apps write `returnType` on a formula field and `summaryOperations: { object, field, function }` on a roll-up, and the metadata admin stamps `returnType` from the inferred formula type. The widgets read only `return_type` and `summary_type`, so on an object-bound form a formula or summary field rendered without its type formatting: a number formula showed `3` instead of `3.00`, a boolean formula `true` instead of `Yes`, a date formula the raw `2026-07-04` instead of the shared date face, and a `sum` roll-up `15750.5` instead of `15750.50`. They now format by what the definition declares.

- **`@object-ui/fields`:** `FormulaField` reads `returnType` (`number` to two decimals, `boolean` as Yes/No, `date` through the shared date face, anything else as text). `SummaryField` reads `summaryOperations.function` (`count` as it arrives, `sum` / `avg` / `min` / `max` to two decimals). Neither reads a snake_case spelling any more. `FormulaField` also no longer formats a `currency` return type, which the spec does not list.
- **`@object-ui/types`, the form-field face:** `FormField` and its zod mirror `FormFieldSchema` declare `returnType` and `summaryOperations`, each the spec's `FieldSchema` member by reference, with the spec's own value rules. The strict authoring face (`StrictAnyComponentSchema`) accepts them on a hand-authored form's field, and judges them: a `returnType` of `datetime`, a `summaryOperations` with no `function`, a `function` of `first`, or an unknown member inside `summaryOperations` is refused on both faces. The tolerant face (`AnyComponentSchema`) used to strip both keys from a form field unjudged, so those values were accepted silently before this change: that half is a narrowing.
- **`@object-ui/types`, the field metadata types:** `FormulaFieldMetadata.returnType` replaces `return_type`; `SummaryFieldMetadata.summaryOperations` replaces `summary_object`, `summary_field`, `summary_type` and `summary_filter`; `PasswordFieldMetadata.minLength` / `maxLength` replace `min_length` / `max_length`. Each is the spec member by reference.

**Breaking, priced as minor under the fixed group's version policy.** A literal annotated as `FormulaFieldMetadata`, `SummaryFieldMetadata` or `PasswordFieldMetadata` that still writes a retired member no longer compiles (an excess-property error naming the key), and at runtime a formula or summary field that carries only `return_type` or `summary_type` renders its value unformatted. `@objectstack/spec`'s `FieldSchema` refuses every retired spelling by name, so no spec-compliant producer writes them. The fix is the spec spelling: `returnType`, and `summaryOperations: { object, field, function }` (with `filter` for the former `summary_filter`).

**Not in this change.** `LookupFieldMetadata.reference_to` and the lookup readers' `reference_to` read stay: in-repo producers still write `reference_to` onto the field definitions those readers are handed, so retiring the spelling is a separate decision on objectui#11070. The strict face keeps refusing `return_type`, `summary_type`, `summary_object` and `summary_field` on a hand-authored form's field, as before.
8 changes: 8 additions & 0 deletions .changeset/6138-fields-schema-block-parity-pr2.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ no shipped type declares:
- `user.mdx` documented the value as a user object; the field stores the user's
id and the picker resolves the rest from `sys_user`.

⚠️ **Dated note, 2026-09-30 — the `return_type` union above has since been retired —
objectui#11070.** "The shipped union is `'text' | 'number' | 'boolean' | 'date' |
'datetime'`" above held when this change landed. Later in this same release objectui#11070
(round 3) retired `FormulaFieldMetadata.return_type` for `@objectstack/spec`'s
`returnType`, typed by reference (`'number' | 'text' | 'boolean' | 'date'`), and
`formula.mdx` teaches that key. `.changeset/11070-field-metadata-spec-spellings.md` states
what ships; the text above is kept as the reading of this change.

Two undeclared-but-consumed keys were found by checking each divergence against
its renderer, and are filed rather than deleted or documented as metadata:
`dependsOn` on select and `description` on a lookup's static options
Expand Down
8 changes: 8 additions & 0 deletions .changeset/8194-fields-date-widget-convention.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ default — so they never implemented the year-dropping decision the shared
which sits in the same function as the descriptor path that already rendered
through `formatDate`.

⚠️ **Dated note, 2026-09-30 — the formula key above has since been renamed —
objectui#11070.** "A `FormulaField` declaring `return_type: 'date'`" above held when this
change landed. Later in this same release objectui#11070 (round 3) moved `FormulaField` to
`@objectstack/spec`'s `returnType` and retired the `return_type` read, so the face described
here is reached by a formula declaring `returnType: 'date'`.
`.changeset/11070-field-metadata-spec-spellings.md` states what ships; the text above is
kept as the reading of this change.

**Visible change**: every one of those faces changes shape in every locale, in
every year — not only the year token. In `en-US` a date renders `Jul 4` this
year and `Jul 4, 2024` for a past year, where it used to render `7/4/2026` and
Expand Down
8 changes: 8 additions & 0 deletions .changeset/8809-date-carrier-unparsable.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The readonly `date` widget faces draw `formatDate`'s em-dash through the shared
`EmptyValue` affordance instead of a plain span (objectui#8809). Two sites:
`DateField`'s readonly branch and `FormulaField`'s `return_type: 'date'` path.

⚠️ **Dated note, 2026-09-30 — the formula key above has since been renamed —
objectui#11070.** "`FormulaField`'s `return_type: 'date'` path" above held when this change
landed. Later in this same release objectui#11070 (round 3) moved `FormulaField` to
`@objectstack/spec`'s `returnType` and retired the `return_type` read, so that path is the
one a formula declaring `returnType: 'date'` takes.
`.changeset/11070-field-metadata-spec-spellings.md` states what ships; the text above is
kept as the reading of this change.

A truthy value `new Date(...)` cannot read — `not-a-date`, `2024-13-45` — used
to reach `formatDate`, which answers it with its own em-dash, and that dash was
painted in a span with no `data-slot` of `empty-value` and no accessible name.
Expand Down
16 changes: 8 additions & 8 deletions content/docs/fields/formula.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ The Formula Field component displays computed values calculated from other field

A formula field is authored as `FormulaFieldMetadata` (`@object-ui/types`), which is
the source of truth for the key set: it extends `BaseFieldMetadata` with the
expression, its declared return type and the recompute switch. `return_type` is a
closed union — `'text' | 'number' | 'boolean' | 'date' | 'datetime'`.
expression, its declared return type and the recompute switch. `returnType` is
`@objectstack/spec`'s own `FieldSchema.returnType`, typed by reference: a closed
union of `'number' | 'text' | 'boolean' | 'date'`.

```ts
import type { FormulaFieldMetadata } from '@object-ui/types';
Expand All @@ -33,7 +34,7 @@ const totalPrice: FormulaFieldMetadata = {
label: 'Total Price',
readonly: true,
formula: 'quantity * unit_price',
return_type: 'number',
returnType: 'number',
auto_compute: true,
};
```
Expand All @@ -43,13 +44,12 @@ they are runtime widget props. See [Field Widget Props](/docs/fields/widget-prop

## Return Types

The formula field formats values based on return type:
The formula field formats values by `returnType`:

- **number**: Displays with decimal precision
- **currency**: Displays with currency symbol
- **number**: Displays with two decimal places
- **boolean**: Displays as Yes/No
- **date**: Displays formatted date
- **text**: Displays as string
- **date**: Displays the formatted date
- **text**: Displays as a string (also the default when `returnType` is absent)

## Formula Examples

Expand Down
7 changes: 4 additions & 3 deletions content/docs/fields/password.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ The Password Field component provides a secure text input for passwords with a t

A password field is authored as `PasswordFieldMetadata` (`@object-ui/types`), which is
the source of truth for the key set: it extends `BaseFieldMetadata` with the two length
bounds. The reveal toggle and the masked read-only rendering are the widget's.
bounds, `minLength` and `maxLength` — `@objectstack/spec`'s own `FieldSchema` members,
typed by reference. The reveal toggle and the masked read-only rendering are the widget's.

```ts
import type { PasswordFieldMetadata } from '@object-ui/types';
Expand All @@ -32,8 +33,8 @@ const password: PasswordFieldMetadata = {
label: 'Password',
placeholder: 'Enter a password',
required: true,
min_length: 12,
max_length: 128,
minLength: 12,
maxLength: 128,
};
```

Expand Down
72 changes: 44 additions & 28 deletions content/docs/fields/summary.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ The Summary Field component displays aggregated values from related records. Thi
## Field Schema

A summary field is authored as `SummaryFieldMetadata` (`@object-ui/types`), which is
the source of truth for the key set: it extends `BaseFieldMetadata` with the related
object, the aggregated field, the aggregation and its filter. `summary_type` is a
closed union — `'count' | 'sum' | 'avg' | 'min' | 'max' | 'first' | 'last'`.
the source of truth for the key set: it extends `BaseFieldMetadata` with the roll-up
definition `summaryOperations` and the auto-update switch. `summaryOperations` is
`@objectstack/spec`'s own `FieldSchema.summaryOperations`, typed by reference: the
child `object`, the child `field` to aggregate, the aggregation `function` — a closed
union of `'count' | 'sum' | 'min' | 'max' | 'avg'` — and optionally the child's
`relationshipField` and a `filter` restricting which child rows are aggregated.

```ts
import type { SummaryFieldMetadata } from '@object-ui/types';
Expand All @@ -32,18 +35,23 @@ const totalRevenue: SummaryFieldMetadata = {
name: 'total_revenue',
label: 'Total Revenue',
readonly: true,
summary_object: 'opportunities',
summary_field: 'amount',
summary_type: 'sum',
summary_filter: { stage: 'closed_won' },
summaryOperations: {
object: 'opportunities',
field: 'amount',
function: 'sum',
filter: { stage: 'closed_won' },
},
auto_update: true,
};
```

The computed value, and the `className` a host supplies, are **not** metadata keys —
they are runtime widget props. See [Field Widget Props](/docs/fields/widget-props).

## Summary Types
## Aggregation Functions

The summary field formats values by `summaryOperations.function`: `count` as it
arrives, the other four to two decimal places.

- **count**: Count of related records
- **sum**: Sum of numeric field values
Expand All @@ -60,9 +68,11 @@ they are runtime widget props. See [Field Widget Props](/docs/fields/widget-prop
type: 'summary',
name: 'task_count',
label: 'Open Tasks',
summary_object: 'tasks',
summary_field: 'id',
summary_type: 'count'
summaryOperations: {
object: 'tasks',
field: 'id',
function: 'count'
}
}
```

Expand All @@ -73,9 +83,11 @@ they are runtime widget props. See [Field Widget Props](/docs/fields/widget-prop
type: 'summary',
name: 'total_sales',
label: 'Total Sales',
summary_object: 'invoices',
summary_field: 'amount',
summary_type: 'sum'
summaryOperations: {
object: 'invoices',
field: 'amount',
function: 'sum'
}
}
```

Expand All @@ -86,9 +98,11 @@ they are runtime widget props. See [Field Widget Props](/docs/fields/widget-prop
type: 'summary',
name: 'avg_response_time',
label: 'Avg Response Time',
summary_object: 'support_tickets',
summary_field: 'response_time_hours',
summary_type: 'avg'
summaryOperations: {
object: 'support_tickets',
field: 'response_time_hours',
function: 'avg'
}
}
```

Expand All @@ -99,9 +113,11 @@ they are runtime widget props. See [Field Widget Props](/docs/fields/widget-prop
type: 'summary',
name: 'highest_bid',
label: 'Highest Bid',
summary_object: 'bids',
summary_field: 'amount',
summary_type: 'max'
summaryOperations: {
object: 'bids',
field: 'amount',
function: 'max'
}
}
```

Expand All @@ -124,24 +140,24 @@ Summary fields are calculated through database aggregations:

```plaintext
// Example backend aggregation
const calculateSummary = async (config: SummaryConfig, parentId: string) => {
const { summary_object, summary_field, summary_type } = config;
const calculateSummary = async (summaryOperations: SummaryOperations, parentId: string) => {
const { object, field, function: fn } = summaryOperations;

switch (summary_type) {
switch (fn) {
case 'count':
return db.count(summary_object, { parent_id: parentId });
return db.count(object, { parent_id: parentId });

case 'sum':
return db.sum(summary_object, summary_field, { parent_id: parentId });
return db.sum(object, field, { parent_id: parentId });

case 'avg':
return db.avg(summary_object, summary_field, { parent_id: parentId });
return db.avg(object, field, { parent_id: parentId });

case 'min':
return db.min(summary_object, summary_field, { parent_id: parentId });
return db.min(object, field, { parent_id: parentId });

case 'max':
return db.max(summary_object, summary_field, { parent_id: parentId });
return db.max(object, field, { parent_id: parentId });
}
};
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"name": "due_date",
"label": "Due Date",
"type": "formula",
"formula": "created_at + 30 days",
"return_type": "date",
"expression": "created_at + 30 days",
"returnType": "date",
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"name": "total",
"label": "Total Amount",
"type": "formula",
"formula": "quantity * price",
"return_type": "number",
"expression": "quantity * price",
"returnType": "number",
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"name": "full_name",
"label": "Full Name",
"type": "formula",
"formula": "first_name + \" \" + last_name",
"return_type": "text",
"expression": "first_name + \" \" + last_name",
"returnType": "text",
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"name": "avg_rating",
"label": "Average Rating",
"type": "summary",
"summary_object": "reviews",
"summary_field": "rating",
"summary_type": "avg",
"summaryOperations": {
"object": "reviews",
"field": "rating",
"function": "avg"
},
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"name": "order_count",
"label": "Total Orders",
"type": "summary",
"summary_object": "orders",
"summary_field": "id",
"summary_type": "count",
"summaryOperations": {
"object": "orders",
"field": "id",
"function": "count"
},
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"name": "total_revenue",
"label": "Total Revenue",
"type": "summary",
"summary_object": "orders",
"summary_field": "amount",
"summary_type": "sum",
"summaryOperations": {
"object": "orders",
"field": "amount",
"function": "sum"
},
"readonly": true
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ const SITES: ReadonlyArray<readonly [string, (locale: string, value: unknown) =>
).container,
],
[
'FormulaField (`return_type: date`)',
'FormulaField (`returnType: date`)',
(locale, value) =>
session(
locale,
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', return_type: 'date' } as any} />,
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', returnType: 'date' }} />,
).container,
],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ const FACES: ReadonlyArray<readonly [string, (value: unknown) => HTMLElement]> =
).container,
],
[
'FormulaField (`return_type: date`)',
'FormulaField (`returnType: date`)',
(value) =>
session(
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', return_type: 'date' } as any} />,
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', returnType: 'date' }} />,
).container,
],
];
Expand Down
2 changes: 1 addition & 1 deletion packages/fields/src/__tests__/date-locale-channel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ describe('zh session — every date branch renders Chinese (objectui#4468)', ()
<FormulaField
value={FIXED_INSTANT}
onChange={() => {}}
field={{ type: 'formula', name: 'computed_on', return_type: 'date' } as any}
field={{ type: 'formula', name: 'computed_on', returnType: 'date' }}
/>,
);
expect(container.textContent).toContain(defaultDateFace(FIXED_INSTANT, 'zh'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ const FACES: ReadonlyArray<readonly [string, (value: unknown) => HTMLElement]> =
).container,
],
[
'FormulaField (`return_type: date`, a date-time result)',
'FormulaField (`returnType: date`, a date-time result)',
(value) =>
session(
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', return_type: 'date' } as any} />,
<FormulaField value={value as any} onChange={() => {}} field={{ type: 'formula', name: 'c', returnType: 'date' }} />,
).container,
],
];
Expand Down
Loading
Loading