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
8 changes: 8 additions & 0 deletions skills/objectstack-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,14 @@ blocks for data. Real component props/callbacks flow through — e.g. `<ObjectFo
`objectName` / `mode` / `recordId` / `onSuccess` / `onCancel`; `<ListView>` honors
`objectName` / `fields` / `onRowClick` / `navigation`.

> **Do not guess props — read the contract.** Each injected block's full prop set
> (name, type, `data`/`controlled`/`callback` kind, required, description) is the
> **[React-tier component contract](./references/react-blocks.md)**, generated from
> [`contracts/react-blocks.contract.json`](./contracts/react-blocks.contract.json).
> It is the authoritative answer to "what props does `<ObjectForm>`/`<ListView>`/…
> take?" — author against it, not from memory. Regenerate after editing the JSON
> with `node skills/objectstack-ui/contracts/build-ref.mjs`.

Master/detail (click a row → edit it → save refreshes the list):

```tsx
Expand Down
54 changes: 54 additions & 0 deletions skills/objectstack-ui/contracts/build-ref.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
// Generates the AI-facing markdown reference from react-blocks.contract.json.
// Run: node skills/objectstack-ui/contracts/build-ref.mjs
import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const here = dirname(fileURLToPath(import.meta.url));
const contract = JSON.parse(readFileSync(join(here, 'react-blocks.contract.json'), 'utf8'));
const out = join(here, '..', 'references', 'react-blocks.md');

const esc = (s) => String(s).replace(/\|/g, '\\|');

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.
const KIND_ORDER = { data: 0, controlled: 1, callback: 2, layout: 3 };

const lines = [];
lines.push('---');
lines.push('title: React-tier component contract');
lines.push('description: The props each injected component accepts in kind:\'react\' page source (ADR-0081). GENERATED from contracts/react-blocks.contract.json — do not edit by hand.');
lines.push('---');
lines.push('');
lines.push('{/* GENERATED by contracts/build-ref.mjs from react-blocks.contract.json — do not edit. */}');
lines.push('');
lines.push(`# React-tier component contract (${contract.adr})`);
lines.push('');
lines.push(contract.note);
lines.push('');
lines.push('Prop **kind**: `data` = static/declarative (mirrors the registry inputs) · `controlled` = drive from React state · `callback` = a React function the component calls.');
lines.push('');

for (const b of contract.blocks) {
lines.push(`## \`<${b.tag}>\` — \`${b.schemaType}\``);
lines.push('');
lines.push(b.summary);
lines.push('');
lines.push('| prop | type | kind | required | description |');
lines.push('|------|------|------|:--------:|-------------|');
const props = [...b.props].sort((a, z) => (KIND_ORDER[a.kind] - KIND_ORDER[z.kind]));
for (const p of props) {
lines.push(`| \`${p.name}\` | \`${esc(p.type)}\` | ${p.kind} | ${p.required ? '✓' : ''} | ${esc(p.description)} |`);
}
lines.push('');
}

if (contract.alsoAvailable) {
lines.push('## Also available');
lines.push('');
lines.push(contract.alsoAvailable.note);
lines.push('');
lines.push('Injected scope (closure variables, reference directly — not props): ' + contract.alsoAvailable.scope.map((s) => `\`${s}\``).join(', ') + '.');
lines.push('');
}

