|
8 | 8 |
|
9 | 9 | import * as React from 'react'; |
10 | 10 | import { cn, Button, Popover, PopoverContent, PopoverTrigger } from '@object-ui/components'; |
11 | | -import { ChevronDown, X, Plus } from 'lucide-react'; |
| 11 | +import { ChevronDown, X, Plus, SlidersHorizontal } from 'lucide-react'; |
12 | 12 | import type { ListViewSchema } from '@object-ui/types'; |
13 | 13 |
|
14 | 14 | /** Resolved option with optional count */ |
@@ -184,80 +184,95 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, className }: |
184 | 184 |
|
185 | 185 | return ( |
186 | 186 | <div className={cn('flex items-center gap-1 overflow-x-auto', className)} data-testid="user-filters-dropdown"> |
187 | | - {resolvedFields.map(f => { |
188 | | - const selected = selectedValues[f.field] || []; |
189 | | - const hasSelection = selected.length > 0; |
| 187 | + <SlidersHorizontal className="h-3.5 w-3.5 text-muted-foreground shrink-0" /> |
| 188 | + {resolvedFields.length === 0 ? ( |
| 189 | + <span className="text-xs text-muted-foreground" data-testid="user-filters-empty"> |
| 190 | + No filter fields |
| 191 | + </span> |
| 192 | + ) : ( |
| 193 | + resolvedFields.map(f => { |
| 194 | + const selected = selectedValues[f.field] || []; |
| 195 | + const hasSelection = selected.length > 0; |
190 | 196 |
|
191 | | - return ( |
192 | | - <Popover key={f.field}> |
193 | | - <PopoverTrigger asChild> |
194 | | - <button |
195 | | - data-testid={`filter-badge-${f.field}`} |
196 | | - className={cn( |
197 | | - 'inline-flex items-center gap-1 rounded-md border h-7 px-2.5 text-xs font-medium transition-colors shrink-0', |
198 | | - hasSelection |
199 | | - ? 'border-primary/30 bg-primary/5 text-primary' |
200 | | - : 'border-border bg-background hover:bg-accent text-foreground', |
201 | | - )} |
202 | | - > |
203 | | - <span className="truncate max-w-[100px]">{f.label || f.field}</span> |
204 | | - {hasSelection && ( |
205 | | - <span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px]"> |
206 | | - {selected.length} |
207 | | - </span> |
208 | | - )} |
209 | | - {hasSelection ? ( |
210 | | - <X |
211 | | - className="h-3 w-3 opacity-60" |
212 | | - data-testid={`filter-clear-${f.field}`} |
213 | | - onClick={e => { |
214 | | - e.stopPropagation(); |
215 | | - handleChange(f.field, []); |
216 | | - }} |
217 | | - /> |
218 | | - ) : ( |
219 | | - <ChevronDown className="h-3 w-3 opacity-60" /> |
220 | | - )} |
221 | | - </button> |
222 | | - </PopoverTrigger> |
223 | | - <PopoverContent align="start" className="w-56 p-2"> |
224 | | - <div className="max-h-60 overflow-y-auto space-y-0.5" data-testid={`filter-options-${f.field}`}> |
225 | | - {f.options.map(opt => ( |
226 | | - <label |
227 | | - key={String(opt.value)} |
228 | | - className={cn( |
229 | | - 'flex items-center gap-2 text-sm py-1.5 px-2 rounded cursor-pointer', |
230 | | - selected.includes(opt.value) ? 'bg-primary/5 text-primary' : 'hover:bg-muted', |
231 | | - )} |
232 | | - > |
233 | | - <input |
234 | | - type="checkbox" |
235 | | - checked={selected.includes(opt.value)} |
236 | | - onChange={() => { |
237 | | - const next = selected.includes(opt.value) |
238 | | - ? selected.filter(v => v !== opt.value) |
239 | | - : [...selected, opt.value]; |
240 | | - handleChange(f.field, next); |
| 197 | + return ( |
| 198 | + <Popover key={f.field}> |
| 199 | + <PopoverTrigger asChild> |
| 200 | + <button |
| 201 | + data-testid={`filter-badge-${f.field}`} |
| 202 | + className={cn( |
| 203 | + 'inline-flex items-center gap-1 rounded-md border h-7 px-2.5 text-xs font-medium transition-colors shrink-0', |
| 204 | + hasSelection |
| 205 | + ? 'border-primary/30 bg-primary/5 text-primary' |
| 206 | + : 'border-border bg-background hover:bg-accent text-foreground', |
| 207 | + )} |
| 208 | + > |
| 209 | + <span className="truncate max-w-[100px]">{f.label || f.field}</span> |
| 210 | + {hasSelection && ( |
| 211 | + <span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px]"> |
| 212 | + {selected.length} |
| 213 | + </span> |
| 214 | + )} |
| 215 | + {hasSelection ? ( |
| 216 | + <X |
| 217 | + className="h-3 w-3 opacity-60" |
| 218 | + data-testid={`filter-clear-${f.field}`} |
| 219 | + onClick={e => { |
| 220 | + e.stopPropagation(); |
| 221 | + handleChange(f.field, []); |
241 | 222 | }} |
242 | | - className="rounded border-input" |
243 | 223 | /> |
244 | | - {opt.color && ( |
245 | | - <span |
246 | | - className="h-2.5 w-2.5 rounded-full shrink-0" |
247 | | - style={{ backgroundColor: opt.color }} |
| 224 | + ) : ( |
| 225 | + <ChevronDown className="h-3 w-3 opacity-60" /> |
| 226 | + )} |
| 227 | + </button> |
| 228 | + </PopoverTrigger> |
| 229 | + <PopoverContent align="start" className="w-56 p-2"> |
| 230 | + <div className="max-h-60 overflow-y-auto space-y-0.5" data-testid={`filter-options-${f.field}`}> |
| 231 | + {f.options.map(opt => ( |
| 232 | + <label |
| 233 | + key={String(opt.value)} |
| 234 | + className={cn( |
| 235 | + 'flex items-center gap-2 text-sm py-1.5 px-2 rounded cursor-pointer', |
| 236 | + selected.includes(opt.value) ? 'bg-primary/5 text-primary' : 'hover:bg-muted', |
| 237 | + )} |
| 238 | + > |
| 239 | + <input |
| 240 | + type="checkbox" |
| 241 | + checked={selected.includes(opt.value)} |
| 242 | + onChange={() => { |
| 243 | + const next = selected.includes(opt.value) |
| 244 | + ? selected.filter(v => v !== opt.value) |
| 245 | + : [...selected, opt.value]; |
| 246 | + handleChange(f.field, next); |
| 247 | + }} |
| 248 | + className="rounded border-input" |
248 | 249 | /> |
249 | | - )} |
250 | | - <span className="truncate flex-1">{opt.label}</span> |
251 | | - {opt.count !== undefined && ( |
252 | | - <span className="text-xs text-muted-foreground">{opt.count}</span> |
253 | | - )} |
254 | | - </label> |
255 | | - ))} |
256 | | - </div> |
257 | | - </PopoverContent> |
258 | | - </Popover> |
259 | | - ); |
260 | | - })} |
| 250 | + {opt.color && ( |
| 251 | + <span |
| 252 | + className="h-2.5 w-2.5 rounded-full shrink-0" |
| 253 | + style={{ backgroundColor: opt.color }} |
| 254 | + /> |
| 255 | + )} |
| 256 | + <span className="truncate flex-1">{opt.label}</span> |
| 257 | + {opt.count !== undefined && ( |
| 258 | + <span className="text-xs text-muted-foreground">{opt.count}</span> |
| 259 | + )} |
| 260 | + </label> |
| 261 | + ))} |
| 262 | + </div> |
| 263 | + </PopoverContent> |
| 264 | + </Popover> |
| 265 | + ); |
| 266 | + }) |
| 267 | + )} |
| 268 | + <button |
| 269 | + className="inline-flex items-center gap-1 h-7 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors shrink-0" |
| 270 | + data-testid="user-filters-add" |
| 271 | + title="Add filter" |
| 272 | + > |
| 273 | + <Plus className="h-3.5 w-3.5" /> |
| 274 | + <span className="hidden sm:inline">Add filter</span> |
| 275 | + </button> |
261 | 276 | </div> |
262 | 277 | ); |
263 | 278 | } |
|
0 commit comments