Docs
Filter Bar

Filter Bar

An advanced filtering component with support for multiple conditions and operators.

import { format } from 'date-fns'
import { useState } from 'react'
import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui'
import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns'
 
function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
  const [date, setDate] = useState<any | undefined>(
    search
      ? {
          from: new Date(search),
          to: undefined,
        }
      : undefined
  )
 
  return (
    <div className="w-full space-y-4">
      <Calendar
        initialFocus
        mode="range"
        defaultMonth={date?.from}
        selected={date}
        onSelect={setDate}
        className="w-full"
      />
      <div className="flex justify-end gap-2 py-3 px-4 border-t">
        <Button type="default" onClick={onCancel}>
          Cancel
        </Button>
        <Button
          type="primary"
          onClick={() =>
            onChange(
              date?.from
                ? date.to
                  ? `${format(date.from, 'yyyy-MM-dd')} - ${format(date.to, 'yyyy-MM-dd')}`
                  : format(date.from, 'yyyy-MM-dd')
                : ''
            )
          }
        >
          Apply
        </Button>
      </div>
    </div>
  )
}
 
function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) {
  const [time, setTime] = useState(search || '')
 
  return (
    <div className="space-y-4">
      <h3 className="text-lg font-medium">Select Time</h3>
      <Input_Shadcn_ type="time" value={time} onChange={(e) => setTime(e.target.value)} />
      <div className="flex justify-end gap-2">
        <Button_Shadcn_ variant="outline" onClick={onCancel}>
          Cancel
        </Button_Shadcn_>
        <Button_Shadcn_ onClick={() => onChange(time)}>Apply</Button_Shadcn_>
      </div>
    </div>
  )
}
 
function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) {
  const [range, setRange] = useState(search || '')
 
  return (
    <div className="space-y-4">
      <h3 className="text-lg font-medium">Select Range</h3>
      <Input_Shadcn_ type="range" value={range} onChange={(e) => setRange(e.target.value)} />
      <div className="flex justify-end gap-2">
        <Button_Shadcn_ variant="outline" onClick={onCancel}>
          Cancel
        </Button_Shadcn_>
        <Button_Shadcn_ onClick={() => onChange(range)}>Apply</Button_Shadcn_>
      </div>
    </div>
  )
}
 
