@@ -37,6 +37,8 @@ export interface UserFiltersProps {
3737 data ?: any [ ] ;
3838 /** Callback when filter state changes */
3939 onFilterChange : ( filters : any [ ] ) => void ;
40+ /** Maximum visible filter badges before collapsing into "More" dropdown (dropdown mode only) */
41+ maxVisible ?: number ;
4042 className ?: string ;
4143}
4244
@@ -53,6 +55,7 @@ export function UserFilters({
5355 objectDef,
5456 data = [ ] ,
5557 onFilterChange,
58+ maxVisible,
5659 className,
5760} : UserFiltersProps ) {
5861 switch ( config . element ) {
@@ -63,6 +66,7 @@ export function UserFilters({
6366 objectDef = { objectDef }
6467 data = { data }
6568 onFilterChange = { onFilterChange }
69+ maxVisible = { maxVisible }
6670 className = { className }
6771 />
6872 ) ;
@@ -138,10 +142,11 @@ interface DropdownFiltersProps {
138142 objectDef ?: any ;
139143 data : any [ ] ;
140144 onFilterChange : ( filters : any [ ] ) => void ;
145+ maxVisible ?: number ;
141146 className ?: string ;
142147}
143148
144- function DropdownFilters ( { fields, objectDef, data, onFilterChange, className } : DropdownFiltersProps ) {
149+ function DropdownFilters ( { fields, objectDef, data, onFilterChange, maxVisible , className } : DropdownFiltersProps ) {
145150 const [ selectedValues , setSelectedValues ] = React . useState <
146151 Record < string , ( string | number | boolean ) [ ] >
147152 > ( ( ) => {
@@ -182,6 +187,89 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, className }:
182187 // eslint-disable-next-line react-hooks/exhaustive-deps
183188 } , [ ] ) ;
184189
190+ // Split fields into visible and overflow based on maxVisible
191+ const visibleFields = maxVisible !== undefined && maxVisible < resolvedFields . length
192+ ? resolvedFields . slice ( 0 , maxVisible )
193+ : resolvedFields ;
194+ const overflowFields = maxVisible !== undefined && maxVisible < resolvedFields . length
195+ ? resolvedFields . slice ( maxVisible )
196+ : [ ] ;
197+
198+ const renderBadge = ( f : ResolvedField ) => {
199+ const selected = selectedValues [ f . field ] || [ ] ;
200+ const hasSelection = selected . length > 0 ;
201+
202+ return (
203+ < Popover key = { f . field } >
204+ < PopoverTrigger asChild >
205+ < button
206+ data-testid = { `filter-badge-${ f . field } ` }
207+ className = { cn (
208+ 'inline-flex items-center gap-1 rounded-md border h-7 px-2.5 text-xs font-medium transition-colors shrink-0' ,
209+ hasSelection
210+ ? 'border-primary/30 bg-primary/5 text-primary'
211+ : 'border-border bg-background hover:bg-accent text-foreground' ,
212+ ) }
213+ >
214+ < span className = "truncate max-w-[100px]" > { f . label || f . field } </ span >
215+ { hasSelection && (
216+ < span className = "flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px]" >
217+ { selected . length }
218+ </ span >
219+ ) }
220+ { hasSelection ? (
221+ < X
222+ className = "h-3 w-3 opacity-60"
223+ data-testid = { `filter-clear-${ f . field } ` }
224+ onClick = { e => {
225+ e . stopPropagation ( ) ;
226+ handleChange ( f . field , [ ] ) ;
227+ } }
228+ />
229+ ) : (
230+ < ChevronDown className = "h-3 w-3 opacity-60" />
231+ ) }
232+ </ button >
233+ </ PopoverTrigger >
234+ < PopoverContent align = "start" className = "w-56 p-2" >
235+ < div className = "max-h-60 overflow-y-auto space-y-0.5" data-testid = { `filter-options-${ f . field } ` } >
236+ { f . options . map ( opt => (
237+ < label
238+ key = { String ( opt . value ) }
239+ className = { cn (
240+ 'flex items-center gap-2 text-sm py-1.5 px-2 rounded cursor-pointer' ,
241+ selected . includes ( opt . value ) ? 'bg-primary/5 text-primary' : 'hover:bg-muted' ,
242+ ) }
243+ >
244+ < input
245+ type = "checkbox"
246+ checked = { selected . includes ( opt . value ) }
247+ onChange = { ( ) => {
248+ const next = selected . includes ( opt . value )
249+ ? selected . filter ( v => v !== opt . value )
250+ : [ ...selected , opt . value ] ;
251+ handleChange ( f . field , next ) ;
252+ } }
253+ className = "rounded border-input"
254+ />
255+ { opt . color && (
256+ < span
257+ className = "h-2.5 w-2.5 rounded-full shrink-0"
258+ style = { { backgroundColor : opt . color } }
259+ />
260+ ) }
261+ < span className = "truncate flex-1" > { opt . label } </ span >
262+ { opt . count !== undefined && (
263+ < span className = "text-xs text-muted-foreground" > { opt . count } </ span >
264+ ) }
265+ </ label >
266+ ) ) }
267+ </ div >
268+ </ PopoverContent >
269+ </ Popover >
270+ ) ;
271+ } ;
272+
185273 return (
186274 < div className = { cn ( 'flex items-center gap-1 overflow-x-auto' , className ) } data-testid = "user-filters-dropdown" >
187275 < SlidersHorizontal className = "h-3.5 w-3.5 text-muted-foreground shrink-0" />
@@ -190,80 +278,30 @@ function DropdownFilters({ fields, objectDef, data, onFilterChange, className }:
190278 No filter fields
191279 </ span >
192280 ) : (
193- resolvedFields . map ( f => {
194- const selected = selectedValues [ f . field ] || [ ] ;
195- const hasSelection = selected . length > 0 ;
196-
197- return (
198- < Popover key = { f . field } >
281+ < >
282+ { visibleFields . map ( renderBadge ) }
283+ { overflowFields . length > 0 && (
284+ < Popover >
199285 < PopoverTrigger asChild >
200286 < 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- ) }
287+ data-testid = "user-filters-more"
288+ className = "inline-flex items-center gap-1 rounded-md border border-border bg-background hover:bg-accent text-foreground h-7 px-2.5 text-xs font-medium transition-colors shrink-0"
208289 >
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 , [ ] ) ;
222- } }
223- />
224- ) : (
225- < ChevronDown className = "h-3 w-3 opacity-60" />
226- ) }
290+ < span > More</ span >
291+ < span className = "flex h-4 min-w-[16px] items-center justify-center rounded-full bg-muted text-[10px] font-medium" >
292+ { overflowFields . length }
293+ </ span >
294+ < ChevronDown className = "h-3 w-3 opacity-60" />
227295 </ button >
228296 </ 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"
249- />
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- ) ) }
297+ < PopoverContent align = "start" className = "w-64 p-2" data-testid = "user-filters-more-content" >
298+ < div className = "space-y-1" >
299+ { overflowFields . map ( renderBadge ) }
262300 </ div >
263301 </ PopoverContent >
264302 </ Popover >
265- ) ;
266- } )
303+ ) }
304+ </ >
267305 ) }
268306 < button
269307 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"
0 commit comments