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

`ObjectSchema.imageField`'s description no longer says the renderer is pending: the record page header draws the picture

Clause-②: no

The console's record page header now reads `imageField`: it draws the named `image` / `avatar` field's value in the record chip beside the title, an `avatar` round and cropped, an `image` whole, and a record whose field is empty shows no picture. The `.describe()` text that `os validate`, the JSON Schema and the reference docs carry dropped its last sentence, "Pending renderer: the record chrome does not draw it yet.", and now says the header draws the picture rather than is to draw it.

Text only: no key, schema shape, refusal, error code or status moves. An `imageField` you already authored takes effect as it is, with nothing to re-author.
2 changes: 1 addition & 1 deletion content/docs/data-modeling/objects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +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. |
| `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 draws beside the title; a record whose field is empty shows no picture. |
| `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
2 changes: 1 addition & 1 deletion content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +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. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws 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). |
| **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: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +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. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws 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). |
| **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
4 changes: 2 additions & 2 deletions content/docs/references/system/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +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. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws 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). |
| **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 @@ -628,7 +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. |
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws 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). |
| **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
7 changes: 7 additions & 0 deletions examples/app-showcase/src/data/objects/field-zoo.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export const FieldZoo = ObjectSchema.create({
pluralLabel: 'Field Zoo',
icon: 'shapes',
description: 'One field of every supported type — exhaustive data-layer coverage.',
// The record's picture: the record page header draws `f_image` beside the
// title. It must name an `image` or `avatar` field of this object; anything
// else is refused at validate/publish. Not seeded, like `showcase_task.cover`
// (see the note in `../seed/index.ts`): a stored image value is a managed
// `sys_file` id, so it comes from an upload, and a specimen without one shows
// no picture rather than a placeholder.
imageField: 'f_image',

fields: {
// ── Core text ───────────────────────────────────────────────────────
Expand Down
8 changes: 4 additions & 4 deletions packages/spec/liveness/object.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
"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",
"status": "live",
"verifiedAt": "2026-10-05",
"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."
"evidence": "packages/spec/src/data/object.zod.ts#refuseNonPictureImageField (the AUTHORING judgement: 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); objectui @9dfaca654311: packages/components/src/renderers/layout/containers.tsx#PageHeaderRenderer (the record chrome reads `objectSchema.imageField` off the record context at :2392 and hands the picture to the record chip's `icon` slot at :2449); objectui @9dfaca654311: packages/components/src/renderers/layout/containers.tsx#recordPictureUrl (:1458, resolves the served row's value of that field to the URL drawn)",
"note": "The record's picture, ruling A on objectstack-ai/hotcrm#1199: one object-level declaration every record detail page reads, not a `page:header` prop. Two halves, both live. The spec judges the pointer at authoring time (`refuseNonPictureImageField`): only a declared `image` / `avatar` field of the same object is accepted. objectui's record chrome draws it at the `.objectui-sha` pin 9dfaca654311, which carries objectui PR 11619 (merge c096f03279): `PageHeaderRenderer` reads the object's `imageField`, `recordPictureUrl` resolves the row's value (an expanded `{ url }`, a bare `sys_file` id served from the storage download path, a legacy URL string, or the first resolvable entry of a `multiple` list), and the picture goes in the record chip's `icon` slot beside the title, an `avatar` drawn round and cropped, an `image` drawn whole. An empty or unresolvable value draws nothing, never initials or a placeholder, and a field the reader may not see arrives masked, so nothing is drawn for it either. The server masks the pointer itself for such a reader (packages/metadata-core/src/object-schema-fls-references.ts). Declared in the reference app by examples/app-showcase/src/data/objects/field-zoo.object.ts (`imageField: 'f_image'`)."
},
"titleFormat": {
"status": "live",
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 | 2 | 52 |
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
12 changes: 7 additions & 5 deletions packages/spec/src/data/object.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2253,12 +2253,14 @@ const ObjectSchemaBase = strictObject(
* - A record whose field is empty shows no picture. The reader draws nothing
* in its place — no initials, no placeholder avatar.
*
* The reader is objectui's record chrome, and it does not read the key yet:
* the liveness ledger holds this row `planned` until it does, so an authored
* value is accepted, stored and served, and takes effect when the renderer
* lands with no re-authoring.
* The reader is objectui's record chrome (`PageHeaderRenderer` in
* `packages/components/src/renderers/layout/containers.tsx`): it resolves the
* record's value of the named field and draws it in the record chip's icon
* slot beside the title, an `avatar` round and cropped, an `image` whole. The
* liveness ledger row `object.imageField` cites that reader at the objectui
* pin it was read at.
*/
imageField: z.string().optional().describe('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.'),
imageField: z.string().optional().describe('The record\'s picture: names the field the record page header (record chrome) draws 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).'),
/**
* [ADR-0085] Semantic role: the object's most important fields, in priority
* order (the first entry wins wherever only one field fits, e.g. child-record
Expand Down
Loading