11'use client'
22
33import { memo , useCallback , useMemo , useRef , useState } from 'react'
4- import { Button , ChipDropdown , ChipInput } from '@sim/emcn'
4+ import { Button , ChipDropdown , ChipInput , cn } from '@sim/emcn'
55import { Plus , X } from '@sim/emcn/icons'
66import { generateShortId } from '@sim/utils/id'
77import type { ColumnDefinition , FilterRule , TablePredicate } from '@/lib/table'
@@ -43,10 +43,18 @@ function isCompleteRule(rule: FilterRule): boolean {
4343interface TableFilterProps {
4444 columns : ColumnDefinition [ ]
4545 filter : TablePredicate | null
46+ autoApply ?: boolean
4647 onChange : ( filter : TablePredicate | null ) => void
48+ onClose ?: ( ) => void
4749}
4850
49- export function TableFilter ( { columns, filter, onChange } : TableFilterProps ) {
51+ export function TableFilter ( {
52+ columns,
53+ filter,
54+ autoApply = false ,
55+ onChange,
56+ onClose,
57+ } : TableFilterProps ) {
5058 const lastAppliedFilterRef = useRef < string | undefined > ( undefined )
5159 const [ rules , setRules ] = useState < FilterRule [ ] > ( ( ) => {
5260 const fromFilter = predicateToFilterRules ( filter )
@@ -67,6 +75,7 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
6775 const nextRules = update ( rulesRef . current )
6876 rulesRef . current = nextRules
6977 setRules ( nextRules )
78+ if ( ! autoApply ) return
7079
7180 const deferredRule = nextRules . find ( ( rule ) => rule . id === deferIncompleteRuleId )
7281 if ( deferredRule && ! isCompleteRule ( deferredRule ) ) return
@@ -77,7 +86,7 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
7786 lastAppliedFilterRef . current = signature
7887 onChange ( nextFilter )
7988 } ,
80- [ columns , onChange ]
89+ [ autoApply , columns , onChange ]
8190 )
8291
8392 // `value` is the filter field key (column id); `label` is what the user sees.
@@ -97,12 +106,26 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
97106
98107 const handleRemove = useCallback (
99108 ( id : string ) => {
109+ if ( ! autoApply ) {
110+ const nextRules = rulesRef . current . filter ( ( rule ) => rule . id !== id )
111+ if ( nextRules . length > 0 ) {
112+ rulesRef . current = nextRules
113+ setRules ( nextRules )
114+ return
115+ }
116+ const resetRules = [ createRule ( columns ) ]
117+ rulesRef . current = resetRules
118+ setRules ( resetRules )
119+ onChange ( null )
120+ onClose ?.( )
121+ return
122+ }
100123 applyRules ( ( current ) => {
101124 const next = current . filter ( ( rule ) => rule . id !== id )
102125 return next . length > 0 ? next : [ createRule ( columns ) ]
103126 } )
104127 } ,
105- [ applyRules , columns ]
128+ [ applyRules , autoApply , columns , onChange , onClose ]
106129 )
107130
108131 const handleUpdate = useCallback (
@@ -156,6 +179,17 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
156179 [ applyRules , columnById ]
157180 )
158181
182+ const handleApply = useCallback ( ( ) => {
183+ onChange ( toAppliedPredicate ( rulesRef . current , columns ) )
184+ } , [ columns , onChange ] )
185+
186+ const handleClear = ( ) => {
187+ const resetRules = [ createRule ( columns ) ]
188+ rulesRef . current = resetRules
189+ setRules ( resetRules )
190+ onChange ( null )
191+ }
192+
159193 return (
160194 < div className = 'border-[var(--border)] border-b bg-[var(--bg)] px-4 py-2' >
161195 < div className = 'flex flex-col gap-1' >
@@ -169,11 +203,13 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
169203 onUpdate = { handleUpdate }
170204 onColumnChange = { handleColumnChange }
171205 onRemove = { handleRemove }
206+ autoApply = { autoApply }
207+ onApply = { handleApply }
172208 onToggleLogical = { handleToggleLogical }
173209 />
174210 ) ) }
175211
176- < div className = 'mt-1 flex items-center' >
212+ < div className = { cn ( 'mt-1 flex items-center' , ! autoApply && 'justify-between' ) } >
177213 < Button
178214 variant = 'ghost'
179215 size = 'sm'
@@ -183,6 +219,23 @@ export function TableFilter({ columns, filter, onChange }: TableFilterProps) {
183219 < Plus className = 'mr-1 size-[10px]' />
184220 Add filter
185221 </ Button >
222+ { ! autoApply && (
223+ < div className = 'flex items-center gap-1.5' >
224+ { filter !== null && (
225+ < Button
226+ variant = 'ghost'
227+ size = 'sm'
228+ onClick = { handleClear }
229+ className = 'px-2 py-1 text-[var(--text-secondary)] text-xs'
230+ >
231+ Clear filters
232+ </ Button >
233+ ) }
234+ < Button variant = 'default' size = 'sm' onClick = { handleApply } className = 'text-xs' >
235+ Apply filter
236+ </ Button >
237+ </ div >
238+ ) }
186239 </ div >
187240 </ div >
188241 </ div >
@@ -197,6 +250,8 @@ interface FilterRuleRowProps {
197250 onUpdate : ( id : string , field : keyof FilterRule , value : string ) => void
198251 onColumnChange : ( id : string , columnId : string ) => void
199252 onRemove : ( id : string ) => void
253+ autoApply : boolean
254+ onApply : ( ) => void
200255 onToggleLogical : ( id : string ) => void
201256}
202257
@@ -208,6 +263,8 @@ const FilterRuleRow = memo(function FilterRuleRow({
208263 onUpdate,
209264 onColumnChange,
210265 onRemove,
266+ autoApply,
267+ onApply,
211268 onToggleLogical,
212269} : FilterRuleRowProps ) {
213270 // Keep a stale column id selectable/visible (e.g. after the column was
@@ -281,11 +338,21 @@ const FilterRuleRow = memo(function FilterRuleRow({
281338 matchTriggerWidth = { false }
282339 className = 'min-w-[100px] flex-1'
283340 />
284- ) : (
341+ ) : autoApply ? (
285342 < FilterValueInput
286343 value = { rule . value }
287344 onCommit = { ( value ) => onUpdate ( rule . id , 'value' , value ) }
288345 />
346+ ) : (
347+ < ChipInput
348+ value = { rule . value }
349+ onChange = { ( event ) => onUpdate ( rule . id , 'value' , event . target . value ) }
350+ onKeyDown = { ( event ) => {
351+ if ( event . key === 'Enter' ) onApply ( )
352+ } }
353+ placeholder = 'Enter a value'
354+ className = 'flex-1'
355+ />
289356 ) }
290357
291358 < Button
0 commit comments