HotCRM Logo

UI Metadata Authoring Guide

How to author Page Layouts, List Views, Dashboards, and Form Views using @objectstack/spec/ui schemas

UI Metadata Authoring Guide

HotCRM defines all UI layouts as TypeScript metadata validated against @objectstack/spec/ui schemas. This guide covers the four core UI metadata types and how to author them.

Overview

Metadata TypeFile SuffixSchemaPurpose
Page Layout*.page.tsPageSchemaRecord detail pages with sections, tabs, related lists
List View*.view.tsViewSchemaRecord list tables with columns, filters, sorting
Dashboard*.dashboard.tsDashboardSchemaMetric dashboards with widget grid layouts
Form View*.form.tsFormViewSchemaData entry forms with sections and field layout

All UI metadata files follow the same validation pattern:

import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';

export const MyPage = {
  // ... page definition
} satisfies Page;

PageSchema.parse(MyPage);  // Runtime validation
export default MyPage;

Page Layout Authoring

Page layouts define record detail pages — the view shown when opening a single record.

Structure

A page layout consists of regions, each containing components:

// packages/crm/src/account.page.ts
import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';

export const AccountPage = {
  name: 'account_detail',
  object: 'account',
  type: 'record' as const,
  label: 'Account Detail Page',

  regions: [
    {
      name: 'header',
      components: [
        {
          type: 'record:highlights' as const,
          properties: {
            fields: ['name', 'type', 'industry', 'annual_revenue', 'phone']
          }
        }
      ]
    },
    {
      name: 'tabs',
      components: [
        {
          type: 'page:tabs' as const,
          properties: {
            tabs: ['account_info', 'address_info', 'additional_info']
          }
        }
      ]
    },
    {
      name: 'account_info',
      components: [
        {
          type: 'record:details' as const,
          label: 'Account Information',
          properties: {
            fields: [
              'name', 'account_number', 'type', 'industry',
              'phone', 'website', 'annual_revenue', 'number_of_employees'
            ],
            columns: 2
          }
        }
      ]
    },
    {
      name: 'related_lists',
      components: [
        {
          type: 'record:related_list' as const,
          label: 'Opportunities',
          properties: {
            object: 'opportunity',
            columns: ['name', 'stage', 'amount', 'close_date', 'probability'],
            filters: [['stage', '!=', 'closed_lost']],
            sort: [{ field: 'close_date', direction: 'asc' }],
            actions: ['new', 'edit', 'delete']
          }
        }
      ]
    }
  ]
} satisfies Page;

PageSchema.parse(AccountPage);
export default AccountPage;

Available Component Types

Component TypePurpose
record:highlightsKey fields displayed prominently in the page header
record:detailsField section with configurable column layout
page:tabsTab navigation between page sections
record:related_listChild/related object table with filters and actions

Region Naming Convention

  • header — Page top, typically contains record:highlights
  • tabs — Tab navigation region
  • Named tab regions (e.g., account_info, address_info) — Tab content
  • related_lists — Bottom section with related object tables

List View Authoring

List views define record list tables — the grid shown when browsing a collection of records.

Structure

A view defines columns, optional filters, sorting, pagination, and bulk actions:

// packages/crm/src/account.view.ts
import type { View } from '@objectstack/spec/ui';
import { ViewSchema } from '@objectstack/spec/ui';

export const AllAccountsView = {
  list: {
    name: 'all_accounts',
    label: 'All Accounts',
    columns: [
      { field: 'name', width: 250, sortable: true, link: true },
      { field: 'type', width: 120, sortable: true },
      { field: 'industry', width: 150, sortable: true },
      { field: 'annual_revenue', width: 150, sortable: true, align: 'right' as const },
      { field: 'phone', width: 150 },
      { field: 'owner', width: 150 },
      { field: 'created_date', width: 150, sortable: true }
    ],
    sort: [{ field: 'name', order: 'asc' as const }],
    bulkActions: ['delete', 'update_owner', 'export'],
    inlineEdit: true,
    pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50, 100] }
  }
} satisfies View;

ViewSchema.parse(AllAccountsView);

Column Properties

PropertyTypeDescription
fieldstringField API name from the object schema
widthnumberColumn width in pixels
sortablebooleanWhether the column can be sorted
linkbooleanWhether to render the value as a link to the record
align'left' | 'right' | 'center'Text alignment

Filtering

Filters use an array-based syntax compatible with ObjectQL:

// Single filter
filter: [['type', '=', 'customer']]

// Multiple filters (AND)
filter: [
  ['annual_revenue', '>', 10000000],
  ['type', '=', 'customer']
]

// Dynamic filter (current user)
filter: [['owner', '=', '${currentUser.id}']]

// Date-based filter
filter: [['created_date', '>=', 'LAST_30_DAYS']]

// IN operator
filter: [['rating', 'IN', ['Hot', 'Warm']]]

Conditional Formatting

Apply visual styles based on record data:

export const HotAccountsView = {
  list: {
    name: 'hot_accounts',
    label: 'Hot Accounts',
    filter: [['rating', '=', 'Hot']],
    columns: [/* ... */],
    conditionalFormatting: [
      {
        condition: 'rating == "Hot"',
        style: { backgroundColor: '#FEF2F2', borderLeft: '3px solid #EF4444' }
      }
    ]
  }
} satisfies View;

Multiple Views per Object

Export multiple named views from a single *.view.ts file:

