View Metadata
Configure list views and form views — grid, kanban, calendar, gantt, and more
View Metadata
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.
The defineView Container
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.
// 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],
});Do not author a flat view object — { name: 'all_tasks', label: 'All Tasks', type: 'grid', columns: [...] } at the top level is not a view
container. Nothing registers from it and no view appears in the switcher.
Every view must live under list, listViews, or formViews, and each
view binds its object via data: { provider: 'object', object: '...' }.
List View
A List View controls how a collection of records is presented. It supports multiple visualization types.
View 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 |
List View Properties
| 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 |
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.
Column Configuration
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',
},
]| 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 | Footer aggregation: none, count, count_empty, count_filled, count_unique, percent_empty, percent_filled, sum, avg, min, max |
align | 'left' | 'center' | 'right' | Text alignment |
link | boolean | Cell functions as the primary navigation link |
Data Source
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' }],
}Type-Specific Configuration
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.
Kanban
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
}Calendar
type: 'calendar',
calendar: {
startDateField: 'start_date',
endDateField: 'end_date',
titleField: 'title',
colorField: 'status',
}Gantt
type: 'gantt',
gantt: {
startDateField: 'start_date',
endDateField: 'end_date',
titleField: 'title',
progressField: 'percent_complete',
dependenciesField: 'depends_on',
}Form View
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.
Basic Structure
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' },
],
},
],
},
},Form Types
| 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 |
Section Configuration
| 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 |
Form Field Configuration
Each field in a form section can be customized:
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) |
Complete Example
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:
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 },
],
},
],
},
},
});Related
- 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