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
29 changes: 29 additions & 0 deletions .changeset/11276-object-grid-properties-bag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@object-ui/types': minor
---

feat(types): an authored `object-grid` takes its props in the spec's `properties` bag; the flat spelling is refused by name (objectui#11276)

**BREAKING (authoring):** an `object-grid` node that writes its props on the node itself is now refused by `safeValidateSchema`, `validateSchema` and the strict authoring face, and so by `objectui validate`. Move each prop into the node's `properties` bag:

```json
{ "type": "object-grid", "properties": { "objectName": "task", "columns": ["title", "status"], "sort": [{ "field": "title", "order": "asc" }] } }
```

Nothing changes at render time: `SchemaRenderer` hoists every `properties` key onto the node before `ObjectGrid` runs, so the bag node draws what the flat one drew, and a stored flat node, or a node built in code (`ObjectView`, `ListView`, the designers, a host mounting `<ObjectGrid schema={…}>`), keeps working with its flat keys.

**Clause-②: yes** — the accept set narrows (a flat `object-grid` node that parsed is refused) and widens (a spec-shaped bag node that was refused parses). Released as `minor` under the objectui narrowing rule, with this banner.

**Why.** `@objectstack/spec`'s `ComponentPropsMap['object-grid']` row is the published declaration of an authored `object-grid` node's props, and the spec's own strict `PageComponentSchema` refuses a prop written on the node as mis-layered (ADR-0089 D3a). The arm here was the flat mirror of the TypeScript `ObjectGridSchema`, so the two validators disagreed both ways: the objectstack showcase's work queues, written in the bag, were refused (the flat mirror found no `objectName` on the node), and the flat node `os validate` refuses was accepted.

**What changed, in observable terms.**

