@xh/hoist 78.0.0-SNAPSHOT.1763665705643 → 78.0.0-SNAPSHOT.1763737348746

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 CHANGED
@@ -4,14 +4,17 @@
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` implementation expanded to support `not begins` and `not ends` operators.
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,15 @@
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
- * `FetchService` will recognize variants on the `application/json` content-type when processing
26
- failed responses and decoding exceptions - e.g. `application/problem+json`.
30
+ * Updated the Cube API's `BucketSpecFn` to return either a concrete `BucketSpec` class instance (as
31
+ before) or a plain object conforming to the new `BucketSpecConfig` interface.
32
+ * Enhanced `FetchService` to recognize variants on the `application/json` content-type when
33
+ processing failed responses and decoding exceptions - e.g. `application/problem+json`.
27
34
 
28
35
  ## 77.1.1 - 2025-11-12
29
36
 
@@ -1,16 +1,36 @@
1
1
  import { BaseRow } from './row/BaseRow';
2
2
  /**
3
- * @see BucketSpecFn
3
+ * Spec to define a bucketing level within the hierarchy of data returned by a Query, as identified
4
+ * by a set of rows passed to a {@link BucketSpecFn} configured on that Query (or defaulted from
5
+ * the Cube). If this object is returned for a candidate set of rows, each row is evaluated by the
6
+ * spec's `bucketFn` to determine if it should yield a value for the bucket, causing the row to be
7
+ * nested underneath a new {@link BucketRow} created to hold all rows with that value.
4
8
  */
9
+ export interface BucketSpecConfig {
10
+ /** Name for the bucketing level configured by this spec - equivalent to a dimension name. */
11
+ name: string;
12
+ /**
13
+ * Function returning the bucketed value (if any) into which the given row should be placed -
14
+ * equivalent to a dimension value. Return null/undefined to exclude the row from bucketing.
15
+ */
16
+ bucketFn: (row: BaseRow) => string;
17
+ /**
18
+ * Function returning bucket row label from the bucket value string returned by bucketFn.
19
+ * Defaults to using the value directly.
20
+ */
21
+ labelFn?: (bucket: string) => string;
22
+ /**
23
+ * Fields on which the `bucketFn` depends, to ensure rows are re-bucketed if dependent field
24
+ * values change. If not provided or does not cover all fields potentially accessed by
25
+ * `bucketFn`, an incremental "data only" update that should have changed a row's bucket can
26
+ * fail to do so.
27
+ */
28
+ dependentFields?: string[];
29
+ }
5
30
  export declare class BucketSpec {
6
31
  name: string;
7
32
  bucketFn: (row: BaseRow) => string;
8
33
  labelFn: (bucket: string) => string;
9
- /**
10
- * @param name - name of bucket.
11
- * @param bucketFn - function to determine which (if any) bucket the given row should
12
- * be placed into
13
- * @param labelFn - function to generate the bucket row label from name returned by bucketFn
14
- **/
15
- constructor(name: string, bucketFn: (row: BaseRow) => string, labelFn?: (bucket: string) => string);
34
+ dependentFields: string[];
35
+ constructor(config: BucketSpecConfig);
16
36
  }
@@ -8,7 +8,7 @@ import { StoreRecord } from '../StoreRecord';
8
8
  import { AggregateRow } from './row/AggregateRow';
9
9
  import { BucketRow } from './row/BucketRow';
10
10
  import { BaseRow } from './row/BaseRow';
11
- import { BucketSpec } from './BucketSpec';
11
+ import { BucketSpec, BucketSpecConfig } from './BucketSpec';
12
12
  export interface CubeConfig {
13
13
  fields: CubeField[] | CubeFieldSpec[];
14
14
  /** Default configs applied to all `CubeField`s constructed internally by this Cube. */
@@ -60,9 +60,9 @@ 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 configuring dynamic sub-aggregations, or null to perform no bucketing.
63
+ * @returns {@link BucketSpecConfig} for dynamic sub-aggregations, or null to perform no bucketing.
64
64
  */
65
- export type BucketSpecFn = (rows: BaseRow[]) => BucketSpec;
65
+ export type BucketSpecFn = (rows: BaseRow[]) => BucketSpecConfig | BucketSpec;
66
66
  /**
67
67
  * A data store that supports grouping, aggregating, and filtering data on multiple dimensions.
68
68
  *
@@ -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
  */
@@ -81,7 +79,7 @@ export interface QueryConfig {
81
79
  *
82
80
  * This can be used to break selected aggregations into sub-groups dynamically, without having
83
81
  * to define another dimension in the Cube and have it apply to all aggregations. See the
84
- * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
82
+ * {@link BucketSpecFn} type and {@link BucketSpecConfig} interface for additional information.
85
83
  *
86
84
  * Defaults to {@link Cube.bucketSpecFn}.
87
85
  */
@@ -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: PlainObject): void;
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 hasDimUpdates;
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 Query.bucketSpecFn}.
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
  */
@@ -8,26 +8,44 @@
8
8
  import {BaseRow} from './row/BaseRow';
9
9
 
10
10
  /**
11
- * @see BucketSpecFn
11
+ * Spec to define a bucketing level within the hierarchy of data returned by a Query, as identified
12
+ * by a set of rows passed to a {@link BucketSpecFn} configured on that Query (or defaulted from
13
+ * the Cube). If this object is returned for a candidate set of rows, each row is evaluated by the
14
+ * spec's `bucketFn` to determine if it should yield a value for the bucket, causing the row to be
15
+ * nested underneath a new {@link BucketRow} created to hold all rows with that value.
12
16
  */
17
+ export interface BucketSpecConfig {
18
+ /** Name for the bucketing level configured by this spec - equivalent to a dimension name. */
19
+ name: string;
20
+ /**
21
+ * Function returning the bucketed value (if any) into which the given row should be placed -
22
+ * equivalent to a dimension value. Return null/undefined to exclude the row from bucketing.
23
+ */
24
+ bucketFn: (row: BaseRow) => string;
25
+ /**
26
+ * Function returning bucket row label from the bucket value string returned by bucketFn.
27
+ * Defaults to using the value directly.
28
+ */
29
+ labelFn?: (bucket: string) => string;
30
+ /**
31
+ * Fields on which the `bucketFn` depends, to ensure rows are re-bucketed if dependent field
32
+ * values change. If not provided or does not cover all fields potentially accessed by
33
+ * `bucketFn`, an incremental "data only" update that should have changed a row's bucket can
34
+ * fail to do so.
35
+ */
36
+ dependentFields?: string[];
37
+ }
38
+
13
39
  export class BucketSpec {
14
40
  name: string;
15
41
  bucketFn: (row: BaseRow) => string;
16
42
  labelFn: (bucket: string) => string;
43
+ dependentFields: string[];
17
44
 
18
- /**
19
- * @param name - name of bucket.
20
- * @param bucketFn - function to determine which (if any) bucket the given row should
21
- * be placed into
22
- * @param labelFn - function to generate the bucket row label from name returned by bucketFn
23
- **/
24
- constructor(
25
- name: string,
26
- bucketFn: (row: BaseRow) => string,
27
- labelFn?: (bucket: string) => string
28
- ) {
29
- this.name = name;
30
- this.bucketFn = bucketFn;
31
- this.labelFn = labelFn ?? (b => b);
45
+ constructor(config: BucketSpecConfig) {
46
+ this.name = config.name;
47
+ this.bucketFn = config.bucketFn;
48
+ this.labelFn = config.labelFn ?? (b => b);
49
+ this.dependentFields = config.dependentFields ?? [];
32
50
  }
33
51
  }
package/data/cube/Cube.ts CHANGED
@@ -17,7 +17,7 @@ import {StoreRecord} from '../StoreRecord';
17
17
  import {AggregateRow} from './row/AggregateRow';
18
18
  import {BucketRow} from './row/BucketRow';
19
19
  import {BaseRow} from './row/BaseRow';
20
- import {BucketSpec} from './BucketSpec';
20
+ import {BucketSpec, BucketSpecConfig} from './BucketSpec';
21
21
  import {defaultsDeep, isEmpty} from 'lodash';
22
22
 
23
23
  export interface CubeConfig {
@@ -82,9 +82,9 @@ 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 configuring dynamic sub-aggregations, or null to perform no bucketing.
85
+ * @returns {@link BucketSpecConfig} for dynamic sub-aggregations, or null to perform no bucketing.
86
86
  */
87
- export type BucketSpecFn = (rows: BaseRow[]) => BucketSpec;
87
+ export type BucketSpecFn = (rows: BaseRow[]) => BucketSpecConfig | BucketSpec;
88
88
 
89
89
  /**
90
90
  * A data store that supports grouping, aggregating, and filtering data on multiple dimensions.
@@ -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
  */
@@ -109,7 +107,7 @@ export interface QueryConfig {
109
107
  *
110
108
  * This can be used to break selected aggregations into sub-groups dynamically, without having
111
109
  * to define another dimension in the Cube and have it apply to all aggregations. See the
112
- * {@link BucketSpecFn} type and {@link BucketSpec} interface for additional information.
110
+ * {@link BucketSpecFn} type and {@link BucketSpecConfig} interface for additional information.
113
111
  *
114
112
  * Defaults to {@link Cube.bucketSpecFn}.
115
113
  */
@@ -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,14 +14,16 @@ 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
+ import {BucketSpec} from '@xh/hoist/data/cube/BucketSpec';
20
22
  import {ViewRowData} from '@xh/hoist/data/cube/ViewRowData';
21
23
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
22
24
  import {shallowEqualArrays} from '@xh/hoist/utils/impl';
23
25
  import {logWithDebug, throwIf} from '@xh/hoist/utils/js';
24
- import {castArray, find, forEach, groupBy, isEmpty, isNil, map} from 'lodash';
26
+ import {castArray, find, forEach, groupBy, isEmpty, isNil, map, uniq} from 'lodash';
25
27
  import {AggregationContext} from './aggregate/AggregationContext';
26
28
  import {AggregateRow} from './row/AggregateRow';
27
29
  import {BaseRow} from './row/BaseRow';
@@ -94,6 +96,7 @@ export class View extends HoistBase {
94
96
  private _rowDatas: ViewRowData[] = null;
95
97
  private _leafMap: Map<StoreRecordId, LeafRow> = null;
96
98
  private _recordMap: Map<StoreRecordId, StoreRecord> = null;
99
+ private _bucketDependentFields = new Set<string>();
97
100
  _aggContext: AggregationContext = null;
98
101
  _rowCache: Map<string, BaseRow> = null;
99
102
 
@@ -207,7 +210,7 @@ export class View extends HoistBase {
207
210
  }
208
211
 
209
212
  @action
210
- noteCubeUpdated(changeLog: PlainObject) {
213
+ noteCubeUpdated(changeLog: StoreChangeLog) {
211
214
  const simpleUpdates = this.getSimpleUpdates(changeLog);
212
215
 
213
216
  if (!simpleUpdates) {
@@ -277,6 +280,8 @@ export class View extends HoistBase {
277
280
  {dimensions, includeRoot} = query,
278
281
  rootId = 'root';
279
282
 
283
+ this._bucketDependentFields.clear();
284
+
280
285
  const records = this._aggContext.filteredRecords;
281
286
  const leafMap: Map<StoreRecordId, LeafRow> = new Map();
282
287
  let newRows = this.groupAndInsertRecords(records, dimensions, rootId, {}, leafMap);
@@ -360,13 +365,19 @@ export class View extends HoistBase {
360
365
  if (!query.bucketSpecFn) return rows;
361
366
  if (!query.includeLeaves && rows[0]?.isLeaf) return rows;
362
367
 
363
- const bucketSpec = query.bucketSpecFn(rows);
364
- if (!bucketSpec) return rows;
368
+ const bucketSpecOrConf = query.bucketSpecFn(rows);
369
+ if (!bucketSpecOrConf) return rows;
365
370
 
366
- const {name: bucketName, bucketFn} = bucketSpec,
371
+ const bucketSpec =
372
+ bucketSpecOrConf instanceof BucketSpec
373
+ ? bucketSpecOrConf
374
+ : new BucketSpec(bucketSpecOrConf);
375
+ const {name: bucketName, bucketFn, dependentFields} = bucketSpec,
367
376
  buckets: Record<string, BaseRow[]> = {},
368
377
  ret: BaseRow[] = [];
369
378
 
379
+ dependentFields.forEach(it => this._bucketDependentFields.add(it));
380
+
370
381
  // Determine which bucket to put this row into (if any)
371
382
  rows.forEach(row => {
372
383
  const bucketVal = bucketFn(row);
@@ -394,14 +405,14 @@ export class View extends HoistBase {
394
405
 
395
406
  // return a list of simple data updates we can apply to leaves.
396
407
  // false if leaf population changing, or aggregations are complex
397
- private getSimpleUpdates(t): StoreRecord[] | false {
408
+ private getSimpleUpdates(t: StoreChangeLog): StoreRecord[] | false {
398
409
  if (!t) return [];
399
410
  if (!this.aggregatorsAreSimple) return false;
400
411
  const {_leafMap, query} = this;
401
412
 
402
413
  // 1) Simple case: no filter
403
414
  if (!query.filter) {
404
- return isEmpty(t.add) && isEmpty(t.remove) && !this.hasDimUpdates(t.update)
415
+ return isEmpty(t.add) && isEmpty(t.remove) && !this.hasDimOrBucketUpdates(t.update)
405
416
  ? t.update
406
417
  : false;
407
418
  }
@@ -425,19 +436,21 @@ export class View extends HoistBase {
425
436
 
426
437
  // 2c) Examine the final set of updates for any changes to dimension field values which would
427
438
  // require rebuilding the row hierarchy
428
- if (this.hasDimUpdates(ret)) return false;
439
+ if (this.hasDimOrBucketUpdates(ret)) return false;
429
440
 
430
441
  return ret;
431
442
  }
432
443
 
433
- private hasDimUpdates(update: StoreRecord[]): boolean {
434
- const {dimensions} = this.query;
435
- if (isEmpty(dimensions)) return false;
444
+ private hasDimOrBucketUpdates(update: StoreRecord[]): boolean {
445
+ const {dimensions} = this.query,
446
+ bucketDependentFields = Array.from(this._bucketDependentFields);
447
+
448
+ if (isEmpty(dimensions) && isEmpty(bucketDependentFields)) return false;
436
449
 
437
- const dimNames = dimensions.map(it => it.name);
450
+ const fieldNames = uniq([...dimensions.map(it => it.name), ...bucketDependentFields]);
438
451
  for (const rec of update) {
439
452
  const curRec = this._leafMap.get(rec.id);
440
- if (dimNames.some(name => rec.data[name] !== curRec.data[name])) return true;
453
+ if (fieldNames.some(name => rec.data[name] !== curRec.data[name])) return true;
441
454
  }
442
455
 
443
456
  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 Query.bucketSpecFn}.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "78.0.0-SNAPSHOT.1763665705643",
3
+ "version": "78.0.0-SNAPSHOT.1763737348746",
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",