77 */
88
99import * as React from 'react' ;
10- import { cn , Button , Input } from '@object-ui/components' ;
10+ import { cn , Button , Input , Popover , PopoverContent , PopoverTrigger , FilterBuilder } from '@object-ui/components' ;
1111import { Search , SlidersHorizontal , ArrowUpDown , X } from 'lucide-react' ;
12+ import type { FilterGroup } from '@object-ui/components' ;
1213import { ViewSwitcher , ViewType } from './ViewSwitcher' ;
1314import { SchemaRenderer } from '@object-ui/react' ;
1415import type { ListViewSchema } from '@object-ui/types' ;
@@ -23,6 +24,39 @@ export interface ListViewProps {
2324 [ key : string ] : any ;
2425}
2526
27+ // Helper to convert FilterBuilder group to ObjectStack AST
28+ function mapOperator ( op : string ) {
29+ switch ( op ) {
30+ case 'equals' : return '=' ;
31+ case 'notEquals' : return '!=' ;
32+ case 'contains' : return 'contains' ;
33+ case 'notContains' : return 'notcontains' ;
34+ case 'greaterThan' : return '>' ;
35+ case 'greaterOrEqual' : return '>=' ;
36+ case 'lessThan' : return '<' ;
37+ case 'lessOrEqual' : return '<=' ;
38+ case 'in' : return 'in' ;
39+ case 'notIn' : return 'not in' ;
40+ case 'before' : return '<' ;
41+ case 'after' : return '>' ;
42+ default : return '=' ;
43+ }
44+ }
45+
46+ function convertFilterGroupToAST ( group : FilterGroup ) : any [ ] {
47+ if ( ! group || ! group . conditions || group . conditions . length === 0 ) return [ ] ;
48+
49+ const conditions = group . conditions . map ( c => {
50+ if ( c . operator === 'isEmpty' ) return [ c . field , '=' , null ] ;
51+ if ( c . operator === 'isNotEmpty' ) return [ c . field , '!=' , null ] ;
52+ return [ c . field , mapOperator ( c . operator ) , c . value ] ;
53+ } ) ;
54+
55+ if ( conditions . length === 1 ) return conditions [ 0 ] ;
56+
57+ return [ group . logic , ...conditions ] ;
58+ }
59+
2660export const ListView : React . FC < ListViewProps > = ( {
2761 schema,
2862 className,
@@ -40,17 +74,42 @@ export const ListView: React.FC<ListViewProps> = ({
4074 const [ sortOrder , setSortOrder ] = React . useState < 'asc' | 'desc' > ( schema . sort ?. [ 0 ] ?. order || 'asc' ) ;
4175 const [ showFilters , setShowFilters ] = React . useState ( false ) ;
4276
77+ const [ currentFilters , setCurrentFilters ] = React . useState < FilterGroup > ( {
78+ id : 'root' ,
79+ logic : 'and' ,
80+ conditions : [ ]
81+ } ) ;
82+
4383 // Data State
4484 const dataSource = props . dataSource ;
4585 const [ data , setData ] = React . useState < any [ ] > ( [ ] ) ;
4686 const [ loading , setLoading ] = React . useState ( false ) ;
87+ const [ objectDef , setObjectDef ] = React . useState < any > ( null ) ;
4788
4889 const storageKey = React . useMemo ( ( ) => {
4990 return schema . id
5091 ? `listview-${ schema . objectName } -${ schema . id } -view`
5192 : `listview-${ schema . objectName } -view` ;
5293 } , [ schema . objectName , schema . id ] ) ;
5394
95+ // Fetch object definition
96+ React . useEffect ( ( ) => {
97+ let isMounted = true ;
98+ const fetchObjectDef = async ( ) => {
99+ if ( ! dataSource || ! schema . objectName ) return ;
100+ try {
101+ const def = await dataSource . getObjectSchema ( schema . objectName ) ;
102+ if ( isMounted ) {
103+ setObjectDef ( def ) ;
104+ }
105+ } catch ( err ) {
106+ console . warn ( "Failed to fetch object schema for ListView:" , err ) ;
107+ }
108+ } ;
109+ fetchObjectDef ( ) ;
110+ return ( ) => { isMounted = false ; } ;
111+ } , [ schema . objectName , dataSource ] ) ;
112+
54113 // Fetch data effect
55114 React . useEffect ( ( ) => {
56115 let isMounted = true ;
@@ -61,16 +120,25 @@ export const ListView: React.FC<ListViewProps> = ({
61120 setLoading ( true ) ;
62121 try {
63122 // Construct filter
64- let filter : any = schema . filters || [ ] ;
65- // TODO: Merge with searchTerm and user filters
66- // For now, we rely on the backend/driver to handle $filter
123+ let finalFilter : any = [ ] ;
124+ const baseFilter = schema . filters || [ ] ;
125+ const userFilter = convertFilterGroupToAST ( currentFilters ) ;
126+
127+ // Merge base filters and user filters
128+ if ( baseFilter . length > 0 && userFilter . length > 0 ) {
129+ finalFilter = [ 'and' , baseFilter , userFilter ] ;
130+ } else if ( userFilter . length > 0 ) {
131+ finalFilter = userFilter ;
132+ } else {
133+ finalFilter = baseFilter ;
134+ }
67135
68136 // Convert sort to query format
69137 // ObjectQL uses simple object: { field: 'asc' }
70138 const sort : any = sortField ? { [ sortField ] : sortOrder } : undefined ;
71139
72140 const results = await dataSource . find ( schema . objectName , {
73- $filter : filter ,
141+ $filter : finalFilter ,
74142 $orderby : sort ,
75143 $top : 100 // Default pagination limit
76144 } ) ;
@@ -99,7 +167,7 @@ export const ListView: React.FC<ListViewProps> = ({
99167 fetchData ( ) ;
100168
101169 return ( ) => { isMounted = false ; } ;
102- } , [ schema . objectName , dataSource , schema . filters , sortField , sortOrder ] ) ; // Re-fetch on filter/sort change
170+ } , [ schema . objectName , dataSource , schema . filters , sortField , sortOrder , currentFilters ] ) ; // Re-fetch on filter/sort change
103171
104172 // Load saved view preference
105173 React . useEffect ( ( ) => {
@@ -251,6 +319,30 @@ export const ListView: React.FC<ListViewProps> = ({
251319 return views ;
252320 } , [ schema . options , schema . viewType ] ) ;
253321
322+ const hasFilters = currentFilters . conditions && currentFilters . conditions . length > 0 ;
323+
324+ const filterFields = React . useMemo ( ( ) => {
325+ if ( ! objectDef ?. fields ) {
326+ // Fallback to schema fields if objectDef not loaded yet
327+ return ( schema . fields || [ ] ) . map ( ( f : any ) => {
328+ if ( typeof f === 'string' ) return { value : f , label : f , type : 'text' } ;
329+ return {
330+ value : f . name || f . fieldName ,
331+ label : f . label || f . name ,
332+ type : f . type || 'text' ,
333+ options : f . options
334+ } ;
335+ } ) ;
336+ }
337+
338+ return Object . entries ( objectDef . fields ) . map ( ( [ key , field ] : [ string , any ] ) => ( {
339+ value : key ,
340+ label : field . label || key ,
341+ type : field . type || 'text' ,
342+ options : field . options
343+ } ) ) ;
344+ } , [ objectDef , schema . fields ] ) ;
345+
254346 return (
255347 < div className = { cn ( 'flex flex-col h-full bg-background' , className ) } >
256348 { /* Airtable-style Toolbar */ }
@@ -267,15 +359,45 @@ export const ListView: React.FC<ListViewProps> = ({
267359
268360 { /* Action Tools */ }
269361 < div className = "flex items-center gap-1" >
270- < Button
271- variant = { showFilters ? "secondary" : "ghost" }
272- size = "sm"
273- onClick = { ( ) => setShowFilters ( ! showFilters ) }
274- className = "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary"
275- >
276- < SlidersHorizontal className = "h-4 w-4 mr-2" />
277- < span className = "hidden lg:inline" > Filter</ span >
278- </ Button >
362+ < Popover open = { showFilters } onOpenChange = { setShowFilters } >
363+ < PopoverTrigger asChild >
364+ < Button
365+ variant = { hasFilters ? "secondary" : "ghost" }
366+ size = "sm"
367+ className = { cn (
368+ "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary" ,
369+ hasFilters && "text-primary bg-secondary/50"
370+ ) }
371+ >
372+ < SlidersHorizontal className = "h-4 w-4 mr-2" />
373+ < span className = "hidden lg:inline" > Filter</ span >
374+ { hasFilters && (
375+ < span className = "ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary" >
376+ { currentFilters . conditions ?. length || 0 }
377+ </ span >
378+ ) }
379+ </ Button >
380+ </ PopoverTrigger >
381+ < PopoverContent align = "start" className = "w-[600px] p-4" >
382+ < div className = "space-y-4" >
383+ < div className = "flex items-center justify-between border-b pb-2" >
384+ < h4 className = "font-medium text-sm" > Filter Records</ h4 >
385+ </ div >
386+ < FilterBuilder
387+ fields = { filterFields }
388+ value = { currentFilters }
389+ onChange = { ( newFilters ) => {
390+ console . log ( 'Filter Changed:' , newFilters ) ;
391+ setCurrentFilters ( newFilters ) ;
392+ // Convert FilterBuilder format to OData $filter string if needed
393+ // For now we just update state and notify listener
394+ // In a real app, this would likely build an OData string
395+ onFilterChange ?.( newFilters ) ;
396+ } }
397+ />
398+ </ div >
399+ </ PopoverContent >
400+ </ Popover >
279401
280402 { sortField && (
281403 < Button
@@ -318,15 +440,7 @@ export const ListView: React.FC<ListViewProps> = ({
318440 </ div >
319441
320442
321- { /* Filters Panel */ }
322- { showFilters && (
323- < div className = "p-4 border rounded-lg bg-muted/30" >
324- < div className = "text-sm font-medium mb-2" > Filters</ div >
325- < div className = "text-xs text-muted-foreground" >
326- Advanced filter UI coming soon. Current filters: { JSON . stringify ( schema . filters || [ ] ) }
327- </ div >
328- </ div >
329- ) }
443+ { /* Filters Panel - Removed as it is now in Popover */ }
330444
331445 { /* View Content */ }
332446 < div className = "flex-1 min-h-0 bg-background relative overflow-hidden" >
0 commit comments