@@ -18,6 +18,154 @@ import type { FilterGroup, SortItem } from '@object-ui/components';
1818import { X , Save , RotateCcw } from 'lucide-react' ;
1919import { useObjectTranslation } from '@object-ui/i18n' ;
2020
21+ // ---------------------------------------------------------------------------
22+ // Operator mapping: @objectstack /spec ↔ FilterBuilder
23+ // ---------------------------------------------------------------------------
24+ const SPEC_TO_BUILDER_OP : Record < string , string > = {
25+ '=' : 'equals' ,
26+ '==' : 'equals' ,
27+ '!=' : 'notEquals' ,
28+ '<>' : 'notEquals' ,
29+ '>' : 'greaterThan' ,
30+ '<' : 'lessThan' ,
31+ '>=' : 'greaterOrEqual' ,
32+ '<=' : 'lessOrEqual' ,
33+ 'contains' : 'contains' ,
34+ 'not_contains' : 'notContains' ,
35+ 'is_empty' : 'isEmpty' ,
36+ 'is_not_empty' : 'isNotEmpty' ,
37+ 'in' : 'in' ,
38+ 'not_in' : 'notIn' ,
39+ 'not in' : 'notIn' ,
40+ 'before' : 'before' ,
41+ 'after' : 'after' ,
42+ 'between' : 'between' ,
43+ // Pass-through for already-normalized IDs
44+ 'equals' : 'equals' ,
45+ 'notEquals' : 'notEquals' ,
46+ 'greaterThan' : 'greaterThan' ,
47+ 'lessThan' : 'lessThan' ,
48+ 'greaterOrEqual' : 'greaterOrEqual' ,
49+ 'lessOrEqual' : 'lessOrEqual' ,
50+ 'notContains' : 'notContains' ,
51+ 'isEmpty' : 'isEmpty' ,
52+ 'isNotEmpty' : 'isNotEmpty' ,
53+ 'notIn' : 'notIn' ,
54+ } ;
55+
56+ const BUILDER_TO_SPEC_OP : Record < string , string > = {
57+ 'equals' : '=' ,
58+ 'notEquals' : '!=' ,
59+ 'greaterThan' : '>' ,
60+ 'lessThan' : '<' ,
61+ 'greaterOrEqual' : '>=' ,
62+ 'lessOrEqual' : '<=' ,
63+ 'contains' : 'contains' ,
64+ 'notContains' : 'not_contains' ,
65+ 'isEmpty' : 'is_empty' ,
66+ 'isNotEmpty' : 'is_not_empty' ,
67+ 'in' : 'in' ,
68+ 'notIn' : 'not in' ,
69+ 'before' : 'before' ,
70+ 'after' : 'after' ,
71+ 'between' : 'between' ,
72+ } ;
73+
74+ // ---------------------------------------------------------------------------
75+ // Field type normalization: ObjectUI → FilterBuilder
76+ // ---------------------------------------------------------------------------
77+ function normalizeFieldType ( rawType ?: string ) : 'text' | 'number' | 'boolean' | 'date' | 'select' {
78+ const t = ( rawType || '' ) . toLowerCase ( ) ;
79+ if ( [ 'integer' , 'int' , 'float' , 'double' , 'number' , 'currency' , 'money' , 'percent' , 'rating' ] . includes ( t ) ) return 'number' ;
80+ if ( [ 'date' , 'datetime' , 'datetime_tz' , 'timestamp' ] . includes ( t ) ) return 'date' ;
81+ if ( [ 'boolean' , 'bool' , 'checkbox' , 'switch' ] . includes ( t ) ) return 'boolean' ;
82+ if ( [ 'select' , 'picklist' , 'single_select' , 'multi_select' , 'enum' ] . includes ( t ) ) return 'select' ;
83+ return 'text' ;
84+ }
85+
86+ // ---------------------------------------------------------------------------
87+ // Spec-style filter bridge: parse any supported format → FilterGroup conditions
88+ // Formats: ['field','=',val], [['f','=',v],['f2','!=',v2]], ['and'|'or', ...]
89+ // Also supports object-style: { field, operator, value }
90+ // ---------------------------------------------------------------------------
91+ function parseSpecFilter ( raw : any ) : { logic : 'and' | 'or' ; conditions : Array < { id : string ; field : string ; operator : string ; value : any } > } {
92+ if ( ! Array . isArray ( raw ) || raw . length === 0 ) {
93+ return { logic : 'and' , conditions : [ ] } ;
94+ }
95+
96+ // Detect ['and', ...conditions] or ['or', ...conditions]
97+ if ( typeof raw [ 0 ] === 'string' && ( raw [ 0 ] === 'and' || raw [ 0 ] === 'or' ) ) {
98+ const logic = raw [ 0 ] as 'and' | 'or' ;
99+ const rest = raw . slice ( 1 ) ;
100+ const conditions = rest . flatMap ( ( item : any ) => parseSingleOrNested ( item ) ) ;
101+ return { logic, conditions } ;
102+ }
103+
104+ // Detect single triplet: ['field', '=', value] (all primitives at top level)
105+ if ( raw . length >= 2 && raw . length <= 3 && typeof raw [ 0 ] === 'string' && typeof raw [ 1 ] === 'string' && ! Array . isArray ( raw [ 0 ] ) ) {
106+ // Check it's not an array of arrays
107+ if ( ! Array . isArray ( raw [ 2 ] ) ) {
108+ const cond = parseTriplet ( raw ) ;
109+ return { logic : 'and' , conditions : cond ? [ cond ] : [ ] } ;
110+ }
111+ }
112+
113+ // Detect array of conditions: [[...], [...]] or [{...}, {...}]
114+ if ( Array . isArray ( raw [ 0 ] ) || ( typeof raw [ 0 ] === 'object' && raw [ 0 ] !== null && ! Array . isArray ( raw [ 0 ] ) ) ) {
115+ const conditions = raw . flatMap ( ( item : any ) => parseSingleOrNested ( item ) ) ;
116+ return { logic : 'and' , conditions } ;
117+ }
118+
119+ // Fallback: try as single triplet
120+ const cond = parseTriplet ( raw ) ;
121+ return { logic : 'and' , conditions : cond ? [ cond ] : [ ] } ;
122+ }
123+
124+ function parseTriplet ( arr : any [ ] ) : { id : string ; field : string ; operator : string ; value : any } | null {
125+ if ( ! Array . isArray ( arr ) || arr . length < 2 ) return null ;
126+ const [ field , op , value ] = arr ;
127+ if ( typeof field !== 'string' || typeof op !== 'string' ) return null ;
128+ return {
129+ id : crypto . randomUUID ( ) ,
130+ field,
131+ operator : SPEC_TO_BUILDER_OP [ op ] || op ,
132+ value : value ?? '' ,
133+ } ;
134+ }
135+
136+ function parseSingleOrNested ( item : any ) : Array < { id : string ; field : string ; operator : string ; value : any } > {
137+ if ( Array . isArray ( item ) ) {
138+ const triplet = parseTriplet ( item ) ;
139+ return triplet ? [ triplet ] : [ ] ;
140+ }
141+ if ( typeof item === 'object' && item !== null && item . field ) {
142+ return [ {
143+ id : item . id || crypto . randomUUID ( ) ,
144+ field : item . field ,
145+ operator : SPEC_TO_BUILDER_OP [ item . operator ] || item . operator || 'equals' ,
146+ value : item . value ?? '' ,
147+ } ] ;
148+ }
149+ return [ ] ;
150+ }
151+
152+ /**
153+ * Convert FilterGroup conditions back to spec-style filter array.
154+ * Produces [['field','op',value], ...] for multiple conditions,
155+ * or ['field','op',value] for single condition,
156+ * or ['and'|'or', ...] when logic is 'or'.
157+ */
158+ function toSpecFilter ( logic : 'and' | 'or' , conditions : Array < { field : string ; operator : string ; value : any } > ) : any [ ] {
159+ const triplets = conditions
160+ . filter ( c => c . field ) // skip empty
161+ . map ( c => [ c . field , BUILDER_TO_SPEC_OP [ c . operator ] || c . operator , c . value ] ) ;
162+
163+ if ( triplets . length === 0 ) return [ ] ;
164+ if ( triplets . length === 1 && logic === 'and' ) return triplets [ 0 ] ;
165+ if ( logic === 'or' ) return [ 'or' , ...triplets ] ;
166+ return triplets ;
167+ }
168+
21169/** View type labels for display */
22170const VIEW_TYPE_LABELS : Record < string , string > = {
23171 grid : 'Grid' ,
@@ -162,20 +310,15 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
162310 return Object . entries ( objectDef . fields ) . map ( ( [ key , field ] : [ string , any ] ) => ( {
163311 value : key ,
164312 label : field . label || key ,
165- type : field . type || 'text' ,
313+ type : normalizeFieldType ( field . type ) ,
166314 options : field . options ,
167315 } ) ) ;
168316 } , [ objectDef . fields ] ) ;
169317
170- // Bridge: view filter array → FilterGroup
318+ // Bridge: view filter (any spec format) → FilterGroup
171319 const filterGroupValue = useMemo < FilterGroup > ( ( ) => {
172- const conditions = ( Array . isArray ( draft . filter ) ? draft . filter : [ ] ) . map ( ( f : any ) => ( {
173- id : f . id || crypto . randomUUID ( ) ,
174- field : f . field || '' ,
175- operator : f . operator || 'equals' ,
176- value : f . value ?? '' ,
177- } ) ) ;
178- return { id : 'root' , logic : 'and' as const , conditions } ;
320+ const parsed = parseSpecFilter ( draft . filter ) ;
321+ return { id : 'root' , logic : parsed . logic , conditions : parsed . conditions } ;
179322 } , [ draft . filter ] ) ;
180323
181324 // Bridge: view sort array → SortItem[]
@@ -187,15 +330,17 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
187330 } ) ) ;
188331 } , [ draft . sort ] ) ;
189332
190- /** Handle FilterBuilder changes → update draft.filter */
333+ /** Handle FilterBuilder changes → update draft.filter in spec format */
191334 const handleFilterChange = useCallback ( ( group : FilterGroup ) => {
192- const filters = group . conditions . map ( c => ( {
193- id : c . id ,
194- field : c . field ,
195- operator : c . operator ,
196- value : c . value ,
197- } ) ) ;
198- updateDraft ( 'filter' , filters ) ;
335+ const specFilter = toSpecFilter (
336+ group . logic ,
337+ group . conditions . map ( c => ( {
338+ field : c . field ,
339+ operator : c . operator ,
340+ value : c . value ,
341+ } ) )
342+ ) ;
343+ updateDraft ( 'filter' , specFilter ) ;
199344 } , [ updateDraft ] ) ;
200345
201346 /** Handle SortBuilder changes → update draft.sort */
@@ -283,7 +428,7 @@ export function ViewConfigPanel({ open, onClose, activeView, objectDef, onViewUp
283428 < Checkbox
284429 data-testid = { `col-checkbox-${ f . value } ` }
285430 checked = { checked }
286- onCheckedChange = { ( c : boolean ) => handleColumnToggle ( f . value , c ) }
431+ onCheckedChange = { ( c ) => handleColumnToggle ( f . value , c === true ) }
287432 className = "h-3.5 w-3.5"
288433 />
289434 < span className = "truncate" > { f . label } </ span >
0 commit comments