@@ -4,7 +4,7 @@ import SearchableSelect from "./SearchableSelect";
44import Button from "./Button" ;
55import FilterInput from "./FilterInput" ;
66import { state } from "../App" ;
7- import { type FilterConfig } from "../types/filters" ;
7+ import { type FilterConfig , type FilterGroup } from "../types/filters" ;
88import {
99 getFieldType ,
1010 getOperatorsForField ,
@@ -133,83 +133,166 @@ const FilterSelector = ({
133133 onFiltersChange,
134134 availableKeys,
135135} : {
136- filters : FilterConfig [ ] ;
137- onFiltersChange : ( filters : FilterConfig [ ] ) => void ;
136+ filters : FilterGroup [ ] ;
137+ onFiltersChange : ( filters : FilterGroup [ ] ) => void ;
138138 availableKeys : string [ ] ;
139139} ) => {
140- const addFilter = ( ) => {
141- onFiltersChange ( [
142- ...filters ,
143- { field : "" , operator : "contains" , value : "" , type : "text" } ,
144- ] ) ;
140+ const addFilterGroup = ( ) => {
141+ onFiltersChange ( [ ...filters , { logic : "AND" , filters : [ ] } ] ) ;
145142 } ;
146143
147- const removeFilter = ( index : number ) => {
144+ const removeFilterGroup = ( index : number ) => {
148145 onFiltersChange ( filters . filter ( ( _ , i ) => i !== index ) ) ;
149146 } ;
150147
151- const updateFilter = ( index : number , updates : Partial < FilterConfig > ) => {
148+ const updateFilterGroupLogic = ( index : number , logic : "AND" | "OR" ) => {
152149 const newFilters = [ ...filters ] ;
153- newFilters [ index ] = { ...newFilters [ index ] , ...updates } ;
150+ newFilters [ index ] = { ...newFilters [ index ] , logic } ;
151+ onFiltersChange ( newFilters ) ;
152+ } ;
153+
154+ const addFilterToGroup = ( groupIndex : number ) => {
155+ const newFilters = [ ...filters ] ;
156+ newFilters [ groupIndex ] . filters . push ( {
157+ field : "" ,
158+ operator : "contains" ,
159+ value : "" ,
160+ type : "text" ,
161+ } ) ;
162+ onFiltersChange ( newFilters ) ;
163+ } ;
164+
165+ const removeFilterFromGroup = ( groupIndex : number , filterIndex : number ) => {
166+ const newFilters = [ ...filters ] ;
167+ newFilters [ groupIndex ] . filters . splice ( filterIndex , 1 ) ;
168+ onFiltersChange ( newFilters ) ;
169+ } ;
170+
171+ const updateFilterInGroup = (
172+ groupIndex : number ,
173+ filterIndex : number ,
174+ updates : Partial < FilterConfig >
175+ ) => {
176+ const newFilters = [ ...filters ] ;
177+ newFilters [ groupIndex ] . filters [ filterIndex ] = {
178+ ...newFilters [ groupIndex ] . filters [ filterIndex ] ,
179+ ...updates ,
180+ } ;
154181 onFiltersChange ( newFilters ) ;
155182 } ;
156183
157184 return (
158185 < div className = "mb-4" >
159186 < div className = "text-xs font-medium text-gray-700 mb-2" > Filters:</ div >
160- < div className = "space-y-2" >
161- { filters . map ( ( filter , index ) => {
162- const fieldType = filter . type || getFieldType ( filter . field ) ;
163- const operators = getOperatorsForField ( filter . field , fieldType ) ;
164-
165- return (
166- < div key = { index } className = "flex items-center space-x-2" >
167- < SearchableSelect
168- value = { filter . field }
169- onChange = { ( value ) => {
170- const newField = value ;
171- const newType = getFieldType ( newField ) ;
172- updateFilter ( index , { field : newField , type : newType } ) ;
173- } }
174- options = { [
175- { value : "" , label : "Select a field..." } ,
176- ...( availableKeys ?. map ( ( key ) => ( {
177- value : key ,
178- label : key ,
179- } ) ) || [ ] ) ,
180- ] }
181- size = "sm"
182- className = "min-w-48"
183- />
184- < SearchableSelect
185- value = { filter . operator }
186- onChange = { ( value ) => updateFilter ( index , { operator : value } ) }
187- options = { operators . map ( ( op ) => ( {
188- value : op . value ,
189- label : op . label ,
190- } ) ) }
191- size = "sm"
192- className = "min-w-32"
193- />
194- < FilterInput
195- filter = { filter }
196- index = { index }
197- onUpdate = { ( updates ) => updateFilter ( index , updates ) }
198- />
187+ < div className = "space-y-4" >
188+ { filters . map ( ( group , groupIndex ) => (
189+ < div
190+ key = { groupIndex }
191+ className = "border border-gray-200 rounded p-3 bg-gray-50"
192+ >
193+ < div className = "flex items-center justify-between mb-3" >
194+ < div className = "flex items-center space-x-2" >
195+ < span className = "text-xs font-medium text-gray-600" >
196+ Group { groupIndex + 1 } :
197+ </ span >
198+ < SearchableSelect
199+ value = { group . logic }
200+ onChange = { ( value ) =>
201+ updateFilterGroupLogic ( groupIndex , value as "AND" | "OR" )
202+ }
203+ options = { [
204+ { value : "AND" , label : "AND (all filters must match)" } ,
205+ { value : "OR" , label : "OR (any filter can match)" } ,
206+ ] }
207+ size = "sm"
208+ className = "min-w-48"
209+ />
210+ </ div >
199211 < button
200- onClick = { ( ) => removeFilter ( index ) }
212+ onClick = { ( ) => removeFilterGroup ( groupIndex ) }
201213 className = "text-xs text-red-600 hover:text-red-800 px-2 py-1"
202214 >
203- Remove
215+ Remove Group
216+ </ button >
217+ </ div >
218+
219+ < div className = "space-y-2 ml-4" >
220+ { group . filters . map ( ( filter , filterIndex ) => {
221+ const fieldType = filter . type || getFieldType ( filter . field ) ;
222+ const operators = getOperatorsForField ( filter . field , fieldType ) ;
223+
224+ return (
225+ < div
226+ key = { filterIndex }
227+ className = "flex items-center space-x-2"
228+ >
229+ < SearchableSelect
230+ value = { filter . field }
231+ onChange = { ( value ) => {
232+ const newField = value ;
233+ const newType = getFieldType ( newField ) ;
234+ updateFilterInGroup ( groupIndex , filterIndex , {
235+ field : newField ,
236+ type : newType ,
237+ } ) ;
238+ } }
239+ options = { [
240+ { value : "" , label : "Select a field..." } ,
241+ ...( availableKeys ?. map ( ( key ) => ( {
242+ value : key ,
243+ label : key ,
244+ } ) ) || [ ] ) ,
245+ ] }
246+ size = "sm"
247+ className = "min-w-48"
248+ />
249+ < SearchableSelect
250+ value = { filter . operator }
251+ onChange = { ( value ) =>
252+ updateFilterInGroup ( groupIndex , filterIndex , {
253+ operator : value ,
254+ } )
255+ }
256+ options = { operators . map ( ( op ) => ( {
257+ value : op . value ,
258+ label : op . label ,
259+ } ) ) }
260+ size = "sm"
261+ className = "min-w-32"
262+ />
263+ < FilterInput
264+ filter = { filter }
265+ onUpdate = { ( updates ) =>
266+ updateFilterInGroup ( groupIndex , filterIndex , updates )
267+ }
268+ />
269+ < button
270+ onClick = { ( ) =>
271+ removeFilterFromGroup ( groupIndex , filterIndex )
272+ }
273+ className = "text-xs text-red-600 hover:text-red-800 px-2 py-1"
274+ >
275+ Remove
276+ </ button >
277+ </ div >
278+ ) ;
279+ } ) }
280+
281+ < button
282+ onClick = { ( ) => addFilterToGroup ( groupIndex ) }
283+ className = "text-xs text-blue-600 hover:text-blue-800 px-2 py-1"
284+ >
285+ + Add Filter to Group
204286 </ button >
205287 </ div >
206- ) ;
207- } ) }
288+ </ div >
289+ ) ) }
290+
208291 < button
209- onClick = { addFilter }
292+ onClick = { addFilterGroup }
210293 className = "text-xs text-blue-600 hover:text-blue-800 px-2 py-1"
211294 >
212- + Add Filter
295+ + Add Filter Group
213296 </ button >
214297 </ div >
215298 </ div >
@@ -239,7 +322,7 @@ const PivotTab = observer(() => {
239322 state . updatePivotConfig ( { selectedAggregator : value } ) ;
240323 } ;
241324
242- const updateFilters = ( filters : FilterConfig [ ] ) => {
325+ const updateFilters = ( filters : FilterGroup [ ] ) => {
243326 state . updatePivotConfig ( { filters } ) ;
244327 } ;
245328
@@ -343,6 +426,16 @@ const PivotTab = observer(() => {
343426 availableKeys = { availableKeys }
344427 />
345428
429+ { /*
430+ Filter Groups allow you to create complex filtering logic:
431+ - Each group can use AND or OR logic internally
432+ - Groups are combined with AND logic (all groups must match)
433+ - Within a group: AND means all filters must match, OR means any filter can match
434+ - Example: Group 1 (AND): field1 = "value1" AND field2 > 10
435+ - Example: Group 2 (OR): field3 = "value3" OR field4 = "value4"
436+ - Result: (field1 = "value1" AND field2 > 10) AND (field3 = "value3" OR field4 = "value4")
437+ */ }
438+
346439 < PivotTable
347440 data = { state . flattenedDataset }
348441 rowFields = {
@@ -358,7 +451,14 @@ const PivotTab = observer(() => {
358451 valueField = {
359452 pivotConfig . selectedValueField as keyof ( typeof state . flattenedDataset ) [ number ]
360453 }
361- aggregator = { pivotConfig . selectedAggregator as any }
454+ aggregator = {
455+ pivotConfig . selectedAggregator as
456+ | "count"
457+ | "sum"
458+ | "avg"
459+ | "min"
460+ | "max"
461+ }
362462 showRowTotals
363463 showColumnTotals
364464 filter = { createFilterFunction ( pivotConfig . filters ) }
0 commit comments