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
45 changes: 45 additions & 0 deletions .changeset/21182-object-image-field.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'@objectstack/spec': minor
---

feat(spec): an object declares which of its fields is the record's picture — `imageField`, beside `nameField`

Clause-②: yes (widening)

`ObjectSchema` accepts one more optional key, `imageField`. It names the field
whose value is the record's picture, the way `nameField` names the field that is
the record's name: one object-level declaration, read by the record page header
(the record chrome every record detail page shares) — not a per-page
`page:header` prop.

```ts
defineStack({
objects: [{
name: 'crm_account',
nameField: 'name',
imageField: 'logo',
fields: {
name: Field.text({ label: 'Name' }),
logo: Field.image({ label: 'Logo' }),
},
}],
});
```

- **What it may name.** A field of the same object whose type is `image` or
`avatar`. A name the object does not declare, or a field of any other type
(a `text` URL column, a `file`), is refused at parse with an issue at
`imageField` that names the two accepted types — so `defineStack`,
`ObjectSchema.create()`, `os validate` and the metadata save door
(`422 INVALID_METADATA`) all refuse it.
- **An empty field.** The contract the reader is held to: a record whose
picture field is empty shows no picture — no initials or placeholder in its
place.
- **Who draws it.** No renderer reads the key yet. The record chrome in
`@object-ui/components` is the reader to come, and until it lands an authored
`imageField` is accepted, stored and served but nothing draws it. It takes
effect when that renderer ships, with no re-authoring. The liveness ledger
records the key as `planned`.

Nothing that parsed before is refused: the key is new, and an object that does
not set it is unchanged.
1 change: 1 addition & 0 deletions content/docs/data-modeling/objects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const Account = ObjectSchema.create({
| :--- | :--- | :--- | :--- |
| `nameField` | `string` | optional | The stored field used as the record display name, e.g. `'name'` or `'title'` (ADR-0079). The deprecated alias `displayNameField` is still accepted. |
| `titleFormat` | `string` | optional | Deprecated (ADR-0079 → `nameField`). Render-only title template (e.g. `'{{record.name}} - {{record.code}}'`); an explicit `nameField` takes precedence |
| `imageField` | `string` | optional | The record's picture: names a field of this object whose type is `image` or `avatar` — any other name is refused when the object is validated or published. It is the one object-level declaration the record page header is to draw beside the title once the renderer reads it; no renderer draws it yet. |
| `highlightFields` | `string[]` | optional | Most-important fields in priority order — default list columns, cards, previews, detail highlight strip (ADR-0085; formerly `compactLayout` — the old spelling was retired and is now rejected) |
| `stageField` | `string \| false` | optional | Linear lifecycle field; `false` declares the status field non-linear and suppresses stage heuristics (ADR-0085) |

Expand Down
1 change: 1 addition & 0 deletions content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ Metadata query with filtering, sorting, and pagination
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) is to draw beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). Pending renderer: the record chrome does not draw it yet. |
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const result = ApiMethod.parse(data);
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) is to draw beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). Pending renderer: the record chrome does not draw it yet. |
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
Expand Down
2 changes: 2 additions & 0 deletions content/docs/references/system/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ Create a new object
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) is to draw beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). Pending renderer: the record chrome does not draw it yet. |
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
Expand Down Expand Up @@ -627,6 +628,7 @@ Create a new object
| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
| **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) is to draw beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). Pending renderer: the record chrome does not draw it yet. |
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,13 @@ describe('#17501 — /meta/types serves a real schema for `action`, and moves no
* against the SERVED document. They prove two things at once: this suite
* measures the same thing the card measured, and none of the thirteen was
* disturbed by the fix. `field` moved 74 → 75 when it gained `picklist`
* (the shared-option-list reference), a declared key, not a derivation change.
* (the shared-option-list reference), a declared key, not a derivation change;
* `object` moved 43 → 44 the same way when it gained `imageField` (the
* record's picture, beside `nameField`).
*/
const CARD_PROPERTY_COUNTS: Record<string, number> = {
agent: 26, app: 30, dashboard: 21, dataset: 16, field: 75, flow: 23,
hook: 22, object: 43, page: 24, position: 12, report: 21, skill: 17, tool: 14,
hook: 22, object: 44, page: 24, position: 12, report: 21, skill: 17, tool: 14,
};

it.each(Object.entries(CARD_PROPERTY_COUNTS))(
Expand Down
1 change: 1 addition & 0 deletions packages/spec/authorable-surface/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@
"data/Object:fileAccessDelegate",
"data/Object:highlightFields",
"data/Object:icon",
"data/Object:imageField",
"data/Object:indexes",
"data/Object:isSystem",
"data/Object:label",
Expand Down
7 changes: 7 additions & 0 deletions packages/spec/liveness/object.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
"status": "live",
"note": "ADR-0079 canonical record-title pointer; read by data/display-name.ts resolveDisplayField/resolveRecordDisplayName, objectql search displayField, and plugin-approvals."
},
"imageField": {
"status": "planned",
"verifiedAt": "2026-10-01",
"evidenceScope": "cross-repo",
"evidence": "packages/spec/src/data/object.zod.ts#refuseNonPictureImageField (the AUTHORING judgement only: a name the object does not declare, or a field of any type but `image` / `avatar`, is refused at parse, so at the defineStack, `os validate` and metadata-save doors alike; a value it ACCEPTS has no reader yet); objectui @31971ff1: packages/components/src/renderers/layout/containers.tsx (the record chrome resolves the header title from `nameField` and reads no image key — zero `imageField` hits there, control `nameField` 4 hits)",
"note": "PLANNED, carrier objectui#11383 (the record chrome draws the record's picture from the object's `imageField` in the record page header), which is the seam's acceptance: flip to `live` citing that reader at the `.objectui-sha` pin that carries it. Declared by ruling A on objectstack-ai/hotcrm#1199 — one object-level declaration every record detail page reads, not a `page:header` prop, and no initials/placeholder avatar when the field is empty. ZERO consumers measured: in this repo `imageField` had 0 hits repo-wide before this key (control `nameField` hits), and objectui @31971ff1 has no object-level `imageField` read (its only `imageField` hits are the gallery view's legacy option, a different key on a different schema). Not `authorWarn`'d, on the `app.branding.logo` precedent and README rule 1: a picture pointer is display metadata, an author who sets it today loses nothing and is not misled about behaviour, and the value takes effect the moment the carrier lands with no re-authoring. The key's own describe says the renderer is pending."
},
"titleFormat": {
"status": "live",
"note": "objectui ({{record.field}} interpolation)."
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/state-counts/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them.

| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 50 | 0 | 0 | 0 | 1 | 51 |
| `object` | 50 | 0 | 0 | 0 | 2 | 52 |
Loading
Loading