11'use client'
22
3- import { useCallback , useMemo , useState } from 'react'
3+ import { memo , useCallback , useMemo , useState } from 'react'
44import { X } from 'lucide-react'
55import { nanoid } from 'nanoid'
66import {
@@ -11,29 +11,26 @@ import {
1111 DropdownMenuTrigger ,
1212} from '@/components/emcn'
1313import { ChevronDown , Plus } from '@/components/emcn/icons'
14- import { cn } from '@/lib/core/utils/cn'
1514import type { Filter , FilterRule } from '@/lib/table'
1615import { COMPARISON_OPERATORS } from '@/lib/table/query-builder/constants'
17- import { filterRulesToFilter } from '@/lib/table/query-builder/converters'
18-
19- const OPERATOR_LABELS : Record < string , string > = {
20- eq : '=' ,
21- ne : '≠' ,
22- gt : '>' ,
23- gte : '≥' ,
24- lt : '<' ,
25- lte : '≤' ,
26- contains : '∋' ,
27- in : '∈' ,
28- } as const
16+ import { filterRulesToFilter , filterToRules } from '@/lib/table/query-builder/converters'
17+
18+ const OPERATOR_LABELS = Object . fromEntries (
19+ COMPARISON_OPERATORS . map ( ( op ) => [ op . value , op . label ] )
20+ ) as Record < string , string >
2921
3022interface TableFilterProps {
3123 columns : Array < { name : string ; type : string } >
24+ filter : Filter | null
3225 onApply : ( filter : Filter | null ) => void
26+ onClose : ( ) => void
3327}
3428
35- export function TableFilter ( { columns, onApply } : TableFilterProps ) {
36- const [ rules , setRules ] = useState < FilterRule [ ] > ( ( ) => [ createRule ( columns ) ] )
29+ export function TableFilter ( { columns, filter, onApply, onClose } : TableFilterProps ) {
30+ const [ rules , setRules ] = useState < FilterRule [ ] > ( ( ) => {
31+ const fromFilter = filterToRules ( filter )
32+ return fromFilter . length > 0 ? fromFilter : [ createRule ( columns ) ]
33+ } )
3734
3835 const columnOptions = useMemo (
3936 ( ) => columns . map ( ( col ) => ( { value : col . name , label : col . name } ) ) ,
@@ -46,71 +43,122 @@ export function TableFilter({ columns, onApply }: TableFilterProps) {
4643
4744 const handleRemove = useCallback (
4845 ( id : string ) => {
49- setRules ( ( prev ) => {
50- const next = prev . filter ( ( r ) => r . id !== id )
51- return next . length === 0 ? [ createRule ( columns ) ] : next
52- } )
46+ const next = rules . filter ( ( r ) => r . id !== id )
47+ if ( next . length === 0 ) {
48+ onApply ( null )
49+ onClose ( )
50+ setRules ( [ createRule ( columns ) ] )
51+ } else {
52+ setRules ( next )
53+ }
5354 } ,
54- [ columns ]
55+ [ columns , onApply , onClose , rules ]
5556 )
5657
5758 const handleUpdate = useCallback ( ( id : string , field : keyof FilterRule , value : string ) => {
5859 setRules ( ( prev ) => prev . map ( ( r ) => ( r . id === id ? { ...r , [ field ] : value } : r ) ) )
5960 } , [ ] )
6061
62+ const handleToggleLogical = useCallback ( ( id : string ) => {
63+ setRules ( ( prev ) =>
64+ prev . map ( ( r ) =>
65+ r . id === id ? { ...r , logicalOperator : r . logicalOperator === 'and' ? 'or' : 'and' } : r
66+ )
67+ )
68+ } , [ ] )
69+
6170 const handleApply = useCallback ( ( ) => {
6271 const validRules = rules . filter ( ( r ) => r . column && r . value )
6372 onApply ( filterRulesToFilter ( validRules ) )
6473 } , [ rules , onApply ] )
6574
66- return (
67- < div className = 'flex flex-col gap-1.5 p-2' >
68- { rules . map ( ( rule ) => (
69- < FilterRuleRow
70- key = { rule . id }
71- rule = { rule }
72- columns = { columnOptions }
73- onUpdate = { handleUpdate }
74- onRemove = { handleRemove }
75- onApply = { handleApply }
76- />
77- ) ) }
78-
79- < div className = 'flex items-center justify-between gap-3' >
80- < Button
81- variant = 'ghost'
82- size = 'sm'
83- onClick = { handleAdd }
84- className = { cn (
85- 'border border-[var(--border)] border-dashed px-2 py-[3px] text-[var(--text-secondary)] text-xs'
86- ) }
87- >
88- < Plus className = 'mr-1 h-[10px] w-[10px]' />
89- Add filter
90- </ Button >
75+ const handleClear = useCallback ( ( ) => {
76+ setRules ( [ createRule ( columns ) ] )
77+ onApply ( null )
78+ } , [ columns , onApply ] )
9179
92- < Button variant = 'default' size = 'sm' onClick = { handleApply } className = 'text-xs' >
93- Apply filter
94- </ Button >
80+ return (
81+ < div className = 'border-[var(--border)] border-b bg-[var(--bg)] px-4 py-2' >
82+ < div className = 'flex flex-col gap-1' >
83+ { rules . map ( ( rule , index ) => (
84+ < FilterRuleRow
85+ key = { rule . id }
86+ rule = { rule }
87+ isFirst = { index === 0 }
88+ columns = { columnOptions }
89+ onUpdate = { handleUpdate }
90+ onRemove = { handleRemove }
91+ onApply = { handleApply }
92+ onToggleLogical = { handleToggleLogical }
93+ />
94+ ) ) }
95+
96+ < div className = 'mt-1 flex items-center justify-between' >
97+ < Button
98+ variant = 'ghost'
99+ size = 'sm'
100+ onClick = { handleAdd }
101+ className = 'px-2 py-1 text-[var(--text-secondary)] text-xs'
102+ >
103+ < Plus className = 'mr-1 h-[10px] w-[10px]' />
104+ Add filter
105+ </ Button >
106+ < div className = 'flex items-center gap-1.5' >
107+ { filter !== null && (
108+ < Button
109+ variant = 'ghost'
110+ size = 'sm'
111+ onClick = { handleClear }
112+ className = 'px-2 py-1 text-[var(--text-secondary)] text-xs'
113+ >
114+ Clear filters
115+ </ Button >
116+ ) }
117+ < Button variant = 'default' size = 'sm' onClick = { handleApply } className = 'text-xs' >
118+ Apply filter
119+ </ Button >
120+ </ div >
121+ </ div >
95122 </ div >
96123 </ div >
97124 )
98125}
99126
100127interface FilterRuleRowProps {
101128 rule : FilterRule
129+ isFirst : boolean
102130 columns : Array < { value : string ; label : string } >
103131 onUpdate : ( id : string , field : keyof FilterRule , value : string ) => void
104132 onRemove : ( id : string ) => void
105133 onApply : ( ) => void
134+ onToggleLogical : ( id : string ) => void
106135}
107136
108- function FilterRuleRow ( { rule, columns, onUpdate, onRemove, onApply } : FilterRuleRowProps ) {
137+ const FilterRuleRow = memo ( function FilterRuleRow ( {
138+ rule,
139+ isFirst,
140+ columns,
141+ onUpdate,
142+ onRemove,
143+ onApply,
144+ onToggleLogical,
145+ } : FilterRuleRowProps ) {
109146 return (
110- < div className = 'flex items-center gap-1' >
147+ < div className = 'flex items-center gap-1.5' >
148+ { isFirst ? (
149+ < span className = 'w-[42px] shrink-0 text-right text-[var(--text-muted)] text-xs' > Where</ span >
150+ ) : (
151+ < button
152+ onClick = { ( ) => onToggleLogical ( rule . id ) }
153+ className = 'w-[42px] shrink-0 rounded-full py-0.5 text-right font-medium text-[10px] text-[var(--text-muted)] uppercase tracking-wide transition-colors hover:text-[var(--text-secondary)]'
154+ >
155+ { rule . logicalOperator }
156+ </ button >
157+ ) }
158+
111159 < DropdownMenu >
112160 < DropdownMenuTrigger asChild >
113- < button className = 'flex h-[30px ] min-w-[100px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]' >
161+ < button className = 'flex h-[28px ] min-w-[100px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]' >
114162 < span className = 'truncate' > { rule . column || 'Column' } </ span >
115163 < ChevronDown className = 'ml-1 h-[10px] w-[10px] shrink-0 text-[var(--text-icon)]' />
116164 </ button >
@@ -129,8 +177,8 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul
129177
130178 < DropdownMenu >
131179 < DropdownMenuTrigger asChild >
132- < button className = 'flex h-[30px ] min-w-[50px ] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]' >
133- < span > { OPERATOR_LABELS [ rule . operator ] ?? rule . operator } </ span >
180+ < button className = 'flex h-[28px ] min-w-[90px ] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]' >
181+ < span className = 'truncate' > { OPERATOR_LABELS [ rule . operator ] ?? rule . operator } </ span >
134182 < ChevronDown className = 'ml-1 h-[10px] w-[10px] shrink-0 text-[var(--text-icon)]' />
135183 </ button >
136184 </ DropdownMenuTrigger >
@@ -151,25 +199,21 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul
151199 value = { rule . value }
152200 onChange = { ( e ) => onUpdate ( rule . id , 'value' , e . target . value ) }
153201 onKeyDown = { ( e ) => {
154- if ( e . key === 'Enter' ) handleApply ( )
202+ if ( e . key === 'Enter' ) onApply ( )
155203 } }
156204 placeholder = 'Enter a value'
157- className = 'h-[30px] min-w-[160px ] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]'
205+ className = 'h-[28px ] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]'
158206 />
159207
160208 < button
161209 onClick = { ( ) => onRemove ( rule . id ) }
162- className = 'flex h-[30px ] w-[30px ] shrink-0 items-center justify-center rounded-[5px] text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
210+ className = 'flex h-[28px ] w-[28px ] shrink-0 items-center justify-center rounded-[5px] text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
163211 >
164212 < X className = 'h-[12px] w-[12px]' />
165213 </ button >
166214 </ div >
167215 )
168-
169- function handleApply ( ) {
170- onApply ( )
171- }
172- }
216+ } )
173217
174218function createRule ( columns : Array < { name : string } > ) : FilterRule {
175219 return {
0 commit comments