ObjectStackObjectStack

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

TypeDescriptionUse Case
gridTable/spreadsheet viewDefault record listing
kanbanCard board grouped by fieldStatus-based workflows
galleryCard grid with imagesVisual content browsing
calendarCalendar with date eventsScheduling and planning
timelineHorizontal timelineProject scheduling
ganttGantt chart with dependenciesProject management
mapGeographic map pinsLocation-based data
chartAggregate chart visualizationDashboards and summaries
treeSelf-referencing hierarchy (tree-grid)Org charts, category trees

List View Properties

PropertyTypeRequiredDescription
columnsstring[] | ListColumn[]Column definitions (required on every list view, including kanban/calendar/…)
labelstringoptionalDisplay label shown in the view switcher
typeenumoptionalView type (see table above; default 'grid')
dataViewDataoptionalData source configuration (defaults to the object provider)
filterarrayoptionalBase filter criteria
sortarrayoptionalSort configuration
searchableFieldsstring[]optionalFields included in search
groupingobjectoptionalRow grouping configuration
paginationobjectoptionalPagination settings
selectionobjectoptionalRow selection mode
navigationobjectoptionalRow click navigation
rowActionsarrayoptionalPer-row action buttons, by action name — see Actions
bulkActionsarrayoptionalBulk selection actions, by action name — see Actions
inlineEditbooleanoptionalEnable inline editing
exportOptionsstring[]optionalEnabled 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',
  },
]
PropertyTypeDescription
fieldstringField name
labelstringDisplay header
widthnumberColumn width in pixels
hiddenbooleanHide column
pinned'left' | 'right'Freeze column position
sortablebooleanAllow column sorting
resizablebooleanAllow column resize
wrapbooleanAllow text wrapping
summaryenumFooter aggregation: none, count, count_empty, count_filled, count_unique, percent_empty, percent_filled, sum, avg, min, max
align'left' | 'center' | 'right'Text alignment
linkbooleanCell 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

TypeDescription
simpleSingle-page form
tabbedForm with tab navigation
wizardMulti-step wizard
splitSplit-pane layout
drawerSide drawer form
modalModal dialog form

Section Configuration

PropertyTypeDescription
namestringStable identifier (snake_case) for i18n lookup
labelstringSection header
columns1-4Grid column count
collapsiblebooleanCan section be collapsed
collapsedbooleanInitially collapsed
visibleWhenstringCEL 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'",
  },
]
PropertyTypeDescription
fieldstringField name
labelstringDisplay label override
placeholderstringPlaceholder text
helpTextstringHelp/hint text
readonlybooleanRead-only override
requiredbooleanRequired override
hiddenbooleanHidden override
span'auto' | 'full'Relative width — 'full' takes the whole row at any column count (preferred)
colSpan1-4Legacy absolute column span — prefer span
widgetstringCustom widget/component name
dependsOnstringParent field for cascading
visibleWhenstringVisibility 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 },
          ],
        },
      ],
    },
  },
});

On this page