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
Prop | Type | Description |
---|---|---|
filterProperties | FilterProperty[] | Array of properties that can be filtered |
filters | FilterGroup | Current filter state |
onFilterChange | (filters: FilterGroup) => void | Callback when filters change |
freeformText | string | Current free-form search text |
onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
aiApiUrl | string? | 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
/>
)
}