Skip to content

Latest commit

 

History

History
452 lines (388 loc) · 15.1 KB

File metadata and controls

452 lines (388 loc) · 15.1 KB
title View Metadata
description 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.

{/* 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],
});
**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
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.

Column Configuration

{/* 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

Bulk Actions Over a Selection

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.

`action.bulkEnabled` was retired in spec 17. The multi-select toolbar is driven by these two view keys only.

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.

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:

{/* 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)

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:

{/* 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 },
          ],
        },
      ],
    },
  },
});

Related