1- import {
2- Box ,
3- Button ,
4- Flex ,
5- HStack ,
6- Icon ,
7- Input ,
8- Text ,
9- Menu
10- } from '@chakra-ui/react' ; import { X } from 'lucide-react' ;
1+ import {
2+ Box ,
3+ Button ,
4+ Flex ,
5+ HStack ,
6+ Icon ,
7+ Input ,
8+ Text ,
9+ Menu ,
10+ Combobox ,
11+ useFilter ,
12+ useListCollection ,
13+ } from '@chakra-ui/react' ;
14+ import { X } from 'lucide-react' ;
1115import type { LiveStatusParams } from '@/api/types' ;
1216import { MdSearch , MdExpandMore } from 'react-icons/md' ;
13-
14- const DEFAULT_GROUPS : string [ ] = [ ] ;
17+ import { useEffect , useState } from 'react' ;
1518
1619interface StatusFiltersProps {
1720 filters : LiveStatusParams ;
18- onFiltersChange : ( filters : LiveStatusParams ) => void ;
19- groups ?: string [ ] ;
21+ onFiltersChange : ( filters : LiveStatusParams & { group ?: string } ) => void ; // single group
22+ groups ?: {
23+ id : string ;
24+ name : string ;
25+ } [ ] ;
26+ selectedGroup ?: string ; // single
27+ onSelectedGroupChange ?: ( group : string | undefined ) => void ; // single
2028 onGroupByChange ?: ( groupBy : string ) => void ;
2129 groupByOptions ?: string [ ] ;
2230 searchTerm ?: string ;
23- onSearchChange ?: ( searchTerm : string ) => void ;
31+ onSearchChange ?: ( search : string ) => void ;
2432 onToggleGroupBy ?: ( groupBy : string , isSelected : boolean ) => void ;
2533}
2634
2735export function StatusFilters ( {
2836 filters,
2937 onFiltersChange,
30- onGroupByChange,
31- groups = DEFAULT_GROUPS ,
38+ groups = [ ] ,
3239 groupByOptions = [ ] ,
3340 searchTerm = '' ,
3441 onSearchChange,
3542 onToggleGroupBy,
43+ selectedGroup,
44+ onSelectedGroupChange,
3645} : StatusFiltersProps ) {
46+ const [ cleared , setCleared ] = useState ( false ) ;
47+
48+ const { contains } = useFilter ( { sensitivity : 'base' } ) ;
49+ const { collection, filter, set } = useListCollection < {
50+ label : string ;
51+ value : string ;
52+ } > ( {
53+ initialItems : [ ] , // start empty
54+ itemToString : item => item . label ,
55+ itemToValue : item => item . value ,
56+ filter : contains ,
57+ } ) ;
58+
59+ useEffect ( ( ) => {
60+ const newItems = groups . map ( g => ( {
61+ label : g . name ,
62+ value : g . id ,
63+ } ) ) ;
64+
65+ set ( newItems ) ;
66+ } , [ groups , set , cleared ] ) ;
67+
3768 const handleGroupChange = ( value : string ) => {
69+ const newGroup = value || undefined ;
70+ onSelectedGroupChange ?.( newGroup ) ;
3871 onFiltersChange ( {
3972 ...filters ,
40- group : value || undefined ,
41- page : 1 , // Reset to first page when filtering
73+ group : newGroup ,
4274 } ) ;
4375 } ;
4476
4577 const handleSearchChange = ( value : string ) => {
4678 // Update search term in parent component
4779 onSearchChange && onSearchChange ( value ) ;
48-
80+
4981 // Optionally reset page when searching
5082 onFiltersChange ( {
5183 ...filters ,
5284 search : value || undefined ,
53- page : 1 ,
5485 } ) ;
5586 } ;
5687
5788 const clearSearch = ( ) => {
58- onSearchChange && onSearchChange ( '' ) ;
59- onFiltersChange ( {
60- ...filters ,
61- search : undefined ,
62- page : 1 ,
63- } ) ;
89+ onSearchChange ?.( '' ) ;
90+ // Optional: reset local cleared state
91+ setCleared ( true ) ;
6492 } ;
6593
66- const clearFilters = ( ) => {
67- onSearchChange && onSearchChange ( '' ) ;
94+ const clearFilter = ( ) => {
6895 onFiltersChange ( {
69- page : 1 ,
70- pageSize : filters . pageSize ,
96+ ...filters ,
97+ group : undefined ,
98+ search : undefined ,
7199 } ) ;
100+ onSelectedGroupChange ?.( undefined ) ;
101+ setCleared ( true ) ;
72102 } ;
73103
74- const hasFilters = filters . group || filters . search ;
75-
76104 return (
77105 < Box
78106 position = { { base : 'sticky' , md : 'static' } }
@@ -87,46 +115,70 @@ export function StatusFilters({
87115 px = { { base : 4 , md : 0 } }
88116 mx = { { base : - 4 , md : 0 } }
89117 borderBottom = { { base : '1px' , md : 'none' } }
90- borderColor = { {
91- base : 'gray.200' ,
118+ borderColor = { {
119+ base : 'gray.200' ,
92120 md : 'transparent' ,
93- _dark : { base : 'gray.700' , md : 'transparent' }
121+ _dark : { base : 'gray.700' , md : 'transparent' } ,
94122 } }
95123 data-testid = 'status-filters'
96124 >
97125 < HStack gap = { { base : 2 , md : 4 } } align = 'center' flexWrap = { { base : 'wrap' , md : 'nowrap' } } >
98126 { /* Group Filter */ }
99- < Menu . Root >
100- < Menu . Trigger asChild >
101- < Button variant = 'outline' borderColor = 'gray.300' >
102- { filters . group || 'All Groups' }
103- < MdExpandMore />
104- </ Button >
105- </ Menu . Trigger >
106- < Menu . Positioner >
107- < Menu . Content >
108- < Menu . Item value = { '' } onSelect = { ( ) => handleGroupChange ( '' ) } >
109- All Groups
110- </ Menu . Item >
111- { groups . map ( group => (
112- < Menu . Item key = { group } value = { group } onSelect = { ( ) => handleGroupChange ( group ) } >
113- { group }
114- </ Menu . Item >
115- ) ) }
116- </ Menu . Content >
117- </ Menu . Positioner >
118- </ Menu . Root >
119-
127+ < Box w = { '25%' } >
128+ < Combobox . Root
129+ size = 'md'
130+ collection = { collection }
131+ value = { selectedGroup ? [ selectedGroup ] : [ ] }
132+ onValueChange = { e => {
133+ handleGroupChange ( e . value [ 0 ] ) ;
134+ setCleared ( false ) ;
135+ } }
136+ onInputValueChange = { e => {
137+ filter ( e . inputValue ) ;
138+ } }
139+ onOpenChange = { open => {
140+ if ( open ) filter ( '' ) ;
141+ } }
142+ openOnClick
143+ >
144+ < Combobox . Control >
145+ < Combobox . Input placeholder = 'Select Group...' />
146+ < Combobox . IndicatorGroup >
147+ < Combobox . ClearTrigger onClick = { clearFilter } />
148+ < Combobox . Trigger />
149+ </ Combobox . IndicatorGroup >
150+ </ Combobox . Control >
151+ < Combobox . Positioner >
152+ < Combobox . Content >
153+ < Combobox . Empty > No groups found</ Combobox . Empty >
154+ < Combobox . Item key = 'allgroups' item = { { label : 'All Groups' , value : '' } } >
155+ < HStack justify = 'space-between' textStyle = 'sm' >
156+ All Groups
157+ </ HStack >
158+ </ Combobox . Item >
159+ { collection . items . map ( item => {
160+ return (
161+ < Combobox . Item key = { item . value } item = { item } >
162+ < HStack justify = 'space-between' textStyle = 'sm' >
163+ { item . label }
164+ </ HStack >
165+ </ Combobox . Item >
166+ ) ;
167+ } ) }
168+ </ Combobox . Content >
169+ </ Combobox . Positioner >
170+ </ Combobox . Root >
171+ </ Box >
120172 { /* Search Input */ }
121173 < Flex w = '80' position = 'relative' align = 'center' >
122- < Icon
123- as = { MdSearch }
124- color = 'gray.400'
125- position = 'absolute'
126- left = '3'
127- top = '50%'
128- transform = 'translateY(-50%)'
129- zIndex = { 2 }
174+ < Icon
175+ as = { MdSearch }
176+ color = 'gray.400'
177+ position = 'absolute'
178+ left = '3'
179+ top = '50%'
180+ transform = 'translateY(-50%)'
181+ zIndex = { 2 }
130182 fontSize = { '18px' }
131183 />
132184 < Input
@@ -158,41 +210,43 @@ export function StatusFilters({
158210 { /* Group By Dropdown */ }
159211 < Menu . Root >
160212 < Menu . Trigger asChild >
161- < Button
162- variant = "outline"
163- borderColor = "gray.300"
164- >
165- { groupByOptions . length > 0
166- ? groupByOptions . map ( opt =>
167- opt === 'status' ? 'Status' :
168- opt === 'group' ? 'Group' : opt
169- ) . join ( ' + ' )
213+ < Button variant = 'outline' borderColor = 'gray.300' >
214+ { groupByOptions . length > 0
215+ ? groupByOptions
216+ . map ( opt => ( opt === 'status' ? 'Status' : opt === 'group' ? 'Group' : opt ) )
217+ . join ( ' + ' )
170218 : 'Group By' }
171219 < MdExpandMore />
172220 </ Button >
173221 </ Menu . Trigger >
174222 < Menu . Positioner px = { 4 } >
175- < Menu . Content minWidth = "200px" borderColor = "gray.300" >
176- < Flex justify = "flex-end" px = { 2 } py = { 1 } borderBottom = "1px solid" borderColor = "gray.200" >
223+ < Menu . Content minWidth = '200px' borderColor = 'gray.300' >
224+ < Flex
225+ justify = 'flex-end'
226+ px = { 2 }
227+ py = { 1 }
228+ borderBottom = '1px solid'
229+ borderColor = 'gray.200'
230+ >
177231 < HStack gap = { 2 } >
178- < Button
179- size = "xs"
180- variant = " ghost"
232+ < Button
233+ size = 'xs'
234+ variant = ' ghost'
181235 onClick = { ( ) => {
182- [ 'status' , 'group' ] . forEach ( opt =>
183- onToggleGroupBy && onToggleGroupBy ( opt , true )
236+ [ 'status' , 'group' ] . forEach (
237+ opt => onToggleGroupBy && onToggleGroupBy ( opt , true )
184238 ) ;
185239 } }
186240 textDecoration = { 'underline' }
187241 >
188242 Select All
189243 </ Button >
190- < Button
191- size = "xs"
192- variant = " ghost"
244+ < Button
245+ size = 'xs'
246+ variant = ' ghost'
193247 onClick = { ( ) => {
194- [ 'status' , 'group' ] . forEach ( opt =>
195- onToggleGroupBy && onToggleGroupBy ( opt , false )
248+ [ 'status' , 'group' ] . forEach (
249+ opt => onToggleGroupBy && onToggleGroupBy ( opt , false )
196250 ) ;
197251 } }
198252 textDecoration = { 'underline' }
@@ -201,63 +255,39 @@ export function StatusFilters({
201255 </ Button >
202256 </ HStack >
203257 </ Flex >
204- < Menu . ItemGroup >
258+ < Menu . ItemGroup >
205259 < Menu . CheckboxItem
206260 cursor = { 'pointer' }
207- value = " status"
261+ value = ' status'
208262 checked = { groupByOptions . includes ( 'status' ) }
209263 onCheckedChange = { ( ) => {
210- onToggleGroupBy && onToggleGroupBy ( 'status' , ! groupByOptions . includes ( 'status' ) ) ;
264+ onToggleGroupBy &&
265+ onToggleGroupBy ( 'status' , ! groupByOptions . includes ( 'status' ) ) ;
211266 } }
212267 >
213- < Flex
214- w = "full"
215- justify = "flex-start"
216- align = "center"
217- gap = { 3 }
218- >
219- < Text as = "span" > Group by Status</ Text >
268+ < Flex w = 'full' justify = 'flex-start' align = 'center' gap = { 3 } >
269+ < Text as = 'span' > Group by Status</ Text >
220270 < Menu . ItemIndicator />
221271 </ Flex >
222272 </ Menu . CheckboxItem >
223273 < Menu . CheckboxItem
224274 cursor = { 'pointer' }
225- value = " group"
275+ value = ' group'
226276 checked = { groupByOptions . includes ( 'group' ) }
227277 onCheckedChange = { ( ) => {
228278 onToggleGroupBy && onToggleGroupBy ( 'group' , ! groupByOptions . includes ( 'group' ) ) ;
229279 } }
230280 >
231- < Flex
232- w = "full"
233- justify = "flex-start"
234- align = "center"
235- gap = { 3 }
236- >
237- < Text as = "span" > Group by Group</ Text >
281+ < Flex w = 'full' justify = 'flex-start' align = 'center' gap = { 3 } >
282+ < Text as = 'span' > Group by Group</ Text >
238283 < Menu . ItemIndicator />
239284 </ Flex >
240285 </ Menu . CheckboxItem >
241286 </ Menu . ItemGroup >
242287 </ Menu . Content >
243288 </ Menu . Positioner >
244289 </ Menu . Root >
245-
246- { /* Clear Filters */ }
247- { hasFilters && (
248- < Button
249- variant = 'outline'
250- size = { { base : 'sm' , md : 'md' } }
251- onClick = { clearFilters }
252- data-testid = 'clear-filters'
253- minHeight = '44px'
254- flexShrink = { 0 }
255- >
256- < X size = { 16 } />
257- Clear
258- </ Button >
259- ) }
260290 </ HStack >
261291 </ Box >
262292 ) ;
263- }
293+ }
0 commit comments