export const AccountListViews = {
  all: AllAccountsView,
  my: MyAccountsView,
  enterprise: EnterpriseAccountsView,
  recent: RecentlyCreatedView,
  hot: HotAccountsView,
  needAttention: NeedAttentionView
};

Dashboard Authoring

Dashboards define metric overview pages with a grid-based widget layout.

Structure

A dashboard contains an array of widgets, each with a type, data source, and grid position:

// packages/crm/src/crm.dashboard.ts
import type { Dashboard } from '@objectstack/spec/ui';
import { DashboardSchema } from '@objectstack/spec/ui';

export const CrmDashboard = {
  name: 'crm_dashboard',
  label: 'Sales Dashboard',
  description: 'Key sales metrics and pipeline overview',
  widgets: [
    {
      title: 'Total Pipeline Value',
      type: 'metric' as const,
      object: 'opportunity',
      valueField: 'amount',
      aggregate: 'sum' as const,
      filter: ['stage', '!=', 'closed_lost'],
      layout: { x: 0, y: 0, w: 3, h: 2 }
    },
    {
      title: 'Pipeline by Stage',
      type: 'funnel' as const,
      object: 'opportunity',
      categoryField: 'stage',
      valueField: 'amount',
      aggregate: 'sum' as const,
      layout: { x: 0, y: 2, w: 6, h: 4 }
    },
    {
      title: 'Win Rate by Month',
      type: 'line' as const,
      object: 'opportunity',
      categoryField: 'close_date',
      valueField: 'probability',
      aggregate: 'avg' as const,
      layout: { x: 6, y: 2, w: 6, h: 4 }
    },
    {
      title: 'Deals Closing This Month',
      type: 'table' as const,
      object: 'opportunity',
      filter: ['stage', '!=', 'closed_lost'],
      layout: { x: 6, y: 6, w: 6, h: 4 }
    }
  ]
} satisfies Dashboard;

DashboardSchema.parse(CrmDashboard);
export default CrmDashboard;

Widget Types

TypePurposeRequired Properties
metricSingle numeric value (e.g., total revenue)object, aggregate, optionally valueField
kpiKey performance indicator with target/trendobject, valueField, aggregate
funnelFunnel chart for stage progressionobject, categoryField, valueField, aggregate
lineLine chart for trends over timeobject, categoryField, valueField, aggregate
barBar chart for comparisonsobject, categoryField, valueField, aggregate
piePie chart for distributionobject, categoryField, valueField, aggregate
tableData table with recordsobject

Grid Layout

The dashboard uses a 12-column grid system:

PropertyDescription
xColumn start position (0–11)
yRow start position
wWidth in columns (1–12)
hHeight in rows

Aggregation Functions

AggregateDescription
sumTotal of all values
countNumber of records
avgAverage value
minMinimum value
maxMaximum value

Form View Authoring

Form views define data entry layouts for creating and editing records.

Structure

A form contains sections, each with a column layout and field list:

// packages/crm/src/contact.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';

export const ContactForm = {
  type: 'simple' as const,
  data: {
    provider: 'object' as const,
    object: 'contact'
  },
  sections: [
    {
      label: 'Contact Information',
      columns: '2' as const,
      fields: [
        { field: 'first_name', required: true },
        { field: 'last_name', required: true },
        { field: 'salutation' },
        { field: 'account_id', label: 'Account' },
        { field: 'title' },
        { field: 'department' },
        { field: 'email', required: true },
        { field: 'phone' }
      ]
    },
    {
      label: 'Communication Preferences',
      columns: '2' as const,
      fields: [
        { field: 'mobile_phone' },
        { field: 'fax' },
        { field: 'preferred_contact' },
        { field: 'last_contact_date' }
      ]
    },
    {
      label: 'Role Details',
      columns: '2' as const,
      fields: [
        { field: 'level' },
        { field: 'is_decision_maker' },
        { field: 'influence_level' },
        { field: 'relationship_strength' },
        { field: 'notes', colSpan: 2 }
      ]
    }
  ]
} satisfies FormView;

FormViewSchema.parse(ContactForm);
export default ContactForm;

Section Properties

PropertyTypeDescription
labelstringSection heading
columns'1' | '2'Number of columns in the section
fieldsFormField[]Array of field definitions

Field Properties

PropertyTypeDescription
fieldstringField API name from the object schema
labelstringOverride display label
requiredbooleanMark as required (supplements object-level required)
colSpannumberColumns to span (use 2 for full-width in a 2-column layout)

Schema Validation

All UI metadata must be validated at export time. This ensures type safety and catches errors before deployment.

// Import the type (for satisfies) and the schema (for parse)
import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';

// Use satisfies for compile-time checking
export const MyPage = { /* ... */ } satisfies Page;

// Use parse() for runtime validation
PageSchema.parse(MyPage);

Schema Summary

SchemaImportValidates
PageSchema@objectstack/spec/uiPage layouts with regions and components
ViewSchema@objectstack/spec/uiList views with columns, filters, pagination
DashboardSchema@objectstack/spec/uiDashboard widgets with grid layout
FormViewSchema@objectstack/spec/uiForm sections with field layout

Common Patterns

Use as const for string literal types to satisfy the schema:

type: 'record' as const,           // Page type
align: 'right' as const,           // Column alignment
order: 'asc' as const,             // Sort order
type: 'metric' as const,           // Widget type
columns: '2' as const,             // Form columns
provider: 'object' as const,       // Form data provider

On this page