const filterProperties = [
  {
    label: 'Name',
    name: 'name',
    type: 'string' as const,
    operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'],
  },
  {
    label: 'Status',
    name: 'status',
    type: 'string' as const,
    options: [
      { label: 'Active', value: 'active' },
      { label: 'Inactive', value: 'inactive' },
      { label: 'Pending', value: 'pending' },
    ],
    operators: ['=', '!='],
  },
  {
    label: 'Type',
    name: 'type',
    type: 'string' as const,
    options: async (search?: string) => {
      await new Promise((resolve) => setTimeout(resolve, 500))
      const allOptions = ['user', 'admin', 'guest']
      return search
        ? allOptions.filter((option) => option.toLowerCase().includes(search.toLowerCase()))
        : allOptions
    },
    operators: ['=', '!='],
  },
  {
    label: 'Time period',
    name: 'created_at',
    type: 'date' as const,
    options: [
      { label: 'Today', value: format(new Date(), 'yyyy-MM-dd') },
      { label: 'Yesterday', value: format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') },
      { label: 'Last 7 days', value: format(new Date(Date.now() - 7 * 86400000), 'yyyy-MM-dd') },
      { label: 'Last 30 days', value: format(new Date(Date.now() - 30 * 86400000), 'yyyy-MM-dd') },
      {
        label: 'Pick a date...',
        component: (props: CustomOptionProps) => <CustomDatePicker {...props} />,
      },
    ],
    operators: ['=', '!=', '>', '<', '>=', '<='],
  },
  {
    label: 'Priority',
    name: 'priority',
    type: 'number' as const,
    options: {
      component: (props: CustomOptionProps) => (
        <div className="p-6">
          <Button onClick={() => props.onChange('1')}>Custom value</Button>
        </div>
      ),
    },
    triggerOnPropertyClick: true,
    operators: ['=', '!=', '>', '<', '>=', '<='],
  },
]
 
const initialFilters: FilterGroup = {
  logicalOperator: 'AND',
  conditions: [],
}
 
export function FilterBarDemo() {
  const [filters, setFilters] = useState<FilterGroup>(initialFilters)
  const [freeformText, setFreeformText] = useState('')
 
  return (
    <div className="w-full">
      <FilterBar
        filterProperties={filterProperties}
        freeformText={freeformText}
        onFreeformTextChange={setFreeformText}
        filters={filters}
        onFilterChange={setFilters}
      />
    </div>
  )
}

Usage

The Filter Bar component provides advanced filtering capabilities with support for multiple conditions, operators, and different field types. It can be used with both static and async options.

const filterProperties = [
  {
    label: 'Name',
    name: 'name',
    type: 'string',
    operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'],
  },
  {
    label: 'Status',
    name: 'status',
    type: 'string',
    options: ['active', 'inactive', 'pending'],
    operators: ['=', '!='],
  },
]
 
export function FilterDemo() {
  const [filters, setFilters] = useState<FilterGroup>(initialFilters)
  const [freeformText, setFreeformText] = useState('')
 
  return (
    <FilterBar
      filterProperties={filterProperties}
      freeformText={freeformText}
      onFreeformTextChange={setFreeformText}
      filters={filters}
      onFilterChange={setFilters}
    />
  )
}

API Reference

FilterProperty

interface FilterProperty {
  label: string
  name: string
  type: 'string' | 'number' | 'date' | 'boolean'
  operators: string[]
  options?: string[] | ((search?: string) => Promise<string[]> | string[])
}

FilterGroup

interface FilterGroup {
  logicalOperator: 'AND' | 'OR'
  conditions: (FilterCondition | FilterGroup)[]
}

FilterCondition

interface FilterCondition {
  propertyName: string
  value: string | number | boolean | Date
  operator: string
}

Component Props

PropTypeDescription
filterPropertiesFilterProperty[]Array of properties that can be filtered
filtersFilterGroupCurrent filter state
onFilterChange(filters: FilterGroup) => voidCallback when filters change
freeformTextstringCurrent free-form search text
onFreeformTextChange(text: string) => voidCallback when free-form text changes
aiApiUrlstring?Optional URL for AI-powered filtering

AI Integration

The Filter Bar component supports AI-powered filtering through an optional API endpoint. When aiApiUrl is provided, the component will send natural language queries to be converted into structured filters.

API Endpoint

The AI API endpoint should accept POST requests with the following structure:

// Request body
interface AIFilterRequest {
  prompt: string // Natural language query
  filterProperties: FilterProperty[] // Available filter properties
}
 
// Response body
interface AIFilterResponse {
  logicalOperator: 'AND' | 'OR'
  conditions: (FilterCondition | FilterGroup)[]
}

Example API Implementation

import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
 
// Define schemas for validation
const FilterProperty = z.object({
  label: z.string(),
  name: z.string(),
  type: z.enum(['string', 'number', 'date', 'boolean']),
  options: z.array(z.string()).optional(),
  operators: z.array(z.string()).optional(),
})
 
const FilterCondition = z.object({
  propertyName: z.string(),
  value: z.union([z.string(), z.number(), z.boolean(), z.null()]),
  operator: z.string(),
})
 
type FilterGroupType = {
  logicalOperator: 'AND' | 'OR'
  conditions: Array<z.infer<typeof FilterCondition> | FilterGroupType>
}
 
const FilterGroup: z.ZodType<FilterGroupType> = z.lazy(() =>
  z.object({
    logicalOperator: z.enum(['AND', 'OR']),
    conditions: z.array(z.union([FilterCondition, FilterGroup])),
  })
)
 
export async function POST(req: Request) {
  const { prompt, filterProperties } = await req.json()
  const filterPropertiesString = JSON.stringify(filterProperties)
 
  try {
    const { object } = await generateObject({
      model: openai('gpt-4-mini'),
      schema: FilterGroup,
      prompt: `Generate a filter group based on the following prompt: "${prompt}". 
              Use only these filter properties: ${filterPropertiesString}. 
              Each property has its own set of valid operators defined in the operators field. 
              Return a filter group with a logical operator ('AND'/'OR') and an array of conditions. 
              Each condition can be either a filter condition or another filter group. 
              Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }. 
              Ensure that the generated filters use only the provided property names and their corresponding operators.`,
    })
 
    // Validate that all propertyNames exist in filterProperties
    const validatePropertyNames = (group: FilterGroupType): boolean => {
      return group.conditions.every((condition) => {
        if ('logicalOperator' in condition) {
          return validatePropertyNames(condition as FilterGroupType)
        }
        const property = filterProperties.find(
          (p: z.infer<typeof FilterProperty>) => p.name === condition.propertyName
        )
        if (!property) return false
        // Validate operator is valid for this property
        return property.operators?.includes(condition.operator) ?? false
      })
    }
 
    if (!validatePropertyNames(object)) {
      throw new Error('Invalid property names or operators in generated filter')
    }
 
    // Zod will throw an error if the object doesn't match the schema
    const validatedFilters = FilterGroup.parse(object)
    return Response.json(validatedFilters)
  } catch (error: any) {
    console.error('Error in AI filtering:', error)
    return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 })
  }
}

Usage with AI

export function FilterDemoWithAI() {
  const [filters, setFilters] = useState<FilterGroup>(initialFilters)
 
  return (
    <FilterBar
      filterProperties={filterProperties}
      filters={filters}
      onFilterChange={setFilters}
      aiApiUrl="/api/filter-ai" // Enable AI filtering
    />
  )
}