Filters & Columns Configuration in filters.ts #

The filters.ts file acts as the declarative schema for interactive filter controls placed in the listing page toolbar as well as the typed column definitions rendered by the data table. Dolfin reads this configuration to render form controls, manage filter state, infer TypeScript types for fetcher.ts, validate incoming requests, and format table columns.


Defining Filters with defineFilters #

Wrap your filter array in defineFilters([...]) to enable full TypeScript type inference across files without manual imports:

typescript
export const filters = defineFilters([
  {
    key: "status",
    label: "Lead Status",
    type: "select",
    options: [
      { label: "New", value: "new" },
      { label: "Contacted", value: "contacted" },
      { label: "Qualified", value: "qualified" },
      { label: "Converted", value: "converted" },
    ],
    defaultValue: "new",
  },
  {
    key: "search",
    label: "Search Leads",
    type: "text",
    placeholder: "Search by name or email...",
  },
]);

Defining Columns with defineColumns #

Export a columns array wrapped in defineColumns([...]). This gives the table deterministic headers, widths, alignments, and type-specific cell renderers (currency, datetime, date, badge, boolean, number, text).

Additionally, the column definitions are analyzed by the platform to automatically generate the typed __ListingRow interface for fetcher.ts and actions/*.ts:

typescript
export const columns = defineColumns([
  { key: "id", label: "ID", type: "number", width: 80, isPrimaryKey: true },
  { key: "name", label: "Lead Name", type: "text" },
  { key: "email", label: "Email Address", type: "text" },
  { key: "phone", label: "Phone", type: "text", nullable: true },
  { key: "value", label: "Estimated Value", type: "currency", currency: "USD" },
  { key: "status", label: "Status", type: "badge" },
  { key: "createdAt", label: "Created At", type: "datetime" },
]);

export default { filters, columns };

Column Properties Specification #

PropertyTypeDescription
keystringRequired. Object property key matching returned row data.
labelstringRequired. Header label rendered in the table column.
typeColumnTypeRequired. Cell renderer and formatting type.
isPrimaryKeybooleanDesignates this column as the unique identifier for row selection and actions.
widthnumberOptional explicit column width in pixels (e.g. 80, 150).
align"left" | "center" | "right"Text alignment within cells. Default is "left".
nullablebooleanWhen true, allows null values and types the property as T | null.
currencystringISO currency symbol or code when type: "currency" (e.g. "USD", "EUR").

Supported Column Types #

Column TypeTypeScript ValueFormatting & Renderer
"text"stringText string with truncate tooltip
"number"numberLocale-formatted number (1,234)
"currency"numberFormatted currency with ISO code (e.g. $1,234.00)
"boolean"booleanSuccess/neutral pill badge (True / False)
"date"string (ISO)Formatted date (e.g. Oct 03, 2026)
"datetime"string (ISO)Formatted timestamp (e.g. Oct 03, 2026 14:30)
"badge"stringCompact pill badge
💡 Tip

Setting isPrimaryKey: true on your unique identifier column (such as id or uuid) enables checkbox selection in the table and powers bulk actions.


Supported Filter Types #

Dolfin supports six filter control primitives:

Filter TypeUI RepresentationRuntime Value TypeExample Usage
"select"Single selection dropdownstring | numberStatus, Pipeline, Priority
"multi_select"Multi-tag dropdown picker(string | number)[]Tags, Multiple Team IDs
"text"Text search input with clear iconstringName, Company, Keyword search
"number"Numeric input fieldnumberMinimum Deal Value, Lead Score
"date_range"Date range picker{ from?: string; to?: string }Created Between, Closed Date
"boolean"Toggle switch / checkboxbooleanIs VIP, Email Verified

Filter Definition Specification #

Every item in the exported filters array implements the FilterDefinition interface:

typescript
interface OptionItem<TValue = string | number> {
  label: string;
  value: TValue;
}

type DynamicOptionsFn<TParent = unknown> = (
  parentValue: TParent,
  dolfin: DolfinApi
) => Promise<OptionItem[]> | OptionItem[];

interface FilterDefinition {
  /** Unique query key passed to context.filter in fetcher.ts */
  key: string;
  /** Human-readable label displayed in the toolbar */
  label: string;
  /** Filter control primitive */
  type: "select" | "multi_select" | "text" | "number" | "date_range" | "boolean";
  /** Optional placeholder text shown when no value is set */
  placeholder?: string;
  /** Optional initial default value */
  defaultValue?: unknown;
  /** Optional key of parent filter this control depends on */
  dependsOn?: string;
  /** Static array or dynamic resolver function (for select & multi_select) */
  options?: readonly OptionItem[] | OptionItem[] | DynamicOptionsFn;
}

Static Options vs. Dynamic Resolvers #

Filter options for "select" and "multi_select" can be supplied as either static arrays or dynamic resolver functions:

1. Static Options #