writeFileSync(out, lines.join('\n'));
console.log('✅ wrote', out, `(${contract.blocks.length} blocks)`);
90 changes: 90 additions & 0 deletions skills/objectstack-ui/contracts/react-blocks.contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"version": 1,
"adr": "ADR-0081",
"note": "Curated contract for the components injected into kind:'react' page source. Reference these by their PascalCase `tag` in JSX. `data` props mirror the objectui registry `inputs` (the same surface the html tier validates); `controlled` props are React-driven state (recordId, mode, filters…); `callback` props are React functions the component invokes. Layout is plain HTML + Tailwind — these blocks are for DATA. Filters use the ObjectQL array form: [field, op, value], e.g. ['status','=','active'] (ops: =, !=, >, <, contains, in). Live data: `const adapter = useAdapter(); await adapter.find(obj, {$filter, $top}); .findOne; .create; .update`.",
"blocks": [
{
"tag": "ObjectForm",
"schemaType": "object-form",
"summary": "Server-connected create / edit / view form for one object. Auto-builds fields from the object schema. Built-in drawer/modal variants via formType.",
"props": [
{ "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object whose record this form edits." },
{ "name": "mode", "type": "'create' | 'edit' | 'view'", "kind": "controlled", "description": "Create a new record, edit/view an existing one." },
{ "name": "recordId", "type": "string | number", "kind": "controlled", "description": "Which record to load (edit/view mode). Drive from React state for master/detail." },
{ "name": "fields", "type": "string[]", "kind": "data", "description": "Limit/order the fields shown (defaults to the object's form fields)." },
{ "name": "initialValues", "type": "Record<string, any>", "kind": "data", "description": "Prefill values in create mode." },
{ "name": "formType", "type": "'simple' | 'tabbed' | 'wizard' | 'split' | 'drawer' | 'modal'", "kind": "data", "description": "Form presentation; 'drawer'/'modal' render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize)." },
{ "name": "onSuccess", "type": "(record) => void", "kind": "callback", "description": "Called after a successful create/update with the saved record. Typical: close a panel + bump a reload key." },
{ "name": "onError", "type": "(error: Error) => void", "kind": "callback", "description": "Called when the save fails." },
{ "name": "onCancel", "type": "() => void", "kind": "callback", "description": "Called when the user cancels." },
{ "name": "submitHandler", "type": "(values) => any | Promise<any>", "kind": "callback", "description": "Custom persistence instead of the default create/update (the form just validates and hands over values)." },
{ "name": "navigateOnSuccess", "type": "string", "kind": "data", "description": "URL to navigate to after save (supports {id})." }
]
},
{
"tag": "ListView",
"schemaType": "list-view",
"summary": "Server-connected object table with toolbar (search/filter/sort), and switchable views (grid/kanban/gallery/calendar/timeline/gantt/map). The primary object-list block.",
"props": [
{ "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to list." },
{ "name": "fields", "type": "string[]", "kind": "data", "description": "Columns to show." },
{ "name": "filters", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter, e.g. ['status','=','active'] or ['and', [...], [...]]. Drive from React state for tabbed/searched lists." },
{ "name": "sort", "type": "SortArray", "kind": "data", "description": "Sort spec." },
{ "name": "viewType", "type": "'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map'", "kind": "data", "description": "Which view to render (default grid). This is how you get a kanban/calendar/gantt of the object." },
{ "name": "navigation", "type": "{ mode: 'page' | 'drawer' | 'modal' | 'split' | 'none' }", "kind": "data", "description": "What a row click does. Set { mode: 'none' } when you handle clicks yourself via onRowClick." },
{ "name": "onRowClick", "type": "(record) => void", "kind": "callback", "description": "Called with the clicked row's record. The hook for master/detail." },
{ "name": "onNavigate", "type": "(recordId, action) => void", "kind": "callback", "description": "Called for page-level navigation (action: 'view' | 'edit')." },
{ "name": "searchableFields", "type": "string[]", "kind": "data", "description": "Fields the toolbar search matches." }
]
},
{
"tag": "ObjectGrid",
"schemaType": "object-grid",
"summary": "Lower-level data grid for an object (columns + filters). Prefer ListView for a full toolbar; use ObjectGrid for an embedded table.",
"props": [
{ "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to render." },
{ "name": "columns", "type": "ColumnDef[]", "kind": "data", "description": "Column definitions." },
{ "name": "filters", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter." }
]
},
{
"tag": "ObjectMasterDetailForm",
"schemaType": "object-master-detail-form",
"summary": "Parent record + child line-items, edited and saved atomically (e.g. Invoice + line items).",
"props": [
{ "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "Parent object." },
{ "name": "mode", "type": "'create' | 'edit'", "kind": "controlled", "description": "Create or edit the parent + children." },
{ "name": "recordId", "type": "string | number", "kind": "controlled", "description": "Parent record to edit." },
{ "name": "childObject", "type": "string", "kind": "data", "required": true, "description": "Child (line-item) object." },
{ "name": "relationshipField", "type": "string", "kind": "data", "required": true, "description": "Field on the child that points to the parent." },
{ "name": "columns", "type": "ColumnDef[]", "kind": "data", "required": true, "description": "Child line-item grid columns." },
{ "name": "totalField", "type": "string", "kind": "data", "description": "Parent field that rolls up the child amounts." },
{ "name": "onSuccess", "type": "(record) => void", "kind": "callback", "description": "After the atomic save." },
{ "name": "onCancel", "type": "() => void", "kind": "callback", "description": "On cancel." }
]
},
{
"tag": "ObjectChart",
"schemaType": "object-chart",
"summary": "Chart over an object's aggregated data (bar/line/area/pie/donut/…).",
"props": [
{ "name": "objectName", "type": "string", "kind": "data", "required": true, "description": "The object to aggregate." },
{ "name": "chartType", "type": "'bar' | 'line' | 'area' | 'pie' | 'donut' | 'scatter' | 'radar' | '…'", "kind": "data", "description": "Chart variant." },
{ "name": "aggregate", "type": "{ field, function, groupBy }", "kind": "data", "description": "Aggregation: function (sum/avg/count) over field, grouped by groupBy." },
{ "name": "filter", "type": "FilterArray", "kind": "controlled", "description": "ObjectQL filter to scope the data." }
]
},
{
"tag": "Block",
"schemaType": "(any)",
"summary": "Escape hatch — render any registered component by type when it isn't injected as a named block.",
"props": [
{ "name": "type", "type": "string", "kind": "data", "required": true, "description": "The registered component type, e.g. 'object-kanban'." }
]
}
],
"alsoAvailable": {
"note": "Other curated public blocks are reachable too. Kanban/calendar/gantt/timeline/map of an object = <ListView viewType=\"kanban\" …>. Metrics/dashboards/pivots and record:* panels can be rendered via <Block type=\"object-metric\" …> / <Block type=\"dashboard\" …> until they are added to this contract.",
"scope": ["React", "useAdapter", "data", "variables", "page"]
}
}
97 changes: 97 additions & 0 deletions skills/objectstack-ui/references/react-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
title: React-tier component contract
description: The props each injected component accepts in kind:'react' page source (ADR-0081). GENERATED from contracts/react-blocks.contract.json — do not edit by hand.
---

{/* GENERATED by contracts/build-ref.mjs from react-blocks.contract.json — do not edit. */}

# React-tier component contract (ADR-0081)

Curated contract for the components injected into kind:'react' page source. Reference these by their PascalCase `tag` in JSX. `data` props mirror the objectui registry `inputs` (the same surface the html tier validates); `controlled` props are React-driven state (recordId, mode, filters…); `callback` props are React functions the component invokes. Layout is plain HTML + Tailwind — these blocks are for DATA. Filters use the ObjectQL array form: [field, op, value], e.g. ['status','=','active'] (ops: =, !=, >, <, contains, in). Live data: `const adapter = useAdapter(); await adapter.find(obj, {$filter, $top}); .findOne; .create; .update`.

Prop **kind**: `data` = static/declarative (mirrors the registry inputs) · `controlled` = drive from React state · `callback` = a React function the component calls.

## `<ObjectForm>` — `object-form`

Server-connected create / edit / view form for one object. Auto-builds fields from the object schema. Built-in drawer/modal variants via formType.

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `objectName` | `string` | data | ✓ | The object whose record this form edits. |
| `fields` | `string[]` | data | | Limit/order the fields shown (defaults to the object's form fields). |
| `initialValues` | `Record<string, any>` | data | | Prefill values in create mode. |
| `formType` | `'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'` | data | | Form presentation; 'drawer'/'modal' render the form in a built-in overlay (use drawerSide/drawerWidth/modalSize). |
| `navigateOnSuccess` | `string` | data | | URL to navigate to after save (supports {id}). |
| `mode` | `'create' \| 'edit' \| 'view'` | controlled | | Create a new record, edit/view an existing one. |
| `recordId` | `string \| number` | controlled | | Which record to load (edit/view mode). Drive from React state for master/detail. |
| `onSuccess` | `(record) => void` | callback | | Called after a successful create/update with the saved record. Typical: close a panel + bump a reload key. |
| `onError` | `(error: Error) => void` | callback | | Called when the save fails. |
| `onCancel` | `() => void` | callback | | Called when the user cancels. |
| `submitHandler` | `(values) => any \| Promise<any>` | callback | | Custom persistence instead of the default create/update (the form just validates and hands over values). |

## `<ListView>` — `list-view`

Server-connected object table with toolbar (search/filter/sort), and switchable views (grid/kanban/gallery/calendar/timeline/gantt/map). The primary object-list block.

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `objectName` | `string` | data | ✓ | The object to list. |
| `fields` | `string[]` | data | | Columns to show. |
| `sort` | `SortArray` | data | | Sort spec. |
| `viewType` | `'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map'` | data | | Which view to render (default grid). This is how you get a kanban/calendar/gantt of the object. |
| `navigation` | `{ mode: 'page' \| 'drawer' \| 'modal' \| 'split' \| 'none' }` | data | | What a row click does. Set { mode: 'none' } when you handle clicks yourself via onRowClick. |
| `searchableFields` | `string[]` | data | | Fields the toolbar search matches. |
| `filters` | `FilterArray` | controlled | | ObjectQL filter, e.g. ['status','=','active'] or ['and', [...], [...]]. Drive from React state for tabbed/searched lists. |
| `onRowClick` | `(record) => void` | callback | | Called with the clicked row's record. The hook for master/detail. |
| `onNavigate` | `(recordId, action) => void` | callback | | Called for page-level navigation (action: 'view' \| 'edit'). |

## `<ObjectGrid>` — `object-grid`

Lower-level data grid for an object (columns + filters). Prefer ListView for a full toolbar; use ObjectGrid for an embedded table.

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `objectName` | `string` | data | ✓ | The object to render. |
| `columns` | `ColumnDef[]` | data | | Column definitions. |
| `filters` | `FilterArray` | controlled | | ObjectQL filter. |

## `<ObjectMasterDetailForm>` — `object-master-detail-form`

Parent record + child line-items, edited and saved atomically (e.g. Invoice + line items).

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `objectName` | `string` | data | ✓ | Parent object. |
| `childObject` | `string` | data | ✓ | Child (line-item) object. |
| `relationshipField` | `string` | data | ✓ | Field on the child that points to the parent. |
| `columns` | `ColumnDef[]` | data | ✓ | Child line-item grid columns. |
| `totalField` | `string` | data | | Parent field that rolls up the child amounts. |
| `mode` | `'create' \| 'edit'` | controlled | | Create or edit the parent + children. |
| `recordId` | `string \| number` | controlled | | Parent record to edit. |
| `onSuccess` | `(record) => void` | callback | | After the atomic save. |
| `onCancel` | `() => void` | callback | | On cancel. |

## `<ObjectChart>` — `object-chart`

Chart over an object's aggregated data (bar/line/area/pie/donut/…).

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `objectName` | `string` | data | ✓ | The object to aggregate. |
| `chartType` | `'bar' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'scatter' \| 'radar' \| '…'` | data | | Chart variant. |
| `aggregate` | `{ field, function, groupBy }` | data | | Aggregation: function (sum/avg/count) over field, grouped by groupBy. |
| `filter` | `FilterArray` | controlled | | ObjectQL filter to scope the data. |

## `<Block>` — `(any)`

Escape hatch — render any registered component by type when it isn't injected as a named block.

| prop | type | kind | required | description |
|------|------|------|:--------:|-------------|
| `type` | `string` | data | ✓ | The registered component type, e.g. 'object-kanban'. |

## Also available

Other curated public blocks are reachable too. Kanban/calendar/gantt/timeline/map of an object = <ListView viewType="kanban" …>. Metrics/dashboards/pivots and record:* panels can be rendered via <Block type="object-metric" …> / <Block type="dashboard" …> until they are added to this contract.

Injected scope (closure variables, reference directly — not props): `React`, `useAdapter`, `data`, `variables`, `page`.
Loading