| title | View Metadata |
|---|---|
| description | Configure list views and form views — grid, kanban, calendar, gantt, and more |
A View defines how records of an Object are displayed to users. ObjectStack supports two main view categories: List Views for browsing records and Form Views for editing individual records.
Views are authored per object inside a defineView({ ... }) container — the default list, named listViews, and named formViews all live in one document. The loader expands the container into independently addressable <object>.<key> view items that power the view switcher.
{/* os:check */}
// src/ui/views/task.view.ts
import { defineView } from '@objectstack/spec';
const data = { provider: 'object' as const, object: 'task' };
export const TaskViews = defineView({
// Default list shown when the object is opened
list: {
label: 'All Tasks',
type: 'grid',
data,
columns: [
{ field: 'title', width: 300 },
{ field: 'status', width: 120 },
{ field: 'assignee', width: 200 },
{ field: 'due_date', width: 150 },
],
sort: [{ field: 'created_at', order: 'desc' }],
},
// Named saved views — entries in the view switcher
listViews: {
urgent: {
label: 'Urgent',
type: 'grid',
data,
columns: [{ field: 'title' }, { field: 'assignee' }, { field: 'due_date' }],
filter: [{ field: 'priority', operator: 'equals', value: 'urgent' }],
},
},
// Named form views
formViews: {
edit: {
type: 'simple',
data,
sections: [
{ label: 'Task', columns: 2, fields: ['title', 'status', 'assignee', 'due_date'] },
],
},
},
});Register the container in your stack config:
// objectstack.config.ts
export default defineStack({
// ...
views: [TaskViews],
});A List View controls how a collection of records is presented. It supports multiple visualization types.
| Type | Description | Use Case |
|---|---|---|
grid |
Table/spreadsheet view | Default record listing |
kanban |
Card board grouped by field | Status-based workflows |
gallery |
Card grid with images | Visual content browsing |
calendar |
Calendar with date events | Scheduling and planning |
timeline |
Horizontal timeline | Project scheduling |
gantt |
Gantt chart with dependencies | Project management |
map |
Geographic map pins | Location-based data |
chart |
Aggregate chart visualization | Dashboards and summaries |
tree |
Self-referencing hierarchy (tree-grid) | Org charts, category trees |
| Property | Type | Required | Description |
|---|---|---|---|
columns |
string[] | ListColumn[] |
✅ | Column definitions (required on every list view, including kanban/calendar/…) |
label |
string |
optional | Display label shown in the view switcher |
type |
enum |
optional | View type (see table above; default 'grid') |
data |
ViewData |
optional | Data source configuration (defaults to the object provider) |
filter |
array |
optional | Base filter criteria |
sort |
array |
optional | Sort configuration |
searchableFields |
string[] |
optional | Fields included in search |
grouping |
object |
optional | Row grouping configuration |
pagination |
object |
optional | Pagination settings |
selection |
object |
optional | Row selection mode |
navigation |
object |
optional | Row click navigation |
rowActions |
array |
optional | Per-row action buttons, by action name — see Actions |
bulkActions |
array |
optional | Bulk selection actions, by action name — see Actions |
bulkActionDefs |
array |
optional | Rich bulk action definitions — mass edits, and the aggregate single-call mode (below) |
inlineEdit |
boolean |
optional | Enable inline editing |
exportOptions |
string[] |
optional | Enabled export formats (csv, xlsx, pdf, json) |
The view's machine name is its key in the container (listViews.urgent on
object task becomes task.urgent); the default list claims task.default.
List and form views share that one namespace — don't reuse a key.
{/* os:check */}
columns: [
{
field: 'name',
label: 'Account Name',
width: 250,
pinned: 'left', // 'left' | 'right' — freeze column
sortable: true,
resizable: true,
summary: 'count', // Column footer aggregation (enum value)
},
{
field: 'annual_revenue',
label: 'Revenue',
width: 150,
align: 'right',
summary: 'sum',
},
{
field: 'opportunity_name',
prefix: { field: 'stage', type: 'badge' }, // compound cell: [Badge] Text
// Object form of `summary` — aggregate a *different* field in this footer
summary: { type: 'sum', field: 'amount_in_base_currency' },
},
]| Property | Type | Description |
|---|---|---|
field |
string |
Field name |
label |
string |
Display header |
width |
number |
Column width in pixels |
hidden |
boolean |
Hide column |
pinned |
'left' | 'right' |
Freeze column position |
sortable |
boolean |
Allow column sorting |
resizable |
boolean |
Allow column resize |
wrap |
boolean |
Allow text wrapping |
summary |
enum | { type, field? } |
Footer aggregation: none, count, count_empty, count_filled, count_unique, percent_empty, percent_filled, sum, avg, min, max. The object form aggregates a different field than the column's own |
prefix |
{ field, type?: 'badge' | 'text' } |
Compound cell — render another field's value inline before this cell's value |
align |
'left' | 'center' | 'right' |
Text alignment |
link |
boolean |
Cell functions as the primary navigation link |
Two keys drive the multi-select toolbar. bulkActions names actions the object
already declares, and each selected record is dispatched once — the action
runs N times for N rows. bulkActionDefs carries richer entries: a mass edit
through the data API (operation: 'update' + a patch), or the aggregate
mode, where the action is called once for the whole selection.
bulkActions: ['mark_done'], // one dispatch per selected record
bulkActionDefs: [
// Mass edit — one bulk write, no action involved.
{ name: 'archive', operation: 'update', patch: { archived: true } },
// Aggregate — ONE call to the declared `export_zip` action, carrying every
// selected id. This is the "N devices → one zip download" shape; also batch
// print, merged-PDF export.
{ name: 'export_zip', operation: 'custom', execution: 'aggregate' },
]An aggregate dispatch delivers the selection to the handler as
params._selectedIds: string[] — read that, not recordId. Results are
all-or-nothing: a handler that cannot cover the whole selection must reject,
and per-row retry is replaced by re-running the action. batchSize does not
apply (the call is never chunked); set maxRecords when the server work is
expensive.
Pick the right key for the dispatch you mean. Per-record is
bulkActions: ['<name>'] — the bare-string form, which has nowhere to carry a
flag and is promoted with the action's own label, params and visible.
Aggregate is the def form, which is where execution lives. A def that says
operation: 'custom' without execution: 'aggregate' is rejected at parse
time: the renderer has no action attached to such a def, so it used to render a
button that reported success for every selected record and did nothing.
A url or api action rendered on the list toolbar can also read the current
selection through target interpolation — ${ctx.selection.ids} (comma-joined)
and ${ctx.selection.count} — without any bulk wiring.
A bulkActionDefs entry is a typed shape — see the
BulkActionDef reference. Unknown keys are
rejected with the canonical spelling named, and so are keys the executor would
never read (patch outside an update, execution outside a custom,
batchSize on an aggregate). One key is deliberately not authorable:
actionDef is attached by the renderer when it resolves the def's name, and
writing it by hand would smuggle an action definition past the action registry.
Views can load data from several sources (object, api, value, and schema):
// From an Object (most common)
data: { provider: 'object', object: 'task' }
// From an external API
data: {
provider: 'api',
read: { url: '/api/external/tasks', method: 'GET' },
}
// Static values
data: {
provider: 'value',
items: [{ id: '1', name: 'Item 1' }],
}Each non-grid visualization reads its settings from a nested config block
named after the type (kanban, calendar, gantt, gallery, timeline,
chart, tree) — not from top-level keys.
type: 'kanban',
columns: ['title', 'assignee', 'priority'], // still required at the top level
kanban: {
groupByField: 'status', // Field to group columns by (usually status/select)
summarizeField: 'amount', // Optional numeric field summed at top of each column
columns: ['title', 'assignee', 'priority'], // Fields shown on each card
}type: 'calendar',
calendar: {
startDateField: 'start_date',
endDateField: 'end_date',
titleField: 'title',
colorField: 'status',
}type: 'gantt',
gantt: {
startDateField: 'start_date',
endDateField: 'end_date',
titleField: 'title',
progressField: 'percent_complete',
dependenciesField: 'depends_on',
}A Form View defines how a single record is displayed for viewing or editing. Form views live under formViews in the same defineView container as the object's list views.
formViews: {
edit: {
type: 'simple',
data: { provider: 'object', object: 'task' },
sections: [
{
label: 'Basic Information',
columns: 2,
fields: [
{ field: 'title', span: 'full' },
{ field: 'status' },
{ field: 'priority' },
{ field: 'assignee' },
{ field: 'due_date' },
],
},
{
label: 'Details',
collapsible: true,
columns: 1,
fields: [
{ field: 'description' },
],
},
],
},
},| Type | Description |
|---|---|
simple |
Single-page form |
tabbed |
Form with tab navigation |
wizard |
Multi-step wizard |
split |
Split-pane layout |
drawer |
Side drawer form |
modal |
Modal dialog form |
| Property | Type | Description |
|---|---|---|
name |
string |
Stable identifier (snake_case) for i18n lookup |
label |
string |
Section header |
columns |
1-4 |
Grid column count |
collapsible |
boolean |
Can section be collapsed |
collapsed |
boolean |
Initially collapsed |
visibleWhen |
string |
CEL predicate — section shown only when TRUE |
fields |
(string | FormField)[] |
Fields in the section |
Each field in a form section can be customized:
{/* os:check */}
fields: [
{
field: 'title',
label: 'Task Title', // Override field label
placeholder: 'Enter title',
helpText: 'A brief description of the task',
required: true, // Override required
span: 'full', // Take the whole row at any column count
visibleWhen: "record.status != 'cancelled'",
},
]| Property | Type | Description |
|---|---|---|
field |
string |
Field name |
label |
string |
Display label override |
placeholder |
string |
Placeholder text |
helpText |
string |
Help/hint text |
readonly |
boolean |
Read-only override |
required |
boolean |
Required override |
hidden |
boolean |
Hidden override |
span |
'auto' | 'full' |
Relative width — 'full' takes the whole row at any column count (preferred) |
colSpan |
1-4 |
Legacy absolute column span — prefer span |
widget |
string |
Custom widget/component name |
dependsOn |
string |
Parent field for cascading |
visibleWhen |
string |
Visibility predicate (CEL); runtime forms bind record + current_user (was visibleOn, ADR-0089) |
One container covering a default grid, a kanban saved view, and a tabbed edit form — mirroring examples/app-showcase/src/ui/views/task.view.ts:
{/* os:check */}
import { defineView } from '@objectstack/spec';
const data = { provider: 'object' as const, object: 'task' };
export const TaskViews = defineView({
list: {
label: 'All Tasks',
type: 'grid',
data,
columns: [
{ field: 'title', label: 'Title' },
{ field: 'assignee', label: 'Assignee' },
{ field: 'priority', label: 'Priority' },
{ field: 'due_date', label: 'Due Date' },
],
sort: [{ field: 'priority', order: 'desc' }],
},
listViews: {
board: {
label: 'Task Board',
type: 'kanban',
data,
columns: ['title', 'assignee', 'priority', 'due_date'],
kanban: {
groupByField: 'status',
columns: ['title', 'assignee', 'priority', 'due_date'],
},
},
},
formViews: {
edit: {
type: 'tabbed',
data,
sections: [
{
name: 'details',
label: 'Details',
columns: 2,
fields: [
{ field: 'title', span: 'full', required: true },
{ field: 'status' },
{ field: 'priority' },
{ field: 'assignee' },
{ field: 'due_date' },
{ field: 'description', span: 'full' },
],
},
{
name: 'system',
label: 'System',
collapsible: true,
collapsed: true,
columns: 2,
fields: [
{ field: 'created_at', readonly: true },
{ field: 'updated_at', readonly: true },
],
},
],
},
},
});- Object Metadata — Define the data behind views
- Field Metadata — Field types that views display
- Page Metadata — Custom pages with component layouts
- Dashboard Metadata — Analytics dashboards with charts