Skip to content

Commit c9c9855

Browse files
author
Dylan Huang
committed
OR / AND filters
1 parent 9ee574b commit c9c9855

File tree

6 files changed

+247
-125
lines changed

6 files changed

+247
-125
lines changed

vite-app/src/GlobalState.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ export class GlobalState {
5959

6060
// Reset pivot configuration to defaults
6161
resetPivotConfig() {
62-
this.pivotConfig = { ...DEFAULT_PIVOT_CONFIG };
62+
this.pivotConfig = {
63+
...DEFAULT_PIVOT_CONFIG,
64+
filters: [], // Ensure filters is an empty array of FilterGroups
65+
};
6366
this.savePivotConfig();
6467
}
6568

vite-app/src/components/FilterInput.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import { commonStyles } from "../styles/common";
44

55
interface FilterInputProps {
66
filter: FilterConfig;
7-
index: number;
87
onUpdate: (updates: Partial<FilterConfig>) => void;
98
}
109

11-
const FilterInput = ({ filter, index, onUpdate }: FilterInputProps) => {
10+
const FilterInput = ({ filter, onUpdate }: FilterInputProps) => {
1211
const fieldType = filter.type || "text";
1312

1413
if (fieldType === "date") {

vite-app/src/components/PivotTab.tsx

Lines changed: 158 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SearchableSelect from "./SearchableSelect";
44
import Button from "./Button";
55
import FilterInput from "./FilterInput";
66
import { state } from "../App";
7-
import { type FilterConfig } from "../types/filters";
7+
import { type FilterConfig, type FilterGroup } from "../types/filters";
88
import {
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)}

vite-app/src/components/PivotTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function PivotTable<T extends Record<string, unknown>>({
106106
filter,
107107
});
108108

109+
debugger;
109110
return (
110111
<TableContainer className={className}>
111112
<table className="w-full min-w-max">

vite-app/src/types/filters.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ export interface FilterOperator {
1212
label: string;
1313
}
1414

15+
// Filter group interface for AND/OR logic
16+
export interface FilterGroup {
17+
logic: "AND" | "OR";
18+
filters: FilterConfig[];
19+
}
20+
1521
// Pivot configuration interface
1622
export interface PivotConfig {
1723
selectedRowFields: string[];
1824
selectedColumnFields: string[];
1925
selectedValueField: string;
2026
selectedAggregator: string;
21-
filters: FilterConfig[];
27+
filters: FilterGroup[];
2228
}

0 commit comments

Comments
 (0)