@xh/hoist 78.0.0-SNAPSHOT.1763665705643 → 78.0.0
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 +11 -5
- package/build/types/core/types/Types.d.ts +2 -0
- package/build/types/data/cube/BucketSpec.d.ts +22 -7
- package/build/types/data/cube/Cube.d.ts +1 -1
- package/build/types/data/cube/Query.d.ts +0 -2
- package/build/types/data/cube/View.d.ts +4 -3
- package/build/types/data/cube/row/BucketRow.d.ts +1 -1
- package/core/types/Types.ts +3 -0
- package/data/cube/BucketSpec.ts +29 -14
- package/data/cube/Cube.ts +1 -1
- package/data/cube/Query.ts +3 -5
- package/data/cube/View.ts +19 -11
- package/data/cube/row/BucketRow.ts +1 -1
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 78.0.0-
|
|
3
|
+
## 78.0.0 - 2025-11-21
|
|
4
4
|
|
|
5
5
|
### 💥 Breaking Changes
|
|
6
6
|
|
|
7
|
-
* `GridModel.cleanColumnState` is now private (not expected to impact applications).
|
|
8
7
|
* `GridModel.setColumnState` no longer patches existing column state, but instead replaces it
|
|
9
8
|
wholesale. Applications that were relying on the prior patching behavior will need to
|
|
10
9
|
call `GridModel.applyColumnStateChanges` instead.
|
|
10
|
+
* `GridModel.cleanColumnState` is now private (not expected to impact applications).
|
|
11
11
|
|
|
12
12
|
### 🎁 New Features
|
|
13
13
|
|
|
14
|
-
* `FieldFilter`
|
|
14
|
+
* Added new `FieldFilter` operators `not begins` and `not ends`.
|
|
15
|
+
* Added new optional `BucketSpec.dependentFields` config to the Cube API, allowing apps to ensure
|
|
16
|
+
proper re-bucketing of rows during data-only updates where those updates could affect bucketing
|
|
17
|
+
determinations made by the spec.
|
|
15
18
|
|
|
16
19
|
### 🐞 Bug Fixes
|
|
17
20
|
|
|
@@ -19,11 +22,14 @@
|
|
|
19
22
|
numerical ID.
|
|
20
23
|
* Fixed issue where newly added columns appearing in the Displayed Columns section of the column
|
|
21
24
|
chooser after loading grid state that was persisted before the columns were added to the grid.
|
|
25
|
+
* Removed a minor Cube `Query` annoyance - `dimensions` are now automatically added to the `fields`
|
|
26
|
+
list and do not need to be manually repeated there.
|
|
22
27
|
|
|
23
28
|
### ⚙️ Technical
|
|
24
29
|
|
|
25
|
-
*
|
|
26
|
-
|
|
30
|
+
* Improved documentation on `BucketSpec` class.
|
|
31
|
+
* Enhanced `FetchService` to recognize variants on the `application/json` content-type when
|
|
32
|
+
processing failed responses and decoding exceptions - e.g. `application/problem+json`.
|
|
27
33
|
|
|
28
34
|
## 77.1.1 - 2025-11-12
|
|
29
35
|
|
|
@@ -20,6 +20,8 @@ export type Thunkable<T> = T | (() => T);
|
|
|
20
20
|
export type Awaitable<T> = Promise<T> | T;
|
|
21
21
|
/** Convenience type for a "plain", string-keyed object holding any kind of values. */
|
|
22
22
|
export type PlainObject = Record<string, any>;
|
|
23
|
+
/** Convenience type to make a set of keys optional in a given type. */
|
|
24
|
+
export type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
23
25
|
/**
|
|
24
26
|
* Specification for debouncing in Hoist.
|
|
25
27
|
*
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { BaseRow } from './row/BaseRow';
|
|
2
|
+
import { SetOptional } from '@xh/hoist/core';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Spec to define a bucketing level within the hierarchy of data returned by a Query, as identified
|
|
5
|
+
* by a set of rows passed to a {@link BucketSpecFn} configured on that Query (or defaulted from
|
|
6
|
+
* the Cube). If this object is returned for a candidate set of rows, each row is evaluated by the
|
|
7
|
+
* spec's `bucketFn` to determine if it should yield a value for the bucket, causing the row to be
|
|
8
|
+
* nested underneath a new {@link BucketRow} created to hold all rows with that value.
|
|
4
9
|
*/
|
|
5
10
|
export declare class BucketSpec {
|
|
11
|
+
/** Name for the bucketing level configured by this spec - equivalent to a dimension name. */
|
|
6
12
|
name: string;
|
|
13
|
+
/**
|
|
14
|
+
* Function returning the bucketed value (if any) into which the given row should be placed -
|
|
15
|
+
* equivalent to a dimension value. Return null/undefined to exclude the row from bucketing.
|
|
16
|
+
*/
|
|
7
17
|
bucketFn: (row: BaseRow) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Function returning bucket row label from the bucket value string returned by bucketFn.
|
|
20
|
+
* Defaults to using the value directly.
|
|
21
|
+
*/
|
|
8
22
|
labelFn: (bucket: string) => string;
|
|
9
23
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
* Fields on which the `bucketFn` depends, to ensure rows are re-bucketed if dependent field
|
|
25
|
+
* values change. If not provided or does not cover all fields potentially accessed by
|
|
26
|
+
* `bucketFn`, an incremental "data only" update that should have changed a row's bucket can
|
|
27
|
+
* fail to do so.
|
|
28
|
+
*/
|
|
29
|
+
dependentFields: string[];
|
|
30
|
+
constructor(config: SetOptional<BucketSpec, 'labelFn' | 'dependentFields'>);
|
|
16
31
|
}
|
|
@@ -60,7 +60,7 @@ export type OmitFn = (row: AggregateRow | BucketRow) => boolean;
|
|
|
60
60
|
* aggregations and create an unwanted "Open" grouping.
|
|
61
61
|
*
|
|
62
62
|
* @param rows - the rows being checked for bucketing
|
|
63
|
-
* @returns BucketSpec for
|
|
63
|
+
* @returns {@link BucketSpec} for dynamic sub-aggregations, or null to perform no bucketing.
|
|
64
64
|
*/
|
|
65
65
|
export type BucketSpecFn = (rows: BaseRow[]) => BucketSpec;
|
|
66
66
|
/**
|
|
@@ -19,8 +19,6 @@ export interface QueryConfig {
|
|
|
19
19
|
* Fields or field names on which data should be grouped and aggregated. These are the ordered
|
|
20
20
|
* grouping levels in the resulting hierarchy - e.g. ['Country', 'State', 'City'].
|
|
21
21
|
*
|
|
22
|
-
* Any fields provided here must also be included in the `fields` array, if specified.
|
|
23
|
-
*
|
|
24
22
|
* If not provided or empty, the resulting data will not be grouped. Specify 'includeRoot' or
|
|
25
23
|
* 'includeLeaves' in that case, otherwise no data will be returned.
|
|
26
24
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HoistBase, PlainObject, Some } from '@xh/hoist/core';
|
|
2
|
-
import { Cube, CubeField, Filter, FilterLike, Query, QueryConfig, Store, StoreRecordId } from '@xh/hoist/data';
|
|
2
|
+
import { Cube, CubeField, Filter, FilterLike, Query, QueryConfig, Store, StoreChangeLog, StoreRecordId } from '@xh/hoist/data';
|
|
3
3
|
import { ViewRowData } from '@xh/hoist/data/cube/ViewRowData';
|
|
4
4
|
import { AggregationContext } from './aggregate/AggregationContext';
|
|
5
5
|
import { BaseRow } from './row/BaseRow';
|
|
@@ -51,6 +51,7 @@ export declare class View extends HoistBase {
|
|
|
51
51
|
private _rowDatas;
|
|
52
52
|
private _leafMap;
|
|
53
53
|
private _recordMap;
|
|
54
|
+
private _bucketDependentFields;
|
|
54
55
|
_aggContext: AggregationContext;
|
|
55
56
|
_rowCache: Map<string, BaseRow>;
|
|
56
57
|
/** @internal - applications should use {@link Cube.createView} */
|
|
@@ -79,7 +80,7 @@ export declare class View extends HoistBase {
|
|
|
79
80
|
/** Update the filter on the current Query.*/
|
|
80
81
|
setFilter(filter: FilterLike): void;
|
|
81
82
|
noteCubeLoaded(): void;
|
|
82
|
-
noteCubeUpdated(changeLog:
|
|
83
|
+
noteCubeUpdated(changeLog: StoreChangeLog): void;
|
|
83
84
|
private fullUpdate;
|
|
84
85
|
private dataOnlyUpdate;
|
|
85
86
|
private loadStores;
|
|
@@ -88,7 +89,7 @@ export declare class View extends HoistBase {
|
|
|
88
89
|
private groupAndInsertRecords;
|
|
89
90
|
private bucketRows;
|
|
90
91
|
private getSimpleUpdates;
|
|
91
|
-
private
|
|
92
|
+
private hasDimOrBucketUpdates;
|
|
92
93
|
private cachedRow;
|
|
93
94
|
private filterRecords;
|
|
94
95
|
private createAggregationContext;
|
|
@@ -5,7 +5,7 @@ import { View } from '../View';
|
|
|
5
5
|
/**
|
|
6
6
|
* Row within a dataset produced by a Cube / View representing aggregated data on a dimension that
|
|
7
7
|
* has been further grouped into a dynamic child "bucket" - a subset of the dimension-level
|
|
8
|
-
* {@link AggregateRow} produced as per a specified {@link
|
|
8
|
+
* {@link AggregateRow} produced as per a specified {@link BucketSpecFn}.
|
|
9
9
|
*
|
|
10
10
|
* This is an internal data structure - {@link ViewRowData} is the public row-level data API.
|
|
11
11
|
*/
|
package/core/types/Types.ts
CHANGED
|
@@ -34,6 +34,9 @@ export type Awaitable<T> = Promise<T> | T;
|
|
|
34
34
|
/** Convenience type for a "plain", string-keyed object holding any kind of values. */
|
|
35
35
|
export type PlainObject = Record<string, any>;
|
|
36
36
|
|
|
37
|
+
/** Convenience type to make a set of keys optional in a given type. */
|
|
38
|
+
export type SetOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* Specification for debouncing in Hoist.
|
|
39
42
|
*
|
package/data/cube/BucketSpec.ts
CHANGED
|
@@ -6,28 +6,43 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {BaseRow} from './row/BaseRow';
|
|
9
|
+
import {SetOptional} from '@xh/hoist/core';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Spec to define a bucketing level within the hierarchy of data returned by a Query, as identified
|
|
13
|
+
* by a set of rows passed to a {@link BucketSpecFn} configured on that Query (or defaulted from
|
|
14
|
+
* the Cube). If this object is returned for a candidate set of rows, each row is evaluated by the
|
|
15
|
+
* spec's `bucketFn` to determine if it should yield a value for the bucket, causing the row to be
|
|
16
|
+
* nested underneath a new {@link BucketRow} created to hold all rows with that value.
|
|
12
17
|
*/
|
|
13
18
|
export class BucketSpec {
|
|
19
|
+
/** Name for the bucketing level configured by this spec - equivalent to a dimension name. */
|
|
14
20
|
name: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Function returning the bucketed value (if any) into which the given row should be placed -
|
|
24
|
+
* equivalent to a dimension value. Return null/undefined to exclude the row from bucketing.
|
|
25
|
+
*/
|
|
15
26
|
bucketFn: (row: BaseRow) => string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Function returning bucket row label from the bucket value string returned by bucketFn.
|
|
30
|
+
* Defaults to using the value directly.
|
|
31
|
+
*/
|
|
16
32
|
labelFn: (bucket: string) => string;
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.labelFn = labelFn ?? (b => b);
|
|
35
|
+
* Fields on which the `bucketFn` depends, to ensure rows are re-bucketed if dependent field
|
|
36
|
+
* values change. If not provided or does not cover all fields potentially accessed by
|
|
37
|
+
* `bucketFn`, an incremental "data only" update that should have changed a row's bucket can
|
|
38
|
+
* fail to do so.
|
|
39
|
+
*/
|
|
40
|
+
dependentFields: string[];
|
|
41
|
+
|
|
42
|
+
constructor(config: SetOptional<BucketSpec, 'labelFn' | 'dependentFields'>) {
|
|
43
|
+
this.name = config.name;
|
|
44
|
+
this.bucketFn = config.bucketFn;
|
|
45
|
+
this.labelFn = config.labelFn ?? (b => b);
|
|
46
|
+
this.dependentFields = config.dependentFields ?? [];
|
|
32
47
|
}
|
|
33
48
|
}
|
package/data/cube/Cube.ts
CHANGED
|
@@ -82,7 +82,7 @@ export type OmitFn = (row: AggregateRow | BucketRow) => boolean;
|
|
|
82
82
|
* aggregations and create an unwanted "Open" grouping.
|
|
83
83
|
*
|
|
84
84
|
* @param rows - the rows being checked for bucketing
|
|
85
|
-
* @returns BucketSpec for
|
|
85
|
+
* @returns {@link BucketSpec} for dynamic sub-aggregations, or null to perform no bucketing.
|
|
86
86
|
*/
|
|
87
87
|
export type BucketSpecFn = (rows: BaseRow[]) => BucketSpec;
|
|
88
88
|
|
package/data/cube/Query.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
StoreRecord
|
|
17
17
|
} from '@xh/hoist/data';
|
|
18
18
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
19
|
-
import {find, isEqual} from 'lodash';
|
|
19
|
+
import {find, isEqual, uniq} from 'lodash';
|
|
20
20
|
import {Cube} from './Cube';
|
|
21
21
|
import {CubeField} from './CubeField';
|
|
22
22
|
|
|
@@ -40,8 +40,6 @@ export interface QueryConfig {
|
|
|
40
40
|
* Fields or field names on which data should be grouped and aggregated. These are the ordered
|
|
41
41
|
* grouping levels in the resulting hierarchy - e.g. ['Country', 'State', 'City'].
|
|
42
42
|
*
|
|
43
|
-
* Any fields provided here must also be included in the `fields` array, if specified.
|
|
44
|
-
*
|
|
45
43
|
* If not provided or empty, the resulting data will not be grouped. Specify 'includeRoot' or
|
|
46
44
|
* 'includeLeaves' in that case, otherwise no data will be returned.
|
|
47
45
|
*/
|
|
@@ -153,8 +151,8 @@ export class Query {
|
|
|
153
151
|
omitFn = cube.omitFn
|
|
154
152
|
}: QueryConfig) {
|
|
155
153
|
this.cube = cube;
|
|
156
|
-
this.fields = this.parseFields(fields);
|
|
157
154
|
this.dimensions = this.parseDimensions(dimensions);
|
|
155
|
+
this.fields = uniq([...this.parseFields(fields), ...(this.dimensions ?? [])]);
|
|
158
156
|
this.includeRoot = includeRoot;
|
|
159
157
|
this.includeLeaves = includeLeaves;
|
|
160
158
|
this.provideLeaves = provideLeaves;
|
|
@@ -234,7 +232,7 @@ export class Query {
|
|
|
234
232
|
private parseDimensions(raw: CubeField[] | string[]): CubeField[] {
|
|
235
233
|
if (!raw) return null;
|
|
236
234
|
if (raw[0] instanceof CubeField) return raw as CubeField[];
|
|
237
|
-
const {fields} = this;
|
|
235
|
+
const {fields} = this.cube;
|
|
238
236
|
return raw.map(name => {
|
|
239
237
|
const field = find(fields, {name});
|
|
240
238
|
throwIf(
|
package/data/cube/View.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
Query,
|
|
15
15
|
QueryConfig,
|
|
16
16
|
Store,
|
|
17
|
+
StoreChangeLog,
|
|
17
18
|
StoreRecord,
|
|
18
19
|
StoreRecordId
|
|
19
20
|
} from '@xh/hoist/data';
|
|
@@ -21,7 +22,7 @@ import {ViewRowData} from '@xh/hoist/data/cube/ViewRowData';
|
|
|
21
22
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
22
23
|
import {shallowEqualArrays} from '@xh/hoist/utils/impl';
|
|
23
24
|
import {logWithDebug, throwIf} from '@xh/hoist/utils/js';
|
|
24
|
-
import {castArray, find, forEach, groupBy, isEmpty, isNil, map} from 'lodash';
|
|
25
|
+
import {castArray, find, forEach, groupBy, isEmpty, isNil, map, uniq} from 'lodash';
|
|
25
26
|
import {AggregationContext} from './aggregate/AggregationContext';
|
|
26
27
|
import {AggregateRow} from './row/AggregateRow';
|
|
27
28
|
import {BaseRow} from './row/BaseRow';
|
|
@@ -94,6 +95,7 @@ export class View extends HoistBase {
|
|
|
94
95
|
private _rowDatas: ViewRowData[] = null;
|
|
95
96
|
private _leafMap: Map<StoreRecordId, LeafRow> = null;
|
|
96
97
|
private _recordMap: Map<StoreRecordId, StoreRecord> = null;
|
|
98
|
+
private _bucketDependentFields = new Set<string>();
|
|
97
99
|
_aggContext: AggregationContext = null;
|
|
98
100
|
_rowCache: Map<string, BaseRow> = null;
|
|
99
101
|
|
|
@@ -207,7 +209,7 @@ export class View extends HoistBase {
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
@action
|
|
210
|
-
noteCubeUpdated(changeLog:
|
|
212
|
+
noteCubeUpdated(changeLog: StoreChangeLog) {
|
|
211
213
|
const simpleUpdates = this.getSimpleUpdates(changeLog);
|
|
212
214
|
|
|
213
215
|
if (!simpleUpdates) {
|
|
@@ -277,6 +279,8 @@ export class View extends HoistBase {
|
|
|
277
279
|
{dimensions, includeRoot} = query,
|
|
278
280
|
rootId = 'root';
|
|
279
281
|
|
|
282
|
+
this._bucketDependentFields.clear();
|
|
283
|
+
|
|
280
284
|
const records = this._aggContext.filteredRecords;
|
|
281
285
|
const leafMap: Map<StoreRecordId, LeafRow> = new Map();
|
|
282
286
|
let newRows = this.groupAndInsertRecords(records, dimensions, rootId, {}, leafMap);
|
|
@@ -363,10 +367,12 @@ export class View extends HoistBase {
|
|
|
363
367
|
const bucketSpec = query.bucketSpecFn(rows);
|
|
364
368
|
if (!bucketSpec) return rows;
|
|
365
369
|
|
|
366
|
-
const {name: bucketName, bucketFn} = bucketSpec,
|
|
370
|
+
const {name: bucketName, bucketFn, dependentFields} = bucketSpec,
|
|
367
371
|
buckets: Record<string, BaseRow[]> = {},
|
|
368
372
|
ret: BaseRow[] = [];
|
|
369
373
|
|
|
374
|
+
dependentFields.forEach(it => this._bucketDependentFields.add(it));
|
|
375
|
+
|
|
370
376
|
// Determine which bucket to put this row into (if any)
|
|
371
377
|
rows.forEach(row => {
|
|
372
378
|
const bucketVal = bucketFn(row);
|
|
@@ -394,14 +400,14 @@ export class View extends HoistBase {
|
|
|
394
400
|
|
|
395
401
|
// return a list of simple data updates we can apply to leaves.
|
|
396
402
|
// false if leaf population changing, or aggregations are complex
|
|
397
|
-
private getSimpleUpdates(t): StoreRecord[] | false {
|
|
403
|
+
private getSimpleUpdates(t: StoreChangeLog): StoreRecord[] | false {
|
|
398
404
|
if (!t) return [];
|
|
399
405
|
if (!this.aggregatorsAreSimple) return false;
|
|
400
406
|
const {_leafMap, query} = this;
|
|
401
407
|
|
|
402
408
|
// 1) Simple case: no filter
|
|
403
409
|
if (!query.filter) {
|
|
404
|
-
return isEmpty(t.add) && isEmpty(t.remove) && !this.
|
|
410
|
+
return isEmpty(t.add) && isEmpty(t.remove) && !this.hasDimOrBucketUpdates(t.update)
|
|
405
411
|
? t.update
|
|
406
412
|
: false;
|
|
407
413
|
}
|
|
@@ -425,19 +431,21 @@ export class View extends HoistBase {
|
|
|
425
431
|
|
|
426
432
|
// 2c) Examine the final set of updates for any changes to dimension field values which would
|
|
427
433
|
// require rebuilding the row hierarchy
|
|
428
|
-
if (this.
|
|
434
|
+
if (this.hasDimOrBucketUpdates(ret)) return false;
|
|
429
435
|
|
|
430
436
|
return ret;
|
|
431
437
|
}
|
|
432
438
|
|
|
433
|
-
private
|
|
434
|
-
const {dimensions} = this.query
|
|
435
|
-
|
|
439
|
+
private hasDimOrBucketUpdates(update: StoreRecord[]): boolean {
|
|
440
|
+
const {dimensions} = this.query,
|
|
441
|
+
bucketDependentFields = Array.from(this._bucketDependentFields);
|
|
442
|
+
|
|
443
|
+
if (isEmpty(dimensions) && isEmpty(bucketDependentFields)) return false;
|
|
436
444
|
|
|
437
|
-
const
|
|
445
|
+
const fieldNames = uniq([...dimensions.map(it => it.name), ...bucketDependentFields]);
|
|
438
446
|
for (const rec of update) {
|
|
439
447
|
const curRec = this._leafMap.get(rec.id);
|
|
440
|
-
if (
|
|
448
|
+
if (fieldNames.some(name => rec.data[name] !== curRec.data[name])) return true;
|
|
441
449
|
}
|
|
442
450
|
|
|
443
451
|
return false;
|
|
@@ -13,7 +13,7 @@ import {View} from '../View';
|
|
|
13
13
|
/**
|
|
14
14
|
* Row within a dataset produced by a Cube / View representing aggregated data on a dimension that
|
|
15
15
|
* has been further grouped into a dynamic child "bucket" - a subset of the dimension-level
|
|
16
|
-
* {@link AggregateRow} produced as per a specified {@link
|
|
16
|
+
* {@link AggregateRow} produced as per a specified {@link BucketSpecFn}.
|
|
17
17
|
*
|
|
18
18
|
* This is an internal data structure - {@link ViewRowData} is the public row-level data API.
|
|
19
19
|
*/
|
package/package.json
CHANGED