Use static options when the available choices are constant or known ahead of time (e.g. fixed status codes or predefined priority tiers):

typescript
export const filters = [
  {
    key: "status",
    label: "Lead Status",
    type: "select",
    options: [
      { label: "New", value: "new" },
      { label: "Contacted", value: "contacted" },
      { label: "Qualified", value: "qualified" },
      { label: "Converted", value: "converted" },
    ],
    defaultValue: "new",
  },
  {
    key: "tags",
    label: "Tags",
    type: "multi_select",
    options: [
      { label: "Enterprise", value: "enterprise" },
      { label: "Mid-Market", value: "mid_market" },
      { label: "SMB", value: "smb" },
    ],
  },
] as const;

2. Dynamic Option Resolvers #

Dynamic options are declared as functions accepting (parentValue, dolfin). They execute in a lightweight isolate sandbox whenever options need to be hydrated or when a parent filter changes:

typescript
export const filters = [
  {
    key: "pipelineId",
    label: "Pipeline",
    type: "select",
    options: [
      { label: "Standard Sales", value: 1 },
      { label: "Partnerships", value: 2 },
    ],
  },
  {
    key: "stageId",
    label: "Pipeline Stage",
    type: "select",
    dependsOn: "pipelineId",
    options: async (pipelineId: number, dolfin) => {
      if (!pipelineId) return [];

      // Query stages from CRM module
      const crm = await dolfin.crm.module(1);
      const stages = await crm.stages.list();

      // Return formatted OptionItem array
      return stages
        .filter((s) => s.pipelineId === pipelineId)
        .map((s) => ({ label: s.name, value: s.id }));
    },
  },
] as const;
ℹ️ Note

Dynamic option resolvers have a maximum execution timeout of 5,000 ms (5 seconds) and a 32 MB isolate memory limit. Ensure queries execute quickly.


Cascading Filters (dependsOn) #

Cascading filters enable parent-child dependencies between dropdowns. When a parent filter value changes, the frontend automatically:

  1. Resets the child filter's active value.
  2. Disables the child dropdown while fetching updated options.
  3. Invokes the child's dynamic options resolver on the server, passing parentValue as the first argument.

Static In-Memory Cascade Example #

When the option set is small, you can map parent keys to child options directly in memory:

typescript
export const filters = [
  {
    key: "country",
    label: "Country",
    type: "select",
    placeholder: "Select country...",
    options: [
      { label: "United States", value: "US" },
      { label: "Canada", value: "CA" },
      { label: "United Kingdom", value: "UK" },
    ],
  },
  {
    key: "state",
    label: "State / Province",
    type: "select",
    dependsOn: "country",
    placeholder: "Select state...",
    options: (country: string) => {
      const stateMap: Record<string, { label: string; value: string }[]> = {
        US: [
          { label: "California", value: "CA" },
          { label: "New York", value: "NY" },
          { label: "Texas", value: "TX" },
        ],
        CA: [
          { label: "Ontario", value: "ON" },
          { label: "Quebec", value: "QC" },
        ],
        UK: [
          { label: "England", value: "ENG" },
          { label: "Scotland", value: "SCT" },
        ],
      };
      return stateMap[country] || [];
    },
  },
] as const;

export default filters;

Dynamic External API Cascade Example #

You can query external REST endpoints via dolfin.http.get inside your dynamic resolver. Remember that dolfin.http.get takes { headers?, timeoutMs? } (no params) and returns res.body:

typescript
export const filters = [
  {
    key: "category",
    label: "Department",
    type: "select",
    options: [
      { label: "Electronics", value: "electronics" },
      { label: "Furniture", value: "furniture" },
    ],
  },
  {
    key: "productSubcategory",
    label: "Subcategory",
    type: "select",
    dependsOn: "category",
    placeholder: "Choose subcategory...",
    options: async (category: string, dolfin) => {
      if (!category) return [];

      const res = await dolfin.http.get(
        `https://api.inventory.example.com/categories/${category}/subcategories`,
        {
          headers: { "Authorization": "Bearer YOUR_SERVICE_TOKEN" },
          timeoutMs: 4000,
        }
      );

      // res.body contains the parsed JSON response
      const items = (res.body as { code: string; name: string }[]) || [];
      return items.map((item) => ({
        label: item.name,
        value: item.code,
      }));
    },
  },
] as const;

export default filters;

Best Practices for Filter Authoring #

  1. Provide Meaningful Placeholders: Add placeholder: "Select status..." to clarify empty selections.
  2. Handle Nullish parentValue: Always check if (!parentValue) return []; at the start of cascading resolvers.
  3. Use as const on Filter Tuples: Adding as const to export const filters = [...] as const; allows TypeScript to strictly infer literal filter keys and value types in fetcher.ts.
  4. Keep Dynamic Resolvers Fast: Resolvers run synchronously on user selection. Avoid expensive operations or slow third-party API chains.