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:
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:
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 #
| Property | Type | Description |
|---|---|---|
key | string | Required. Object property key matching returned row data. |
label | string | Required. Header label rendered in the table column. |
type | ColumnType | Required. Cell renderer and formatting type. |
isPrimaryKey | boolean | Designates this column as the unique identifier for row selection and actions. |
width | number | Optional explicit column width in pixels (e.g. 80, 150). |
align | "left" | "center" | "right" | Text alignment within cells. Default is "left". |
nullable | boolean | When true, allows null values and types the property as T | null. |
currency | string | ISO currency symbol or code when type: "currency" (e.g. "USD", "EUR"). |
Supported Column Types #
| Column Type | TypeScript Value | Formatting & Renderer |
|---|---|---|
"text" | string | Text string with truncate tooltip |
"number" | number | Locale-formatted number (1,234) |
"currency" | number | Formatted currency with ISO code (e.g. $1,234.00) |
"boolean" | boolean | Success/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" | string | Compact pill badge |
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 Type | UI Representation | Runtime Value Type | Example Usage |
|---|---|---|---|
"select" | Single selection dropdown | string | number | Status, Pipeline, Priority |
"multi_select" | Multi-tag dropdown picker | (string | number)[] | Tags, Multiple Team IDs |
"text" | Text search input with clear icon | string | Name, Company, Keyword search |
"number" | Numeric input field | number | Minimum Deal Value, Lead Score |
"date_range" | Date range picker | { from?: string; to?: string } | Created Between, Closed Date |
"boolean" | Toggle switch / checkbox | boolean | Is VIP, Email Verified |
Filter Definition Specification #
Every item in the exported filters array implements the FilterDefinition interface:
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):
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:
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;
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:
- Resets the child filter's active value.
- Disables the child dropdown while fetching updated options.
- Invokes the child's dynamic options resolver on the server, passing
parentValueas 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:
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:
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 #
- Provide Meaningful Placeholders: Add
placeholder: "Select status..."to clarify empty selections. - Handle Nullish
parentValue: Always checkif (!parentValue) return [];at the start of cascading resolvers. - Use
as conston Filter Tuples: Addingas consttoexport const filters = [...] as const;allows TypeScript to strictly infer literal filter keys and value types infetcher.ts. - Keep Dynamic Resolvers Fast: Resolvers run synchronously on user selection. Avoid expensive operations or slow third-party API chains.