@vuu-ui/vuu-filters 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +35 -0
- package/src/filter-input/FilterInput.css +34 -0
- package/src/filter-input/FilterInput.tsx +43 -0
- package/src/filter-input/codemirror-basic-setup.ts +112 -0
- package/src/filter-input/filter-language-parser/FilterLanguage.ts +21 -0
- package/src/filter-input/filter-language-parser/generated/filter-parser.js +16 -0
- package/src/filter-input/filter-language-parser/generated/filter-parser.terms.js +32 -0
- package/src/filter-input/filter-language-parser/grammar/filter.grammar +53 -0
- package/src/filter-input/filter-language-parser/index.ts +1 -0
- package/src/filter-input/filter-language-parser/walkTree.ts +236 -0
- package/src/filter-input/highlighting.ts +9 -0
- package/src/filter-input/index.ts +2 -0
- package/src/filter-input/theme.ts +44 -0
- package/src/filter-input/useCodeMirrorEditor.ts +191 -0
- package/src/filter-input/useFilterAutoComplete.ts +296 -0
- package/src/filter-toolbar/FilterDropdown.tsx +60 -0
- package/src/filter-toolbar/FilterToolbar.css +11 -0
- package/src/filter-toolbar/FilterToolbar.tsx +26 -0
- package/src/filter-toolbar/index.ts +1 -0
- package/src/filter-toolbar/useFilterToolbar.tsx +85 -0
- package/src/filter-utils.ts +483 -0
- package/src/filterTypes.ts +84 -0
- package/src/index.ts +4 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { partition } from "@vuu-ui/vuu-utils/src/array-utils";
|
|
2
|
+
import {
|
|
3
|
+
AndFilter,
|
|
4
|
+
Filter,
|
|
5
|
+
FilterClause,
|
|
6
|
+
FilterCombinatorOp,
|
|
7
|
+
isMultiClauseFilter,
|
|
8
|
+
isMultiValueFilter,
|
|
9
|
+
isAndFilter,
|
|
10
|
+
isOrFilter,
|
|
11
|
+
isInFilter,
|
|
12
|
+
OrFilter,
|
|
13
|
+
isSingleValueFilter,
|
|
14
|
+
} from "./filterTypes";
|
|
15
|
+
import { Row } from "@vuu-ui/vuu-utils/src/row-utils";
|
|
16
|
+
import { KeyedColumnDescriptor } from "@vuu-ui/vuu-datagrid/src/grid-model";
|
|
17
|
+
|
|
18
|
+
export const AND = "and";
|
|
19
|
+
export const EQUALS = "=";
|
|
20
|
+
export const GREATER_THAN = ">";
|
|
21
|
+
export const LESS_THAN = "<";
|
|
22
|
+
export const OR = "or";
|
|
23
|
+
export const STARTS_WITH = "starts";
|
|
24
|
+
export const ENDS_WITH = "ends";
|
|
25
|
+
export const IN = "in";
|
|
26
|
+
|
|
27
|
+
export type FilterType =
|
|
28
|
+
| "and"
|
|
29
|
+
| "="
|
|
30
|
+
| ">"
|
|
31
|
+
| ">="
|
|
32
|
+
| "in"
|
|
33
|
+
| "<="
|
|
34
|
+
| "<"
|
|
35
|
+
| "NOT_IN"
|
|
36
|
+
| "NOT_SW"
|
|
37
|
+
| "or"
|
|
38
|
+
| "SW";
|
|
39
|
+
|
|
40
|
+
export const SET_FILTER_DATA_COLUMNS = [
|
|
41
|
+
{ name: "name", flex: 1 },
|
|
42
|
+
{ name: "count", width: 40, type: "number" },
|
|
43
|
+
{ name: "totalCount", width: 40, type: "number" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const BIN_FILTER_DATA_COLUMNS = [
|
|
47
|
+
{ name: "bin" },
|
|
48
|
+
{ name: "count" },
|
|
49
|
+
{ name: "bin-lo" },
|
|
50
|
+
{ name: "bin-hi" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const filterClauses = (
|
|
54
|
+
filter: Filter | null,
|
|
55
|
+
clauses: FilterClause[] = []
|
|
56
|
+
) => {
|
|
57
|
+
if (filter) {
|
|
58
|
+
if (isMultiClauseFilter(filter)) {
|
|
59
|
+
filter.filters.forEach((f) => clauses.push(...filterClauses(f)));
|
|
60
|
+
} else {
|
|
61
|
+
// TODO why did we originally stringify 'values' ?
|
|
62
|
+
// clauses.push({ column, op, value: value ?? values?.join(',') });
|
|
63
|
+
clauses.push(filter);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return clauses;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type AddFilterOptions = {
|
|
70
|
+
combineWith: FilterCombinatorOp;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const DEFAULT_ADD_FILTER_OPTS: AddFilterOptions = {
|
|
74
|
+
combineWith: "and",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function addFilter(
|
|
78
|
+
existingFilter: Filter | null,
|
|
79
|
+
filter: Filter,
|
|
80
|
+
{ combineWith = AND }: AddFilterOptions = DEFAULT_ADD_FILTER_OPTS
|
|
81
|
+
): Filter | null {
|
|
82
|
+
if (includesNoValues(filter)) {
|
|
83
|
+
if (isMultiClauseFilter(filter)) {
|
|
84
|
+
// TODO identify the column that is contributing the no-values filter
|
|
85
|
+
} else {
|
|
86
|
+
existingFilter = removeFilterForColumn(existingFilter, {
|
|
87
|
+
name: filter.column,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} else if (includesAllValues(filter)) {
|
|
91
|
+
// A filter that returns all values is a way to remove filtering for this column
|
|
92
|
+
if (isMultiClauseFilter(filter)) {
|
|
93
|
+
// TODO identify the column that is contributing the all-values filter
|
|
94
|
+
} else {
|
|
95
|
+
return removeFilterForColumn(existingFilter, { name: filter.column });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!existingFilter) {
|
|
100
|
+
return filter;
|
|
101
|
+
} else if (!filter) {
|
|
102
|
+
return existingFilter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (existingFilter.op === AND && filter.op === AND) {
|
|
106
|
+
return {
|
|
107
|
+
op: AND,
|
|
108
|
+
filters: combine(existingFilter.filters, filter.filters),
|
|
109
|
+
};
|
|
110
|
+
} else if (existingFilter.op === AND) {
|
|
111
|
+
const filters = replaceOrInsert(existingFilter.filters, filter);
|
|
112
|
+
return filters.length > 1 ? { op: AND, filters } : filters[0];
|
|
113
|
+
} else if (filter.op === AND) {
|
|
114
|
+
return { op: AND, filters: filter.filters.concat(existingFilter) };
|
|
115
|
+
} else if (filterEquals(existingFilter, filter, true)) {
|
|
116
|
+
return filter;
|
|
117
|
+
} else if (sameColumn(existingFilter, filter)) {
|
|
118
|
+
return merge(existingFilter, filter);
|
|
119
|
+
} else {
|
|
120
|
+
return { op: combineWith, filters: [existingFilter, filter] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function includesNoValues(filter?: Filter | null): boolean {
|
|
125
|
+
// TODO make sure we catch all cases...
|
|
126
|
+
if (!filter) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (isInFilter(filter) && filter.values.length === 0) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
isAndFilter(filter) && filter.filters!.some((f) => includesNoValues(f))
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getFilterColumn(column: Column | ColumnGroup) {
|
|
138
|
+
return isColumnGroup(column) ? column.columns[0] : column;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//TODO might need some refinement for Quotes etc
|
|
142
|
+
export const filterAsQuery = (f: Filter, namedFilters = {}): string => {
|
|
143
|
+
if (isMultiClauseFilter(f)) {
|
|
144
|
+
const [clause1, clause2] = f.filters;
|
|
145
|
+
return `${filterAsQuery(clause1, namedFilters)} ${f.op} ${filterAsQuery(
|
|
146
|
+
clause2,
|
|
147
|
+
namedFilters
|
|
148
|
+
)}`;
|
|
149
|
+
} else if (isMultiValueFilter(f)) {
|
|
150
|
+
return `${f.column} ${f.op} [${f.values.join(",")}]`;
|
|
151
|
+
} else {
|
|
152
|
+
return `${f.column} ${f.op} ${f.value}`;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// TODO types for different types of filters
|
|
157
|
+
|
|
158
|
+
interface CommonFilter {
|
|
159
|
+
colName?: string;
|
|
160
|
+
otherColFilters?: Filter[];
|
|
161
|
+
// values?: any[];
|
|
162
|
+
mode?: any;
|
|
163
|
+
value?: any;
|
|
164
|
+
values?: any;
|
|
165
|
+
op?: "or" | "and";
|
|
166
|
+
column?: string;
|
|
167
|
+
filters?: Filter[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface OtherFilter extends CommonFilter {
|
|
171
|
+
type: FilterType;
|
|
172
|
+
values?: any[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type RowFilterFn = (row: Row) => boolean;
|
|
176
|
+
|
|
177
|
+
// export function shouldShowFilter(filterColumnName: string, column: Column): boolean {
|
|
178
|
+
// const filterColumn = getFilterColumn(column);
|
|
179
|
+
// if (isColumnGroup(filterColumn)) {
|
|
180
|
+
// return filterColumn.columns.some((col) => col.name === filterColumnName);
|
|
181
|
+
// } else {
|
|
182
|
+
// return filterColumnName === filterColumn.name;
|
|
183
|
+
// }
|
|
184
|
+
// }
|
|
185
|
+
|
|
186
|
+
function includesAllValues(filter?: Filter | null): boolean {
|
|
187
|
+
if (!filter) {
|
|
188
|
+
return false;
|
|
189
|
+
} else if (filter.op === STARTS_WITH && filter.value === "") {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return filter.op === STARTS_WITH && filter.value === "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If we add an IN filter and there is an existing NOT_IN, we would always expect the IN
|
|
196
|
+
// values to exist in the NOT_IN set (as long as user interaction is driving the filtering)
|
|
197
|
+
function replaceOrInsert(filters: Filter[], filter: Filter) {
|
|
198
|
+
// const { type, column, values } = filter;
|
|
199
|
+
// if (type === IN) {
|
|
200
|
+
// let idx = filters.findIndex((f) => f.type === EQUALS && f.column === column);
|
|
201
|
+
// if (idx !== -1) {
|
|
202
|
+
// const { values: existingValues } = filters[idx];
|
|
203
|
+
// if (values.every((value) => existingValues.indexOf(value) !== -1)) {
|
|
204
|
+
// if (values.length === existingValues.length) {
|
|
205
|
+
// // we simply remove the existing 'other' filter ...
|
|
206
|
+
// return filters.filter((f, i) => i !== idx);
|
|
207
|
+
// } else {
|
|
208
|
+
// // ... or strip the matching values from the 'other' filter values
|
|
209
|
+
// let newValues = existingValues.filter((value) => !values.includes(value));
|
|
210
|
+
// return filters.map((filter, i) =>
|
|
211
|
+
// i === idx ? { ...filter, values: newValues } : filter
|
|
212
|
+
// );
|
|
213
|
+
// }
|
|
214
|
+
// } else if (values.some((value) => existingValues.indexOf(value) !== -1)) {
|
|
215
|
+
// console.log(`partial overlap between IN and NOT_IN`);
|
|
216
|
+
// }
|
|
217
|
+
// } else {
|
|
218
|
+
// idx = filters.findIndex((f) => f.type === type && f.colName === filter.colName);
|
|
219
|
+
// if (idx !== -1) {
|
|
220
|
+
// return filters.map((f, i) => (i === idx ? merge(f, filter) : f));
|
|
221
|
+
// }
|
|
222
|
+
// }
|
|
223
|
+
// }
|
|
224
|
+
|
|
225
|
+
return filters.concat(filter);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function merge(f1: Filter, f2: Filter): Filter | null {
|
|
229
|
+
const { op: t1 } = f1;
|
|
230
|
+
const { op: t2 } = f2;
|
|
231
|
+
|
|
232
|
+
if (includesNoValues(f2)) {
|
|
233
|
+
return f2;
|
|
234
|
+
} else if (isInFilter(f1) && isInFilter(f2)) {
|
|
235
|
+
return {
|
|
236
|
+
...f1,
|
|
237
|
+
values: f1.values.concat(
|
|
238
|
+
f2.values.filter((v: any) => !f1.values.includes(v))
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
} else if (f1.op === STARTS_WITH && f2.op === STARTS_WITH) {
|
|
242
|
+
return {
|
|
243
|
+
op: OR,
|
|
244
|
+
filters: [f1, f2],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return f2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function combine(existingFilters: Filter[], replacementFilters: Filter[]) {
|
|
252
|
+
// TODO need a safer REGEX here
|
|
253
|
+
function equivalentType({ op: t1 }: Filter, { op: t2 }: Filter) {
|
|
254
|
+
return t1 === t2 || t1[0] === t2[0];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const replaces = (existingFilter: Filter, replacementFilter: Filter) => {
|
|
258
|
+
return (
|
|
259
|
+
existingFilter.column === replacementFilter.column &&
|
|
260
|
+
equivalentType(existingFilter, replacementFilter)
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const stillApplicable = (existingFilter: Filter) =>
|
|
265
|
+
replacementFilters.some((replacementFilter) =>
|
|
266
|
+
replaces(existingFilter, replacementFilter)
|
|
267
|
+
) === false;
|
|
268
|
+
|
|
269
|
+
return existingFilters.filter(stillApplicable).concat(replacementFilters);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function removeColumnFromFilter(
|
|
273
|
+
column: KeyedColumnDescriptor,
|
|
274
|
+
filter: Filter
|
|
275
|
+
): [Filter | undefined, string] {
|
|
276
|
+
// TODO need to recurse into nested and/or
|
|
277
|
+
if (isMultiClauseFilter(filter)) {
|
|
278
|
+
const [clause1, clause2] = filter.filters;
|
|
279
|
+
if (clause1.column === column.name) {
|
|
280
|
+
return [clause2, filterAsQuery(clause2)];
|
|
281
|
+
} else if (clause2.column === column.name) {
|
|
282
|
+
return [clause1, filterAsQuery(clause1)];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return [undefined, ""];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function removeFilter(sourceFilter: Filter, filterToRemove: Filter) {
|
|
289
|
+
if (filterEquals(sourceFilter, filterToRemove, true)) {
|
|
290
|
+
return null;
|
|
291
|
+
} else if (sourceFilter.op !== AND) {
|
|
292
|
+
throw Error(
|
|
293
|
+
`removeFilter cannot remove ${JSON.stringify(
|
|
294
|
+
filterToRemove
|
|
295
|
+
)} from ${JSON.stringify(sourceFilter)}`
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
const filters = (sourceFilter as AndFilter).filters.filter(
|
|
299
|
+
(f) => !filterEquals(f, filterToRemove)
|
|
300
|
+
);
|
|
301
|
+
return filters.length > 0 ? { type: AND, filters } : null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function splitFilterOnColumn(
|
|
306
|
+
filter: Filter | null,
|
|
307
|
+
columnName: string
|
|
308
|
+
): [Filter | null, Filter | null] {
|
|
309
|
+
if (!filter) {
|
|
310
|
+
return [null, null];
|
|
311
|
+
} else if (filter.column === columnName) {
|
|
312
|
+
return [filter, null];
|
|
313
|
+
} else if (filter.op !== AND) {
|
|
314
|
+
return [null, filter];
|
|
315
|
+
} else {
|
|
316
|
+
const [[columnFilter = null], filters] = partition(
|
|
317
|
+
(filter as AndFilter).filters,
|
|
318
|
+
(f) => f.column === columnName
|
|
319
|
+
);
|
|
320
|
+
return filters.length === 1
|
|
321
|
+
? [columnFilter, filters[0]]
|
|
322
|
+
: [columnFilter, { op: AND, filters }];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const overrideColName = (filter: Filter, column: string): Filter => {
|
|
327
|
+
if (isMultiClauseFilter(filter)) {
|
|
328
|
+
return {
|
|
329
|
+
op: filter.op,
|
|
330
|
+
filters: filter.filters.map((f) => overrideColName(f, column)),
|
|
331
|
+
};
|
|
332
|
+
} else {
|
|
333
|
+
return { ...filter, column };
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export function extractFilterForColumn(
|
|
338
|
+
filter: Filter | null,
|
|
339
|
+
columnName: string
|
|
340
|
+
) {
|
|
341
|
+
if (!filter) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const { op, column } = filter;
|
|
345
|
+
switch (op) {
|
|
346
|
+
case AND:
|
|
347
|
+
case OR:
|
|
348
|
+
return collectFiltersForColumn(
|
|
349
|
+
op,
|
|
350
|
+
(filter as AndFilter | OrFilter).filters,
|
|
351
|
+
columnName
|
|
352
|
+
);
|
|
353
|
+
default:
|
|
354
|
+
return column === columnName ? filter : null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function collectFiltersForColumn(
|
|
359
|
+
op: "and" | "or",
|
|
360
|
+
filters: Filter[],
|
|
361
|
+
columnName: string
|
|
362
|
+
) {
|
|
363
|
+
const results: Filter[] = [];
|
|
364
|
+
filters.forEach((filter) => {
|
|
365
|
+
const ffc = extractFilterForColumn(filter, columnName);
|
|
366
|
+
if (ffc !== null) {
|
|
367
|
+
results.push(ffc);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
if (results.length === 1) {
|
|
371
|
+
return results[0];
|
|
372
|
+
} else {
|
|
373
|
+
return {
|
|
374
|
+
op,
|
|
375
|
+
filters: results,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function filterIncludesColumn(
|
|
381
|
+
filter: Filter,
|
|
382
|
+
column: KeyedColumnDescriptor
|
|
383
|
+
): boolean {
|
|
384
|
+
if (!filter) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
const { op, column: filterColName } = filter;
|
|
388
|
+
switch (op) {
|
|
389
|
+
case AND:
|
|
390
|
+
case OR:
|
|
391
|
+
return (
|
|
392
|
+
filter.filters != null &&
|
|
393
|
+
filter.filters.some((f) => filterIncludesColumn(f, column))
|
|
394
|
+
);
|
|
395
|
+
default:
|
|
396
|
+
return filterColName === column.name;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function removeFilterForColumn(
|
|
401
|
+
sourceFilter: Filter | null,
|
|
402
|
+
column: Column
|
|
403
|
+
): Filter | null {
|
|
404
|
+
const colName = column.name;
|
|
405
|
+
if (!sourceFilter) {
|
|
406
|
+
return null;
|
|
407
|
+
} else if (sourceFilter.column === colName) {
|
|
408
|
+
return null;
|
|
409
|
+
} else if (isAndFilter(sourceFilter) || isOrFilter(sourceFilter)) {
|
|
410
|
+
const { op } = sourceFilter;
|
|
411
|
+
const filters = sourceFilter.filters;
|
|
412
|
+
const otherColFilters = filters.filter((f) => f.column !== colName);
|
|
413
|
+
switch (otherColFilters.length) {
|
|
414
|
+
case 0:
|
|
415
|
+
return null;
|
|
416
|
+
case 1:
|
|
417
|
+
return otherColFilters[0];
|
|
418
|
+
default:
|
|
419
|
+
return { op, filters: otherColFilters };
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
return sourceFilter;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const sameColumn = (f1: Filter, f2: Filter) => f1.column === f2.column;
|
|
427
|
+
|
|
428
|
+
export function filterEquals(f1?: Filter, f2?: Filter, strict = false) {
|
|
429
|
+
if (f1 && f2 && sameColumn(f1, f2)) {
|
|
430
|
+
if (!strict) {
|
|
431
|
+
return true;
|
|
432
|
+
} else {
|
|
433
|
+
return (
|
|
434
|
+
f1.op === f2.op &&
|
|
435
|
+
((isSingleValueFilter(f1) &&
|
|
436
|
+
isSingleValueFilter(f2) &&
|
|
437
|
+
f1.value === f2.value) ||
|
|
438
|
+
(isMultiValueFilter(f1) &&
|
|
439
|
+
isMultiValueFilter(f2) &&
|
|
440
|
+
sameValues(f1.values, f2.values)))
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
//TODO roll this into next function
|
|
449
|
+
export function projectFilterData(filterRows: Row[]) {
|
|
450
|
+
return filterRows.map((row, idx) => [idx, 0, 0, null, row.name, row.count]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// only suitable for small arrays of simple types (e.g. filter values)
|
|
454
|
+
function sameValues<T>(arr1: T[], arr2: T[]) {
|
|
455
|
+
if (arr1 === arr2) {
|
|
456
|
+
return true;
|
|
457
|
+
} else if (arr1.length === arr2.length) {
|
|
458
|
+
const a = arr1.slice().sort();
|
|
459
|
+
const b = arr2.slice().sort();
|
|
460
|
+
return a.join("|") === b.join("|");
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export const updateFilter = (
|
|
466
|
+
filter: Filter | undefined,
|
|
467
|
+
newFilter: Filter,
|
|
468
|
+
mode: "add" | "replace"
|
|
469
|
+
): Filter => {
|
|
470
|
+
if (mode === "replace" || filter === undefined) {
|
|
471
|
+
return newFilter;
|
|
472
|
+
} else if (filter.op === "and") {
|
|
473
|
+
return {
|
|
474
|
+
...filter,
|
|
475
|
+
filters: filter.filters.concat(newFilter),
|
|
476
|
+
};
|
|
477
|
+
} else {
|
|
478
|
+
return {
|
|
479
|
+
op: "and",
|
|
480
|
+
filters: [filter, newFilter],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type SingleValueFilterClauseOp =
|
|
2
|
+
| "="
|
|
3
|
+
| "!="
|
|
4
|
+
| ">"
|
|
5
|
+
| ">="
|
|
6
|
+
| "<="
|
|
7
|
+
| "<"
|
|
8
|
+
| "starts"
|
|
9
|
+
| "ends";
|
|
10
|
+
export type MultipleValueFilterClauseOp = "in";
|
|
11
|
+
export type FilterClauseOp =
|
|
12
|
+
| SingleValueFilterClauseOp
|
|
13
|
+
| MultipleValueFilterClauseOp;
|
|
14
|
+
export type FilterCombinatorOp = "and" | "or";
|
|
15
|
+
export type FilterOp = FilterClauseOp | FilterCombinatorOp;
|
|
16
|
+
|
|
17
|
+
const singleValueFilterOps = new Set<SingleValueFilterClauseOp>([
|
|
18
|
+
"=",
|
|
19
|
+
"!=",
|
|
20
|
+
">",
|
|
21
|
+
">=",
|
|
22
|
+
"<",
|
|
23
|
+
"<=",
|
|
24
|
+
"starts",
|
|
25
|
+
"ends",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export interface NamedFilter {
|
|
29
|
+
name?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SingleValueFilterClause extends NamedFilter {
|
|
33
|
+
op: SingleValueFilterClauseOp;
|
|
34
|
+
column: string;
|
|
35
|
+
value: string | number;
|
|
36
|
+
}
|
|
37
|
+
export interface MultiValueFilterClause extends NamedFilter {
|
|
38
|
+
op: MultipleValueFilterClauseOp;
|
|
39
|
+
column: string;
|
|
40
|
+
values: string[] | number[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type FilterClause = SingleValueFilterClause | MultiValueFilterClause;
|
|
44
|
+
|
|
45
|
+
export interface MultiClauseFilter extends NamedFilter {
|
|
46
|
+
column?: never;
|
|
47
|
+
op: FilterCombinatorOp;
|
|
48
|
+
filters: Filter[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AndFilter extends MultiClauseFilter {
|
|
52
|
+
op: "and";
|
|
53
|
+
}
|
|
54
|
+
export interface OrFilter extends MultiClauseFilter {
|
|
55
|
+
op: "or";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Filter = FilterClause | MultiClauseFilter;
|
|
59
|
+
|
|
60
|
+
// convenience methods to check filter type
|
|
61
|
+
export const isNamedFilter = (f?: Filter) =>
|
|
62
|
+
f !== undefined && f.name !== undefined;
|
|
63
|
+
|
|
64
|
+
// ... with type constraints
|
|
65
|
+
export const isSingleValueFilter = (f?: Filter): f is SingleValueFilterClause =>
|
|
66
|
+
f !== undefined &&
|
|
67
|
+
singleValueFilterOps.has(f.op as SingleValueFilterClauseOp);
|
|
68
|
+
|
|
69
|
+
export const isFilterClause = (
|
|
70
|
+
f?: Filter
|
|
71
|
+
): f is SingleValueFilterClause | MultiValueFilterClause =>
|
|
72
|
+
f !== undefined && (isSingleValueFilter(f) || isMultiValueFilter(f));
|
|
73
|
+
|
|
74
|
+
export const isMultiValueFilter = (f?: Filter): f is MultiValueFilterClause =>
|
|
75
|
+
f !== undefined && f.op === "in";
|
|
76
|
+
|
|
77
|
+
export const isInFilter = (f: Filter): f is MultiValueFilterClause =>
|
|
78
|
+
f.op === "in";
|
|
79
|
+
export const isAndFilter = (f: Filter): f is AndFilter => f.op === "and";
|
|
80
|
+
export const isOrFilter = (f: Filter): f is OrFilter => f.op === "or";
|
|
81
|
+
|
|
82
|
+
export function isMultiClauseFilter(f?: Filter): f is MultiClauseFilter {
|
|
83
|
+
return f !== undefined && (f.op === "and" || f.op === "or");
|
|
84
|
+
}
|
package/src/index.ts
ADDED