import { Plus, X, WarningCircle } from '@phosphor-icons/react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { cn } from '@/lib/utils' import type { FilterCondition, FilterOp, FilterGroup, FilterNode } from '../types' import { FilterFieldCombobox } from './FilterFieldCombobox' import { DateValueInput } from './filter-builder/DateValueInput' const OPERATORS: { value: FilterOp; label: string }[] = [ { value: 'equals', label: 'equals' }, { value: 'not_equals', label: 'does not equal' }, { value: 'contains', label: 'contains' }, { value: 'not_contains', label: 'does not contain' }, { value: 'is_empty', label: 'is empty' }, { value: 'is_not_empty', label: 'is not empty' }, { value: 'before', label: 'before' }, { value: 'after', label: 'after' }, ] const NO_VALUE_OPS = new Set(['is_empty', 'is_not_empty']) const DATE_OPS = new Set(['before', 'after']) const REGEX_OPS = new Set(['contains', 'not_contains', 'equals', 'not_equals']) function supportsRegex(op: FilterOp): boolean { return REGEX_OPS.has(op) } function normalizeRegexFlag(op: FilterOp, enabled: boolean): boolean | undefined { return supportsRegex(op) && enabled ? true : undefined } function hasInvalidRegex(value: string, regexEnabled: boolean): boolean { if (!regexEnabled) return false try { new RegExp(value, 'i') return false } catch { return true } } function isFilterGroup(node: FilterNode): node is FilterGroup { return 'all' in node || 'any' in node } function getGroupChildren(group: FilterGroup): FilterNode[] { return 'all' in group ? group.all : group.any } function getGroupMode(group: FilterGroup): 'all' | 'any' { return 'all' in group ? 'all' : 'any' } function setGroupChildren(mode: 'all' | 'any', children: FilterNode[]): FilterGroup { return mode === 'all' ? { all: children } : { any: children } } function OperatorSelect({ value, onChange }: { value: FilterOp onChange: (v: FilterOp) => void }) { return ( ) } function TextValueInput({ value, onChange, regexEnabled, regexSupported, invalidRegex, onToggleRegex }: { value: string onChange: (v: string) => void regexEnabled: boolean regexSupported: boolean invalidRegex: boolean onToggleRegex: () => void }) { return (
onChange(e.target.value)} data-testid="filter-value-input" aria-invalid={invalidRegex || undefined} /> {invalidRegex && (
{regexSupported && ( )}
) } function FilterRow({ condition, fields, onUpdate, onRemove }: { condition: FilterCondition fields: string[] onUpdate: (c: FilterCondition) => void onRemove: () => void }) { const isDateOp = DATE_OPS.has(condition.op) const regexSupported = supportsRegex(condition.op) const regexEnabled = regexSupported && condition.regex === true const invalidRegex = regexSupported && hasInvalidRegex(String(condition.value ?? ''), regexEnabled) return (
onUpdate({ ...condition, field: v })} /> onUpdate({ ...condition, op, regex: normalizeRegexFlag(op, regexEnabled) })} /> {!NO_VALUE_OPS.has(condition.op) && ( isDateOp ? onUpdate({ ...condition, value: v })} /> : ( onUpdate({ ...condition, value: v })} regexEnabled={regexEnabled} regexSupported={regexSupported} invalidRegex={invalidRegex} onToggleRegex={() => onUpdate({ ...condition, regex: regexEnabled ? undefined : true })} /> ) )}
) } function FilterGroupView({ group, fields, depth, onChange, onRemove }: { group: FilterGroup fields: string[] depth: number onChange: (g: FilterGroup) => void onRemove?: () => void }) { const mode = getGroupMode(group) const children = getGroupChildren(group) const toggleMode = () => { onChange(setGroupChildren(mode === 'all' ? 'any' : 'all', children)) } const updateChild = (index: number, node: FilterNode) => { const next = [...children] next[index] = node onChange(setGroupChildren(mode, next)) } const removeChild = (index: number) => { const next = children.filter((_, i) => i !== index) onChange(setGroupChildren(mode, next)) } const addCondition = () => { onChange(setGroupChildren(mode, [...children, { field: fields[0] ?? 'type', op: 'equals' as FilterOp, value: '' }])) } const addGroup = () => { const nested: FilterGroup = { all: [{ field: fields[0] ?? 'type', op: 'equals' as FilterOp, value: '' }] } onChange(setGroupChildren(mode, [...children, nested])) } return (
0 ? 'ml-3 border-l-2 border-border pl-3 py-1' : ''}>
{mode === 'all' ? 'Match all conditions' : 'Match any condition'} {onRemove && ( )}
{children.map((child, i) => isFilterGroup(child) ? ( updateChild(i, g)} onRemove={() => removeChild(i)} /> ) : ( updateChild(i, c)} onRemove={() => removeChild(i)} /> ) )}
) } export interface FilterBuilderProps { group: FilterGroup onChange: (group: FilterGroup) => void availableFields: string[] } export function FilterBuilder({ group, onChange, availableFields }: FilterBuilderProps) { const fields = availableFields.length > 0 ? availableFields : ['type'] return ( ) }