Skip to content

Commit ab892aa

Browse files
committed
Update StatusFilters and Dashboard with Chakra UI v3 Combobox and refactored filtering
1 parent ba1147f commit ab892aa

File tree

2 files changed

+169
-128
lines changed

2 files changed

+169
-128
lines changed
Lines changed: 150 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,106 @@
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';
1115
import type { LiveStatusParams } from '@/api/types';
1216
import { MdSearch, MdExpandMore } from 'react-icons/md';
13-
14-
const DEFAULT_GROUPS: string[] = [];
17+
import { useEffect, useState } from 'react';
1518

1619
interface 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

2735
export 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

Comments
 (0)