Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit f20667b

Browse files
committed
docs(empty-state): document the declared empty-state link and the list page's own emptyState; changeset
Claude-Session: https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5a98f3 commit f20667b

5 files changed

Lines changed: 97 additions & 23 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/components': minor
3+
'@object-ui/plugin-list': minor
4+
'@object-ui/plugin-grid': minor
5+
'@object-ui/app-shell': minor
6+
---
7+
8+
An empty list draws its declared link, and a list page's own empty state wins over its source view's (objectui#12270).
9+
10+
`@objectstack/spec` 18 gives the empty state (`EmptyStateSchema`) one optional `link: { label, url }`, and a list page's `interfaceConfig` an `emptyState` of its own with that shape. Both parsed and neither was drawn:
11+
12+
- a list view's empty state (`ListView`, on its `grid` and `gallery` visualizations) and the `object-grid` block's authored empty state now draw the link, beside the add-record action where the view offers one;
13+
- a list page's own `interfaceConfig.emptyState` now wins, whole, over the one its source view declares, on every visualization; the source view's is the fallback when the page declares none.
14+
15+
The link is followed as a `type: 'url'` action through the action runner, so it opens what such an action opens: an in-app path in place, an `http(s)://` address as the runner opens an external target, and `mailto:` in place. The runner refuses anything else at the click and reports it. The link's `label` takes a string or an inline locale map.
16+
17+
`@object-ui/components` exports `EmptyStateLink`, the one component every empty state draws the link with.
18+
19+
**Clause-②: yes (widening)** — the renderers start honouring two keys the spec declares (`emptyState.link`, `interfaceConfig.emptyState`), so what they draw widens. No existing key changes.

‎content/docs/api/schema-reference.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ the spec's row did not declare until objectstack#20694; it is authorable in the
827827
| `frozenColumns` | `number` | Number of columns frozen on scroll. |
828828
| `conditionalFormatting` | `ConditionalFormattingRule[]` | Row styling rules, each `{ condition, style }` — a CEL `condition` over the row's `record.*` and a CSS `style` map, the rule a list view declares; the first matching rule styles the row. The native `{ field, operator, value }` rule, its `expression`, and a colour written beside `condition` (rather than inside `style`) are retired (objectui#11533): `@object-ui/types` refuses them by name on `ObjectGridSchema`, the `object-view` `table` slot and `list-view`. `{ field: 'priority', operator: 'equals', value: 'high', backgroundColor: '#fee2e2' }` is `{ condition: "record.priority == 'high'", style: { backgroundColor: '#fee2e2' } }`. In an authored node's `properties` bag the member is `@objectstack/spec`'s row member, judged by the installed spec. A grid stored with a retired rule still paints it. |
829829
| `navigation` | `ViewNavigationConfig` | SPA navigation configuration. |
830-
| `emptyState` | `EmptyState` — `{ title?, message?, icon? }` | Drawn in place of an empty table: a Lucide `icon`, a `title` (default: the table's "No results found") and a `message` (default: none). `title` and `message` are `string \| I18nLabel`, each resolved against the display locale; a map with no usable entry keeps that member's default. Not drawn when a term in the grid's own server-side search box emptied it — the table and its search box stay (objectui#11068). Authored in the `properties` bag: the spec's row declares it as the list view's own `EmptyStateSchema` (objectstack#20694, objectui#11227). |
830+
| `emptyState` | `EmptyState` — `{ title?, message?, icon?, link? }` | Drawn in place of an empty table: a Lucide `icon`, a `title` (default: the table's "No results found"), a `message` (default: none) and one `link: { label, url }` the user follows (default: none), opened as a `type: 'url'` action opens it, through the action runner (objectui#12270). `title`, `message` and the link's `label` are `string \| I18nLabel`, each resolved against the display locale; a map with no usable entry keeps that member's default. Not drawn when a term in the grid's own server-side search box emptied it — the table and its search box stay (objectui#11068). Authored in the `properties` bag: the spec's row declares it as the list view's own `EmptyStateSchema` (objectstack#20694, objectui#11227). |
831831

832832
> **`name`, `placeholder`, `rowSpecActions` and `bulkSpecActions` are retired on
833833
> this node (objectui#11068).** Nothing ever read them: `rowSpecActions` /

‎content/docs/plugins/plugin-grid.mdx‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ below for why this plugin deliberately does not claim it.
9494
| `columns` | `string[] \| ListColumn[]` | Field names or column objects — see below. |
9595
| `label` | `I18nLabel` | Table caption and export file title. |
9696
| `description` | `I18nLabel` | One line of help text above the grid — see `description` and `emptyState` below. |
97-
| `emptyState` | `EmptyState` (`@objectstack/spec/ui`) | `{ title?, message?, icon? }`, drawn in place of an empty table — see below. |
97+
| `emptyState` | `EmptyState` (`@objectstack/spec/ui`) | `{ title?, message?, icon?, link? }`, drawn in place of an empty table — see below. |
9898
| `filter` | `ViewFilterRule[]` | Baked into the query, lowered to `$filter`. |
9999
| `sort` | `[{ field, order }]` | Initial order; a header click replaces it. |
100100
| `pagination` | `PaginationConfig` | `{ pageSize?, pageSizeOptions? }` — **strict**, and its presence is what enables paging. |
@@ -123,15 +123,21 @@ row declares both (objectstack#20694), so both are bag members and both are in
123123

124124
- `description` — one line of help text drawn above the grid. A string, or an
125125
inline locale map resolved against the display locale the way `label` is.
126-
- `emptyState: { title?, message?, icon? }` — the list view's own empty-state
127-
shape (`EmptyState`), drawn **in place of** an empty table: a Lucide `icon`, a
128-
`title` (default: the table's own "No results found") and a `message`
129-
(default: none). `title` and `message` each take a string or an inline locale
130-
map, resolved against the display locale; a map with no usable entry keeps
131-
that member's default. It is not drawn when a term typed into the grid's own
132-
server-side search box is what emptied it — the table and its search box stay,
133-
so the term can be cleared. Leave the key out and an empty grid draws the
134-
table's own empty row, as before.
126+
- `emptyState: { title?, message?, icon?, link? }` — the list view's own
127+
empty-state shape (`EmptyState`), drawn **in place of** an empty table: a
128+
Lucide `icon`, a `title` (default: the table's own "No results found"), a
129+
`message` (default: none) and one `link: { label, url }` the user follows
130+
(default: none; objectui#12270). `title`, `message` and the link's `label`
131+
each take a string or an inline locale map, resolved against the display
132+
locale; a map with no usable entry keeps that member's default, and a link
133+
whose label resolves to no text is not drawn. The link is followed as a
134+
`type: 'url'` action through the action runner, so it opens what such an
135+
action opens: an in-app path (`/…` or `./…`) in place, an `http(s)://`
136+
address as the runner opens an external target, and `mailto:` in place; the
137+
runner refuses anything else at the click and reports it. It is not drawn when
138+
a term typed into the grid's own server-side search box is what emptied it —
139+
the table and its search box stay, so the term can be cleared. Leave the key
140+
out and an empty grid draws the table's own empty row, as before.
135141

136142
```json
137143
{
@@ -142,7 +148,8 @@ row declares both (objectstack#20694), so both are bag members and both are in
142148
"emptyState": {
143149
"title": { "en": "No contacts yet", "fr": "Aucun contact" },
144150
"message": "Add one to get started",
145-
"icon": "users"
151+
"icon": "users",
152+
"link": { "label": "How contacts work", "url": "/docs/contacts" }
146153
}
147154
}
148155
}

‎packages/plugin-grid/README.md‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ const grid: ObjectGridBlockNode = {
200200
| `columns` | `string[] \| ListColumn[]` | Field names or column objects — see below. |
201201
| `label` | `I18nLabel` | Table caption and export title. |
202202
| `description` | `I18nLabel` | One line of help text above the grid — see `description` and `emptyState` below. |
203-
| `emptyState` | `EmptyState` (`@objectstack/spec/ui`) | `{ title?, message?, icon? }`, drawn in place of an empty table — see below. |
203+
| `emptyState` | `EmptyState` (`@objectstack/spec/ui`) | `{ title?, message?, icon?, link? }`, drawn in place of an empty table — see below. |
204204
| `filter` | `ViewFilterRule[]` | Lowered to `$filter`. |
205205
| `sort` | `[{ field, order }]` | Initial order; a header click replaces it. |
206206
| `pagination` | `PaginationConfig` | `{ pageSize?, pageSizeOptions? }` — **strict**, and its presence is what enables paging. |
@@ -229,15 +229,21 @@ row declares both (objectstack#20694), so both are bag members and both are in
229229

230230
- `description` — one line of help text drawn above the grid. A string, or an
231231
inline locale map resolved against the display locale the way `label` is.
232-
- `emptyState: { title?, message?, icon? }` — the list view's own empty-state
233-
shape (`EmptyState`), drawn **in place of** an empty table: a Lucide `icon`, a
234-
`title` (default: the table's own "No results found") and a `message`
235-
(default: none). `title` and `message` each take a string or an inline locale
236-
map, resolved against the display locale; a map with no usable entry keeps
237-
that member's default. It is not drawn when a term typed into the grid's own
238-
server-side search box is what emptied it — the table and its search box stay,
239-
so the term can be cleared. Leave the key out and an empty grid draws the
240-
table's own empty row, as before.
232+
- `emptyState: { title?, message?, icon?, link? }` — the list view's own
233+
empty-state shape (`EmptyState`), drawn **in place of** an empty table: a
234+
Lucide `icon`, a `title` (default: the table's own "No results found"), a
235+
`message` (default: none) and one `link: { label, url }` the user follows
236+
(default: none; objectui#12270). `title`, `message` and the link's `label`
237+
each take a string or an inline locale map, resolved against the display
238+
locale; a map with no usable entry keeps that member's default, and a link
239+
whose label resolves to no text is not drawn. The link is followed as a
240+
`type: 'url'` action through the action runner, so it opens what such an
241+
action opens: an in-app path (`/…` or `./…`) in place, an `http(s)://`
242+
address as the runner opens an external target, and `mailto:` in place; the
243+
runner refuses anything else at the click and reports it. It is not drawn when
244+
a term typed into the grid's own server-side search box is what emptied it —
245+
the table and its search box stay, so the term can be cleared. Leave the key
246+
out and an empty grid draws the table's own empty row, as before.
241247

242248
```json
243249
{
@@ -248,7 +254,8 @@ row declares both (objectstack#20694), so both are bag members and both are in
248254
"emptyState": {
249255
"title": { "en": "No contacts yet", "fr": "Aucun contact" },
250256
"message": "Add one to get started",
251-
"icon": "users"
257+
"icon": "users",
258+
"link": { "label": "How contacts work", "url": "/docs/contacts" }
252259
}
253260
}
254261
}

‎packages/plugin-list/README.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,47 @@ const viewTypes: Record<NonNullable<ListViewSchema['viewType']>, string> = {
246246
export { view, richColumns, viewTypes };
247247
```
248248

249+
## Empty state
250+
251+
When the `grid` or `gallery` visualization has no record to show, the view
252+
draws its `emptyState` (the spec's `EmptyState`): a Lucide `icon`, a `title`, a
253+
`message` and one `link: { label, url }` the user follows, such as the how-to
254+
page that explains what belongs in the list (objectui#12270). A member left out
255+
keeps the localized default copy; `title`, `message` and the link's `label`
256+
each take a string or an inline locale map, resolved against the display
257+
locale.
258+
259+
The link is followed as a `type: 'url'` action through the action runner, so it
260+
opens what such an action opens: an in-app path (`/…` or `./…`) in place, an
261+
`http(s)://` address as the runner opens an external target, and `mailto:` in
262+
place. The runner refuses anything else at the click and reports it. The link
263+
is drawn beside the add-record action when the view's `addRecord` block offers
264+
one; the two are separate, and either, both or neither may be drawn.
265+
266+
```typescript
267+
import type { ListViewSchema } from '@object-ui/types';
268+
269+
const emptyTasks: ListViewSchema = {
270+
type: 'list-view',
271+
objectName: 'tasks',
272+
viewType: 'grid',
273+
columns: ['title'],
274+
emptyState: {
275+
title: 'No open tasks',
276+
message: 'Tasks assigned to you appear here.',
277+
icon: 'inbox',
278+
link: { label: 'How tasks are assigned', url: '/docs/tasks' },
279+
},
280+
};
281+
282+
export { emptyTasks };
283+
```
284+
285+
A list page (`interfaceConfig` of a `type: 'list'` page) may declare its own
286+
`emptyState` of the same shape. The page's own block wins, whole, over the one
287+
its source view declares, on every visualization; the source view's is the
288+
fallback when the page declares none.
289+
249290
## Page size and fetch batch
250291

251292
A list view asks the server for one window of records. When

0 commit comments

Comments
 (0)