- `@object-ui/types/zod` exports `ObjectGridBlockSchema`, built the way `ObjectGanttBlockSchema` is: `BaseSchema`, the `object-grid` literal, the node-level `responsiveStyles`, the node's `dataSource` binding (the spec's `ElementDataSourceSchema`), and `properties`, which is the spec row by reference, so the row's members, value types and strictness judge the bag.
- Each member of the row written flat on the node is refused on both faces, with a message that names its bag member (`Did you mean \`objectName\` → \`properties.objectName\`?`). The refusal set is read off the row. `label` stays on the node too, because the spec's page component declares a node-level `label`; a flat `defaultSort` keeps the row's own retirement message. `operators`, `rowSpecActions`, `bulkSpecActions`, `name`, `placeholder` and `showFilters` keep their retirement messages, `onNavigate` stays a refused runtime slot, and `body` / `children` stay refused (objectui#9256).
- The record-source rule stays, read in the bag: the node needs `properties.objectName`, or a `dataSource` binding naming the object. The spec has no such rule.
- `ObjectGridBlockSchema` reaches `AnyComponentSchema` through `ObjectQLPublicBlockComponentSchema`. `ObjectGridSchema` leaves `ObjectQLComponentSchema`.
- Inside the bag the spec row decides, and it differs from the flat mirror. `filter` and `defaultFilters` take the `ViewFilterRule` array only (`[{ field, operator, value }]`), so the tuple form, the record form and a record-form `defaultFilters` are refused there. The row types `columns` entries, `selection`, `exportOptions`, `operations`, `navigation`, `grouping` and a few display keys as unknown, so their shapes are not judged inside the bag: notably a bare-array `exportOptions`, which the flat mirror refuses by name (objectui#7762), is not refused there. The row declares keys the flat mirror does not (`rowHeight`, `grouping`, `aggregations`, `conditionalFormatting`, `rowColor`, `bulkActionDefs`, `navigation`, `singleClickEdit`, `resizableColumns`, `reorderableColumns`, `operations`), so they parse in the bag. It does not declare `emptyState` or `keyboardNavigation`, so the bag refuses them, and on the node the strict face refuses them as unknown keys: `emptyState` is not authorable in a document until the spec's row declares it.

**The same release's changesets** that describe what the validators say about an `object-grid` written flat (`.changeset/7762-object-grid-export-options-bare-array-refusal.md`, `.changeset/9739-object-grid-operators-tombstone.md`, `.changeset/11068-grid-declared-keys.md`, `.changeset/11068-object-grid-show-filters.md`, `.changeset/11117-datasource-objectname-waiver.md`, `.changeset/10872-flat-arm-responsive-styles.md` and others) describe the flat mirror, which still behaves as they say. On an authored node the props now live in the bag, where the spec row judges them as listed above; the retirements, the record-source rule and `responsiveStyles` hold there as they say.

**What did not move.** The TypeScript `ObjectGridSchema` and its zod mirror `ObjectGridSchema` stay published and unchanged in shape. They are the node as `ObjectGrid` reads it after the hoist, and as code composes it, and the `object-view` `table` slot is still built from the mirror.
7 changes: 7 additions & 0 deletions .changeset/11276-plugin-grid-properties-bag-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@object-ui/plugin-grid': patch
---

docs(plugin-grid): authored `object-grid` examples write their props in the `properties` bag (objectui#11276)

The README's authored `object-grid` examples now write `{ "type": "object-grid", "properties": { … } }`, the spelling `@objectstack/spec`'s `ComponentPropsMap['object-grid']` row declares and `objectui validate` now requires, each bag checked with `satisfies ObjectGridProps` (the spec's row type). `emptyState` is described as what it is today: a key `ObjectGrid` reads that the spec's row does not declare, so not authorable in a document. The examples that mount `ObjectGrid` directly keep the flat `schema` prop: a component mounted without `SchemaRenderer` receives the node as it reads it, and nothing hoists a bag there. Under `src/`, a new render pin holds that the bag and a stored flat node draw the same grid, and the CRUD-guide render pin reads the guide's bag. No runtime change.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,19 @@ published is not an error: the expression is returned as its own source text.
```json
{
"type": "object-grid",
"objectName": "user",
"title": "Users",
"columns": [
{ "field": "name", "label": "Name", "sortable": true },
{ "field": "email", "label": "Email" },
{ "field": "role", "label": "Role" },
{ "field": "status", "label": "Status" },
{ "field": "created_at", "label": "Joined" }
],
"showSearch": true,
"operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }
"properties": {
"objectName": "user",
"title": "Users",
"columns": [
{ "field": "name", "label": "Name", "sortable": true },
{ "field": "email", "label": "Email" },
{ "field": "role", "label": "Role" },
{ "field": "status", "label": "Status" },
{ "field": "created_at", "label": "Joined" }
],
"showSearch": true,
"operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }
}
}
```

Expand Down
75 changes: 40 additions & 35 deletions content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,37 +730,36 @@ These schemas integrate with [ObjectStack](https://objectstack.ai) for automatic

A data grid that auto-fetches from an ObjectQL object definition. Includes search, filters, pagination, grouping, and inline editing.

An authored `object-grid` node takes its props in its `properties` bag, whose members are `@objectstack/spec`'s `ComponentPropsMap['object-grid']` row. `objectui validate` judges the bag against that row and refuses a prop written flat on the node by name, naming its bag member, as the spec's own page component does (objectui#11276). `SchemaRenderer` hoists the bag onto the node before `ObjectGrid` runs, so `ObjectGridSchema` is the node as the renderer reads it, and the table below lists its members. Base props such as `description` and `className` stay on the node, beside the bag.

```json
{
"type": "object-grid",
"objectName": "Contact",
"title": "All Contacts",
"description": "Manage your contacts",
"searchableFields": ["name", "email", "company"],
"resizable": true,
"columns": [
{ "field": "name" },
{ "field": "email" },
{ "field": "company" },
{ "field": "phone" },
{ "field": "status", "label": "Status", "sortable": true }
],
"sort": [{ "field": "name", "order": "asc" }],
"operations": {
"create": true,
"update": true,
"delete": true,
"export": true
},
"rowActions": ["edit", "delete"],
"pagination": {
"pageSize": 25,
"pageSizeOptions": [10, 25, 50, 100]
},
"emptyState": {
"title": "No contacts yet",
"message": "Contacts you add appear here.",
"icon": "users"
"properties": {
"objectName": "Contact",
"title": "All Contacts",
"searchableFields": ["name", "email", "company"],
"resizable": true,
"columns": [
{ "field": "name" },
{ "field": "email" },
{ "field": "company" },
{ "field": "phone" },
{ "field": "status", "label": "Status", "sortable": true }
],
"sort": [{ "field": "name", "order": "asc" }],
"operations": {
"create": true,
"update": true,
"delete": true,
"export": true
},
"rowActions": ["edit", "delete"],
"pagination": {
"pageSize": 25,
"pageSizeOptions": [10, 25, 50, 100]
}
}
}
```
Expand All @@ -770,7 +769,9 @@ also author `showFilters` (now retired on this node, below), `striped` and
`operations.read`, which nothing reads, and `selection: { enabled, mode }` and
`pagination.enabled`, which the validator refuses: selection is spelled `selection: { type: 'multiple' }` (`'none'`,
`'single'` or `'multiple'`), and `pagination` declares no on switch — its
presence enables paging.
presence enables paging. It also authored `emptyState`, which the grid reads but
the spec's row does not declare, so it is not authorable in a document today (see
its row below).

| Property | Type | Description |
|----------|------|-------------|
Expand All @@ -789,7 +790,7 @@ presence enables paging.
| `grouping` | `GroupingConfig` | Row grouping configuration. **Server-side**: the set of groups, every group count and every per-group aggregation come from the group header query (`dataSource.queryGroupHeaders`), and each group's rows are paged by the server. Rows handed in whole are grouped in the browser (exact); over a data source with no header query, a grid that fetches its own rows refuses grouping with an error naming `queryGroupHeaders`. |
| `frozenColumns` | `number` | Number of columns frozen on scroll. |
| `navigation` | `ViewNavigationConfig` | SPA navigation configuration. |
| `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). 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). |
| `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). 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). **Not authorable in a document today:** the spec's `object-grid` row does not declare it, so `objectui validate` refuses it in the `properties` bag, and the strict face refuses it on the node; a host mounting `<ObjectGrid schema={…}>` or composing the node in code can set it. |

> **`name`, `placeholder`, `rowSpecActions` and `bulkSpecActions` are retired on
> this node (objectui#11068).** Nothing ever read them: `rowSpecActions` /
Expand Down Expand Up @@ -1282,8 +1283,10 @@ A toggle control that switches between different view types (list, grid, kanban,
"icon": "List",
"schema": {
"type": "object-grid",
"objectName": "Contact",
"columns": ["name", "email", "phone"]
"properties": {
"objectName": "Contact",
"columns": ["name", "email", "phone"]
}
}
},
{
Expand Down Expand Up @@ -1366,10 +1369,12 @@ Schemas are designed to compose. Nest any `SchemaNode` inside another to build c
},
{
"type": "object-grid",
"objectName": "Lead",
"title": "All Leads",
"showSearch": true,
"columns": ["name", "company", "status", "value"]
"properties": {
"objectName": "Lead",
"title": "All Leads",
"showSearch": true,
"columns": ["name", "company", "status", "value"]
}
}
]
}
Expand Down
21 changes: 12 additions & 9 deletions content/docs/fields/grid.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,20 +236,23 @@ For advanced grid features, use the grid plugin (`@object-ui/plugin-grid`):
{ type: 'grid', name: 'items', columns: [...] }

// Advanced grid with full features (requires plugin)
// Keys sit on the node — a `props` envelope is never read by the renderer
// The block's props go in its `properties` bag (the spec's
// `ComponentPropsMap['object-grid']` row) — a `props` envelope is never read
{
type: 'object-grid', // the registered type name — there is no `plugin:grid`
bind: 'items', // resolves the array from the surrounding data scope
columns: [ // spec `ListColumn[]` — each entry is keyed by `field`
{ field: 'product', label: 'Product' },
{ field: 'quantity', label: 'Quantity', sortable: false }
],
editable: true, // Plugin feature: inline cell editing
pagination: { pageSize: 20 } // Plugin feature: pagination
bind: 'items', // node-level: resolves the array from the surrounding data scope
properties: {
columns: [ // spec `ListColumn[]` — each entry is keyed by `field`
{ field: 'product', label: 'Product' },
{ field: 'quantity', label: 'Quantity', sortable: false }
],
editable: true, // Plugin feature: inline cell editing
pagination: { pageSize: 20 } // Plugin feature: pagination
}
}
```

**Note**: `editable` and `pagination` are read off the node by `@object-ui/plugin-grid`, not by the basic grid field. Sorting and search are **not** node keys: column sorting is on by default and is turned off per column (`{ field, sortable: false }`), and there is no node-level `filterable` — a grid that fetches its own rows declares its query with `objectName` plus `filter` / `sort`.
**Note**: `editable` and `pagination` are read by `@object-ui/plugin-grid` (after `SchemaRenderer` hoists the bag onto the node), not by the basic grid field. Sorting and search are **not** node keys: column sorting is on by default and is turned off per column (`{ field, sortable: false }`), and there is no node-level `filterable` — a grid that fetches its own rows declares its query with `objectName` plus `filter` / `sort`.

## Use Cases

Expand Down
12 changes: 7 additions & 5 deletions content/docs/guide/building-crud-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ function App() {
<SchemaRenderer
schema={{
type: 'object-grid',
objectName: 'task',
properties: { objectName: 'task' },
}}
/>
</div>
Expand All @@ -253,7 +253,9 @@ a grid that draws its header and nothing else:
`dataSource` written on the block itself reaches that one block and never
becomes context for anything under it.
- **The object is named by `objectName`.** That is the key each of these blocks
declares as required; a differently-spelled key (`object`) is not read, so the
declares as required — inside the `properties` bag on `object-grid` and
`object-form`, as the spec's page component declares their props, and on the
node on `detail-view`; a differently-spelled key (`object`) is not read, so the
block has no object to query.

Neither mistake used to say anything — the block simply rendered empty. Both now
Expand All @@ -278,7 +280,7 @@ carries a data source of its own:
<!-- doc-snippet: fragment — JSX to place inside the Step 5 App component; showForm, editId and their setters are the state declared in the block above -->
```tsx
<SchemaRenderer
schema={{ type: 'object-grid', objectName: 'task' }}
schema={{ type: 'object-grid', properties: { objectName: 'task' } }}
onRowClick={(row: any) => { setEditId(row.id); setShowForm(true); }}
/>

Expand Down Expand Up @@ -354,8 +356,8 @@ so this block does not offer that fallback.
in the toolbar — on by default — and typing there drives
`DataSource.find()`'s `$search` parameter directly; there is no separate
query-param key to author. Add `searchableFields: ['title', 'description']`
to the schema to narrow which fields the server matches; leave it out and the
server decides.
to the grid's `properties` bag to narrow which fields the server matches; leave
it out and the server decides.

## Step 8: Add a Detail View

Expand Down
2 changes: 1 addition & 1 deletion content/docs/guide/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ A `page` node has no action row of its own. Buttons are NODES, and they go in `c
},
{
"type": "object-grid",
"objectName": "products"
"properties": { "objectName": "products" }
}
]
}
Expand Down
Loading
Loading