@xh/hoist 75.0.0-SNAPSHOT.1753294066799 → 75.0.0-SNAPSHOT.1753376908830
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/CHANGELOG.md +6 -0
- package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
- package/build/types/data/filter/FieldFilter.d.ts +3 -0
- package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +1 -1
- package/cmp/filter/FilterChooserModel.ts +106 -38
- package/data/filter/FieldFilter.ts +4 -0
- package/desktop/cmp/button/grid/ExpandToLevelButton.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
which `dimensions` are provided to the model.
|
|
17
17
|
* The usage of the `RelativeTimestamp` component has been streamlined by deprecating the `options`
|
|
18
18
|
prop. All `RelativeTimestampOptions` are now supported by this component as top-level props.
|
|
19
|
+
* `FilterChooserModel` has been enhanced to better handle multiple simultaneous filters with
|
|
20
|
+
different `op`s on the same field. "Inclusive" ops (e.g. `=`, `like`) will be OR'ed together,
|
|
21
|
+
"Exclusive" ops (e.g. `!=`, `not like`) will be AND'ed together and range ops (e.g. `<`, `>` )
|
|
22
|
+
will use a heuristic to create a meaningful query that will actually return results. This
|
|
23
|
+
behavior is consistent with current behavior and user intuition, and should maximize the ability
|
|
24
|
+
to create useful queries.
|
|
19
25
|
|
|
20
26
|
### 🐞 Bug Fixes
|
|
21
27
|
|
|
@@ -95,7 +95,6 @@ export declare class FilterChooserModel extends HoistModel {
|
|
|
95
95
|
*/
|
|
96
96
|
setValue(rawValue: FilterLike): void;
|
|
97
97
|
setSelectValue(selectValue: string[]): void;
|
|
98
|
-
toDisplayFilters(filter: Filter): any[];
|
|
99
98
|
queryAsync(query: string): Promise<FilterChooserOption[]>;
|
|
100
99
|
autoComplete(value: any): void;
|
|
101
100
|
createFilterOption(filter: Filter): FilterChooserOption;
|
|
@@ -114,6 +113,9 @@ export declare class FilterChooserModel extends HoistModel {
|
|
|
114
113
|
getFieldSpec(fieldName: string): FilterChooserFieldSpec;
|
|
115
114
|
validateFilter(f: Filter): f is FilterChooserFilter;
|
|
116
115
|
getDefaultIntroHelpText(): string;
|
|
116
|
+
private toValueFilter;
|
|
117
|
+
private toDisplayFilters;
|
|
118
|
+
private implicitOrFieldFilters;
|
|
117
119
|
private initPersist;
|
|
118
120
|
private setValueInternal;
|
|
119
121
|
/**
|
|
@@ -17,6 +17,9 @@ export declare class FieldFilter extends Filter {
|
|
|
17
17
|
readonly value: any;
|
|
18
18
|
static OPERATORS: string[];
|
|
19
19
|
static ARRAY_OPERATORS: string[];
|
|
20
|
+
static INCLUDE_LIKE_OPERATORS: string[];
|
|
21
|
+
static EXCLUDE_LIKE_OPERATORS: string[];
|
|
22
|
+
static RANGE_LIKE_OPERATORS: string[];
|
|
20
23
|
/**
|
|
21
24
|
* Constructor - not typically called by apps - create via {@link parseFilter} instead.
|
|
22
25
|
* @internal
|
|
@@ -9,7 +9,7 @@ export interface ExpandToLevelButtonProps extends Omit<ButtonProps, 'title'> {
|
|
|
9
9
|
/** Position for menu, as per Blueprint docs. */
|
|
10
10
|
popoverPosition?: Position;
|
|
11
11
|
/** Title for the menu popover - defaults to "Expand To Level". */
|
|
12
|
-
title
|
|
12
|
+
title?: ReactNode;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* A menu button to expand a multi-level grouped or tree grid out to a desired level.
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
XH
|
|
16
16
|
} from '@xh/hoist/core';
|
|
17
17
|
import {
|
|
18
|
-
combineValueFilters,
|
|
19
18
|
CompoundFilter,
|
|
20
19
|
FieldFilter,
|
|
21
20
|
Filter,
|
|
@@ -32,6 +31,8 @@ import {createObservableRef} from '@xh/hoist/utils/react';
|
|
|
32
31
|
import {
|
|
33
32
|
cloneDeep,
|
|
34
33
|
compact,
|
|
34
|
+
every,
|
|
35
|
+
first,
|
|
35
36
|
flatMap,
|
|
36
37
|
flatten,
|
|
37
38
|
forEach,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
isFinite,
|
|
42
43
|
isObject,
|
|
43
44
|
isString,
|
|
45
|
+
map,
|
|
44
46
|
partition,
|
|
45
47
|
sortBy,
|
|
46
48
|
uniq,
|
|
@@ -222,48 +224,12 @@ export class FilterChooserModel extends HoistModel {
|
|
|
222
224
|
const [filters, suggestions] = partition(parsedValues, 'op');
|
|
223
225
|
|
|
224
226
|
// Round-trip actual filters through main value setter above.
|
|
225
|
-
this.setValue(
|
|
227
|
+
this.setValue(this.toValueFilter(filters));
|
|
226
228
|
|
|
227
229
|
// And then programmatically re-enter any suggestion
|
|
228
230
|
if (suggestions.length === 1) this.autoComplete(suggestions[0]);
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
// Transfer the value filter to the canonical set of individual filters for display.
|
|
232
|
-
// Filters with arrays values will be split.
|
|
233
|
-
toDisplayFilters(filter: Filter) {
|
|
234
|
-
if (!filter) return [];
|
|
235
|
-
|
|
236
|
-
let ret;
|
|
237
|
-
const unsupported = s => {
|
|
238
|
-
throw XH.exception(`Unsupported Filter in FilterChooserModel: ${s}`);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// 1) Flatten CompoundFilters across disparate fields to FieldFilters.
|
|
242
|
-
if (filter instanceof CompoundFilter && !filter.field) {
|
|
243
|
-
ret = filter.filters;
|
|
244
|
-
} else {
|
|
245
|
-
ret = [filter];
|
|
246
|
-
}
|
|
247
|
-
if (ret.some(f => !(f instanceof FieldFilter) && !(f instanceof CompoundFilter))) {
|
|
248
|
-
unsupported('Filters must be FieldFilters or CompoundFilters.');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 2) Recognize unsupported multiple filters for array-based filters.
|
|
252
|
-
const groupMap = groupBy(ret, ({op, field}) => `${op}|${field}`);
|
|
253
|
-
forEach(groupMap, filters => {
|
|
254
|
-
const {op} = filters[0];
|
|
255
|
-
if (filters.length > 1 && FieldFilter.ARRAY_OPERATORS.includes(op)) {
|
|
256
|
-
unsupported(`Multiple filters cannot be provided with ${op} operator`);
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// 3) Finally unroll multi-value filters to one value per filter.
|
|
261
|
-
// The multiple values will later be restored.
|
|
262
|
-
return flatMap(ret, f => {
|
|
263
|
-
return isArray(f.value) ? f.value.map(value => new FieldFilter({...f, value})) : f;
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
233
|
//-------------
|
|
268
234
|
// Querying
|
|
269
235
|
//---------------
|
|
@@ -411,6 +377,108 @@ export class FilterChooserModel extends HoistModel {
|
|
|
411
377
|
// -------------------------------
|
|
412
378
|
// Implementation
|
|
413
379
|
// -------------------------------
|
|
380
|
+
|
|
381
|
+
// Take the raw flat displayed FieldFilter specs and combine them with appropriate semantics
|
|
382
|
+
// into a proper Filter for the value of ths model. Field Filters on the same field are going
|
|
383
|
+
// to be combined into a FieldFilter with array values as well as potentially appropriate
|
|
384
|
+
// compound filters to combine different ops. See toDisplayFilters() for the inverse of this
|
|
385
|
+
// operation.
|
|
386
|
+
private toValueFilter(specs: FieldFilterSpec[] = []): FilterLike {
|
|
387
|
+
const ret: FilterLike[] = [];
|
|
388
|
+
|
|
389
|
+
// group filters by field -- we'll produce up to two ANDable filters per field
|
|
390
|
+
const fieldMap = groupBy(specs, 'field');
|
|
391
|
+
forEach(fieldMap, specs => {
|
|
392
|
+
// a) combine filters with SAME operator in to a single FieldFilter
|
|
393
|
+
const opMap = groupBy(specs, 'op');
|
|
394
|
+
specs = flatMap(opMap, specs => {
|
|
395
|
+
const firstSpec = first(specs);
|
|
396
|
+
return specs.length > 1 && FieldFilter.ARRAY_OPERATORS.includes(firstSpec.op)
|
|
397
|
+
? {...firstSpec, value: map(specs, 'value')}
|
|
398
|
+
: specs;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Process like operators together, potentially creating sub-OR clauses.
|
|
402
|
+
[
|
|
403
|
+
FieldFilter.INCLUDE_LIKE_OPERATORS,
|
|
404
|
+
FieldFilter.EXCLUDE_LIKE_OPERATORS,
|
|
405
|
+
FieldFilter.RANGE_LIKE_OPERATORS
|
|
406
|
+
].forEach(type => {
|
|
407
|
+
const filters = specs.filter(s => type.includes(s.op));
|
|
408
|
+
if (this.implicitOrFieldFilters(filters)) {
|
|
409
|
+
ret.push([{op: 'OR', filters}]);
|
|
410
|
+
} else {
|
|
411
|
+
ret.push(...filters);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return ret;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Transfer the value filter to the canonical set of individual filters for display.
|
|
420
|
+
// See toValueFilter() for the inverse of this operation.
|
|
421
|
+
private toDisplayFilters(filter: Filter): Filter[] {
|
|
422
|
+
if (!filter) return [];
|
|
423
|
+
const unsupported = s => {
|
|
424
|
+
throw XH.exception(`Unsupported Filter in FilterChooserModel: ${s}`);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
let ret: Filter[] = [filter];
|
|
428
|
+
|
|
429
|
+
// 0) Can always unwind the top level AND -- its implicit.
|
|
430
|
+
if (filter instanceof CompoundFilter && filter.op == 'AND') {
|
|
431
|
+
ret = filter.filters;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 1) Further flatten 2nd-Level CompoundFilters to FieldFilters.
|
|
435
|
+
// OR'ed filters on the same field can be decomposed if they will later be re-combined
|
|
436
|
+
ret = ret.flatMap(f => {
|
|
437
|
+
return f instanceof CompoundFilter &&
|
|
438
|
+
(f.op == 'AND' ||
|
|
439
|
+
(f.field && this.implicitOrFieldFilters(f.filters as FieldFilter[])))
|
|
440
|
+
? f.filters
|
|
441
|
+
: f;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// 2) Recognize misc unsupported filters.
|
|
445
|
+
if (!ret.every(f => f instanceof FieldFilter || f instanceof CompoundFilter)) {
|
|
446
|
+
unsupported('Filters must be FieldFilters or CompoundFilters.');
|
|
447
|
+
}
|
|
448
|
+
const fieldFilters = ret.filter(it => it instanceof FieldFilter) as FieldFilter[];
|
|
449
|
+
const groupMap = groupBy(fieldFilters, ({op, field}) => `${op}|${field}`);
|
|
450
|
+
forEach(groupMap, filters => {
|
|
451
|
+
const {op} = filters[0];
|
|
452
|
+
if (filters.length > 1 && FieldFilter.ARRAY_OPERATORS.includes(op)) {
|
|
453
|
+
unsupported(`Multiple filters cannot be provided with ${op} operator`);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// 3) Finally unroll multi-value filters to one filter per value.
|
|
458
|
+
return flatMap(ret, f => {
|
|
459
|
+
return f instanceof FieldFilter && isArray(f.value)
|
|
460
|
+
? f.value.map(value => new FieldFilter({...f, value}))
|
|
461
|
+
: f;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Should Field Filters on a particular Field be implicitly OR'ed together?
|
|
466
|
+
private implicitOrFieldFilters(filters: Array<FieldFilterSpec | FieldFilter>): boolean {
|
|
467
|
+
const {INCLUDE_LIKE_OPERATORS, RANGE_LIKE_OPERATORS} = FieldFilter;
|
|
468
|
+
if (filters.length < 2) return false;
|
|
469
|
+
|
|
470
|
+
// For INCLUDE_LIKE, treat them like "equals" and OR them
|
|
471
|
+
if (every(filters, f => INCLUDE_LIKE_OPERATORS.includes(f.op))) return true;
|
|
472
|
+
|
|
473
|
+
// For RANGE_LIKE, recognize simple "exterior" bifurcated range as an OR, otherwise AND
|
|
474
|
+
if (filters.length == 2 && every(filters, f => RANGE_LIKE_OPERATORS.includes(f.op))) {
|
|
475
|
+
const [a, b] = sortBy(filters, 'op');
|
|
476
|
+
return a.op.startsWith('<') && b.op.startsWith('>') && a.value <= b.value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
414
482
|
private initPersist({
|
|
415
483
|
persistValue = true,
|
|
416
484
|
persistFavorites = true,
|
|
@@ -69,6 +69,10 @@ export class FieldFilter extends Filter {
|
|
|
69
69
|
'excludes'
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
+
static INCLUDE_LIKE_OPERATORS = ['=', 'like', 'begins', 'ends', 'includes'];
|
|
73
|
+
static EXCLUDE_LIKE_OPERATORS = ['!=', 'not like', 'excludes'];
|
|
74
|
+
static RANGE_LIKE_OPERATORS = ['>', '>=', '<', '<='];
|
|
75
|
+
|
|
72
76
|
/**
|
|
73
77
|
* Constructor - not typically called by apps - create via {@link parseFilter} instead.
|
|
74
78
|
* @internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "75.0.0-SNAPSHOT.
|
|
3
|
+
"version": "75.0.0-SNAPSHOT.1753376908830",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|