@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.
@@ -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
@@ -0,0 +1,4 @@
1
+ export * from "./filter-input";
2
+ export * from "./filter-toolbar";
3
+ export * from "./filter-utils";
4
+ export * from "./filterTypes";