@xh/hoist 59.3.2 → 59.4.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -3
  2. package/admin/differ/DifferModel.ts +5 -7
  3. package/appcontainer/AppContainerModel.ts +8 -10
  4. package/appcontainer/AppStateModel.ts +3 -2
  5. package/appcontainer/PageStateModel.ts +1 -2
  6. package/appcontainer/SizingModeModel.ts +2 -2
  7. package/cmp/ag-grid/AgGrid.ts +4 -3
  8. package/cmp/ag-grid/AgGridModel.ts +8 -9
  9. package/cmp/chart/Chart.ts +4 -3
  10. package/cmp/dataview/DataViewModel.ts +2 -2
  11. package/cmp/filter/FilterChooserModel.ts +5 -5
  12. package/cmp/grid/Grid.ts +2 -2
  13. package/cmp/grid/GridContextMenu.ts +2 -2
  14. package/cmp/grid/GridModel.ts +13 -21
  15. package/cmp/grid/Types.ts +6 -5
  16. package/cmp/grid/columns/Column.ts +12 -12
  17. package/cmp/grid/columns/ColumnGroup.ts +17 -6
  18. package/cmp/grid/helpers/GridCountLabel.ts +5 -4
  19. package/cmp/grid/impl/ColumnWidthCalculator.ts +3 -3
  20. package/cmp/grid/impl/GridPersistenceModel.ts +11 -4
  21. package/cmp/grid/renderers/MultiFieldRenderer.ts +28 -22
  22. package/cmp/grouping/GroupingChooserModel.ts +2 -2
  23. package/cmp/tab/TabContainerModel.ts +2 -2
  24. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +10 -4
  25. package/core/HoistBase.ts +44 -5
  26. package/core/elem.ts +2 -2
  27. package/core/impl/InstallServices.ts +2 -8
  28. package/core/load/LoadSupport.ts +3 -3
  29. package/core/model/HoistModel.ts +1 -1
  30. package/data/Store.ts +1 -1
  31. package/data/UrlStore.ts +3 -3
  32. package/data/filter/CompoundFilter.ts +5 -3
  33. package/data/filter/FieldFilter.ts +4 -3
  34. package/data/filter/Filter.ts +2 -3
  35. package/data/filter/FunctionFilter.ts +2 -1
  36. package/data/impl/RecordSet.ts +5 -5
  37. package/desktop/appcontainer/ToastSource.ts +1 -1
  38. package/desktop/cmp/rest/Actions.ts +15 -9
  39. package/desktop/cmp/treemap/TreeMap.ts +4 -8
  40. package/package.json +1 -1
  41. package/svc/AutoRefreshService.ts +3 -3
  42. package/svc/FetchService.ts +5 -5
  43. package/svc/GridAutosizeService.ts +4 -7
  44. package/svc/TrackService.ts +6 -6
  45. package/svc/WebSocketService.ts +14 -15
  46. package/utils/async/AsyncUtils.ts +3 -2
  47. package/utils/async/Timer.ts +4 -3
  48. package/utils/js/BrowserUtils.ts +8 -8
  49. package/utils/js/LangUtils.ts +10 -9
  50. package/utils/js/LogUtils.ts +66 -26
  51. package/utils/react/LayoutPropUtils.ts +3 -3
@@ -4,11 +4,11 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {GridModel} from '../GridModel';
8
7
  import {box} from '@xh/hoist/cmp/layout';
9
8
  import {BoxProps, hoistCmp, HoistProps, useContextModel} from '@xh/hoist/core';
10
9
  import {fmtNumber} from '@xh/hoist/format';
11
- import {pluralize, singularize, withDefault} from '@xh/hoist/utils/js';
10
+ import {logError, pluralize, singularize, withDefault} from '@xh/hoist/utils/js';
11
+ import {GridModel} from '../GridModel';
12
12
 
13
13
  export interface GridCountLabelProps extends HoistProps, BoxProps {
14
14
  /** GridModel to which this component should bind. */
@@ -49,8 +49,9 @@ export const [GridCountLabel, gridCountLabel] = hoistCmp.withFactory<GridCountLa
49
49
  gridModel = withDefault(gridModel, useContextModel(GridModel));
50
50
 
51
51
  if (!gridModel) {
52
- console.error(
53
- "No GridModel available to GridCountLabel. Provide via a 'gridModel' prop, or context."
52
+ logError(
53
+ `GridModel not found - provide via 'gridModel' prop or context.`,
54
+ GridCountLabel
54
55
  );
55
56
  return '';
56
57
  }
@@ -9,7 +9,7 @@ import {GridAutosizeOptions} from '@xh/hoist/cmp/grid/GridAutosizeOptions';
9
9
  import {XH} from '@xh/hoist/core';
10
10
  import {CompoundFilter, FieldFilter, Filter, StoreRecord} from '@xh/hoist/data';
11
11
  import {forEachAsync} from '@xh/hoist/utils/async';
12
- import {stripTags} from '@xh/hoist/utils/js';
12
+ import {logWarn, stripTags} from '@xh/hoist/utils/js';
13
13
  import {
14
14
  forOwn,
15
15
  groupBy,
@@ -74,7 +74,7 @@ export class ColumnWidthCalculator {
74
74
  try {
75
75
  return this.getHeaderWidth(gridModel, column, autosizeIncludeHeaderIcons, bufferPx);
76
76
  } catch (e) {
77
- console.warn(`Error calculating max header width for column "${column.colId}".`, e);
77
+ logWarn([`Error calculating max header width for colId '${column.colId}'.`, e], this);
78
78
  } finally {
79
79
  this.resetHeaderClassNames();
80
80
  }
@@ -104,7 +104,7 @@ export class ColumnWidthCalculator {
104
104
  return await this.calcLevelWidthAsync(gridModel, records, column, options);
105
105
  }
106
106
  } catch (e) {
107
- console.warn(`Error calculating max data width for column "${column.colId}".`, e);
107
+ logWarn([`Error calculating max data width for colId '${column.colId}'.`, e], this);
108
108
  } finally {
109
109
  this.resetClassNames();
110
110
  }
@@ -4,11 +4,12 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistModel, managed, PersistenceProvider, PlainObject, XH} from '@xh/hoist/core';
7
+ import {GridSorterLike} from '@xh/hoist/cmp/grid';
8
+ import {HoistModel, managed, PersistenceProvider, Some, XH} from '@xh/hoist/core';
8
9
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
9
10
  import {isUndefined} from 'lodash';
10
11
  import {GridModel} from '../GridModel';
11
- import {GridModelPersistOptions} from '../Types';
12
+ import {ColumnState, GridModelPersistOptions} from '../Types';
12
13
 
13
14
  /**
14
15
  * Model to manage persisting state from GridModel.
@@ -22,7 +23,13 @@ export class GridPersistenceModel extends HoistModel {
22
23
  gridModel: GridModel;
23
24
 
24
25
  @observable.ref
25
- state: PlainObject;
26
+ state: {
27
+ columns?: Partial<ColumnState>[];
28
+ sortBy?: Some<GridSorterLike>;
29
+ groupBy?: Some<string>;
30
+ version?: number;
31
+ autosize?: any;
32
+ };
26
33
 
27
34
  @managed
28
35
  provider: PersistenceProvider;
@@ -51,7 +58,7 @@ export class GridPersistenceModel extends HoistModel {
51
58
  run: state => this.provider.write(state)
52
59
  });
53
60
  } catch (e) {
54
- console.error(e);
61
+ this.logError(e);
55
62
  this.state = {version: this.VERSION};
56
63
  }
57
64
 
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import {ColumnRenderer} from '@xh/hoist/cmp/grid';
8
8
  import {div, span} from '@xh/hoist/cmp/layout';
9
- import {throwIf, warnIf} from '@xh/hoist/utils/js';
10
- import {isString, partition} from 'lodash';
9
+ import {throwIf, warnIf, intersperse} from '@xh/hoist/utils/js';
10
+ import {isNil, isString, partition, pull} from 'lodash';
11
11
  import {ReactNode} from 'react';
12
12
 
13
13
  /**
@@ -30,24 +30,24 @@ export function multiFieldRenderer(value, context): ReactNode {
30
30
  );
31
31
 
32
32
  const {mainRenderer, delimiter, subFields = []} = multiFieldConfig,
33
- [topFields, bottomFields] = partition(subFields, it => it.position === 'top'),
34
- topRowItems = [],
35
- bottomRowItems = [];
33
+ [topFields, bottomFields] = partition(subFields, it => it.position === 'top');
36
34
 
37
- // Render main field to top row
38
- topRowItems.push(renderMainField(value, mainRenderer, context));
35
+ // Render main field and subfields to top row
36
+ let topRowItems: ReactNode[] = [
37
+ renderMainField(value, mainRenderer, context),
38
+ ...topFields.map(it => renderSubField(it, context))
39
+ ];
40
+ pull(topRowItems, null);
39
41
 
40
- // Render SubFields to top row
41
- topFields.forEach(it => {
42
- if (delimiter) topRowItems.push(renderDelimiter(delimiter));
43
- topRowItems.push(renderSubField(it, context));
44
- });
42
+ // Render subfield to bottom row
43
+ let bottomRowItems: ReactNode[] = bottomFields.map(it => renderSubField(it, context));
44
+ pull(bottomRowItems, null);
45
45
 
46
- // Render SubFields to bottom row
47
- bottomFields.forEach((it, idx) => {
48
- if (delimiter && idx > 0) bottomRowItems.push(renderDelimiter(delimiter));
49
- bottomRowItems.push(renderSubField(it, context));
50
- });
46
+ // Insert delimiter if applicable
47
+ if (delimiter) {
48
+ topRowItems = intersperse(topRowItems, renderDelimiter(delimiter));
49
+ bottomRowItems = intersperse(bottomRowItems, renderDelimiter(delimiter));
50
+ }
51
51
 
52
52
  return div({
53
53
  className: 'xh-multifield-renderer',
@@ -107,14 +107,20 @@ function renderSubField({colId, label}, context) {
107
107
 
108
108
  if (label && !isString(label)) label = headerName;
109
109
 
110
- return div({
111
- className: 'xh-multifield-renderer-field',
112
- items: [label ? `${label}: ` : null, renderValue(value, renderer, column, context)]
113
- });
110
+ const renderedVal = renderValue(value, renderer, column, context),
111
+ renderedValIsEmpty = renderedVal === '' || isNil(renderedVal);
112
+
113
+ return renderedValIsEmpty
114
+ ? null
115
+ : div({
116
+ className: 'xh-multifield-renderer-field',
117
+ items: [label ? `${label}: ` : null, renderedVal]
118
+ });
114
119
  }
115
120
 
116
121
  function renderValue(value, renderer, column, context) {
117
- return renderer ? renderer(value, {...context, column}) : value;
122
+ const ret = renderer ? renderer(value, {...context, column}) : value;
123
+ return isNil(ret) ? null : ret;
118
124
  }
119
125
 
120
126
  function renderDelimiter(delimiter) {
@@ -170,7 +170,7 @@ export class GroupingChooserModel extends HoistModel {
170
170
  run: state => this.provider.write(state)
171
171
  });
172
172
  } catch (e) {
173
- console.error(e);
173
+ this.logError(e);
174
174
  XH.safeDestroy(this.provider);
175
175
  this.provider = null;
176
176
  }
@@ -190,7 +190,7 @@ export class GroupingChooserModel extends HoistModel {
190
190
  @action
191
191
  setValue(value: string[]) {
192
192
  if (!this.validateValue(value)) {
193
- console.warn('Attempted to set GroupingChooser to invalid value: ', value);
193
+ this.logWarn('Attempted to set invalid value', value);
194
194
  return;
195
195
  }
196
196
  this.value = value;
@@ -144,7 +144,7 @@ export class TabContainerModel extends HoistModel {
144
144
 
145
145
  if (route) {
146
146
  if (XH.isMobileApp) {
147
- console.warn('Tab container routing is not supported for mobile applications.');
147
+ this.logWarn('TabContainer routing is not supported for mobile applications.');
148
148
  return;
149
149
  }
150
150
 
@@ -384,7 +384,7 @@ export class TabContainerModel extends HoistModel {
384
384
  this.provider = PersistenceProvider.create({path: 'tabContainer', ...persistWith});
385
385
  state = this.provider.read() || null;
386
386
  } catch (e) {
387
- console.error(e);
387
+ this.logError(e);
388
388
  XH.safeDestroy(this.provider);
389
389
  this.provider = null;
390
390
  }
@@ -4,11 +4,12 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistModel, managed, PersistenceProvider, PlainObject} from '@xh/hoist/core';
7
+ import {GridSorterLike} from '@xh/hoist/cmp/grid';
8
+ import {HoistModel, managed, PersistenceProvider, Some} from '@xh/hoist/core';
8
9
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
9
10
  import {isUndefined} from 'lodash';
10
11
  import {ZoneGridModel} from '../ZoneGridModel';
11
- import {ZoneGridModelPersistOptions} from '../Types';
12
+ import {Zone, ZoneGridModelPersistOptions, ZoneMapping} from '../Types';
12
13
 
13
14
  /**
14
15
  * Model to manage persisting state from ZoneGridModel.
@@ -22,7 +23,12 @@ export class ZoneGridPersistenceModel extends HoistModel {
22
23
  zoneGridModel: ZoneGridModel;
23
24
 
24
25
  @observable.ref
25
- state: PlainObject;
26
+ state: {
27
+ sortBy?: GridSorterLike;
28
+ groupBy?: Some<string>;
29
+ version?: number;
30
+ mappings?: Record<Zone, Some<string | ZoneMapping>>;
31
+ };
26
32
 
27
33
  @managed
28
34
  provider: PersistenceProvider;
@@ -51,7 +57,7 @@ export class ZoneGridPersistenceModel extends HoistModel {
51
57
  run: state => this.provider.write(state)
52
58
  });
53
59
  } catch (e) {
54
- console.error(e);
60
+ this.logError(e);
55
61
  this.state = {version: this.VERSION};
56
62
  }
57
63
 
package/core/HoistBase.ts CHANGED
@@ -4,8 +4,17 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {XH, PersistenceProvider, PersistOptions, DebounceSpec} from './';
8
- import {throwIf, getOrCreate} from '@xh/hoist/utils/js';
7
+ import {XH, PersistenceProvider, PersistOptions, DebounceSpec, Some} from './';
8
+ import {
9
+ throwIf,
10
+ getOrCreate,
11
+ logInfo,
12
+ logDebug,
13
+ logError,
14
+ logWarn,
15
+ withDebug,
16
+ withInfo
17
+ } from '@xh/hoist/utils/js';
9
18
  import {
10
19
  cloneDeep,
11
20
  debounce as lodashDebounce,
@@ -26,7 +35,7 @@ import {
26
35
  when as mobxWhen
27
36
  } from '@xh/hoist/mobx';
28
37
  import {IAutorunOptions, IReactionOptions} from 'mobx/dist/api/autorun';
29
- import {IReactionDisposer} from 'mobx/dist/internal';
38
+ import {IReactionDisposer, IEqualsComparer} from 'mobx/dist/internal';
30
39
 
31
40
  export interface HoistBaseClass {
32
41
  new (...args: any[]): HoistBase;
@@ -69,6 +78,33 @@ export abstract class HoistBase {
69
78
  /** Default persistence options for this object. */
70
79
  persistWith: PersistOptions = null;
71
80
 
81
+ //--------------------------------------------------
82
+ // Logging Delegates
83
+ //--------------------------------------------------
84
+ logInfo(...messages: unknown[]) {
85
+ logInfo(messages, this);
86
+ }
87
+
88
+ logWarn(...messages: unknown[]) {
89
+ logWarn(messages, this);
90
+ }
91
+
92
+ logError(...messages: unknown[]) {
93
+ logError(messages, this);
94
+ }
95
+
96
+ logDebug(...messages: unknown[]) {
97
+ logDebug(messages, this);
98
+ }
99
+
100
+ withInfo<T>(messages: Some<unknown>, fn: () => T): T {
101
+ return withInfo<T>(messages, fn, this);
102
+ }
103
+
104
+ withDebug<T>(messages: Some<unknown>, fn: () => T): T {
105
+ return withDebug<T>(messages, fn, this);
106
+ }
107
+
72
108
  /**
73
109
  * Add and start one or more managed reactions.
74
110
  *
@@ -257,7 +293,7 @@ export abstract class HoistBase {
257
293
  /**
258
294
  * Object containing options accepted by MobX 'reaction' API as well as arguments below.
259
295
  */
260
- export interface ReactionSpec<T = any> extends IReactionOptions<T, any> {
296
+ export type ReactionSpec<T = any> = IReactionOptions<T, any> & {
261
297
  /**
262
298
  * Function returning data to observe - first arg to the underlying reaction() call.
263
299
  * Specify this or `when`.
@@ -275,7 +311,10 @@ export interface ReactionSpec<T = any> extends IReactionOptions<T, any> {
275
311
 
276
312
  /** Specify to debounce run function */
277
313
  debounce?: DebounceSpec;
278
- }
314
+
315
+ /** Specify a default from {@link comparer} or a custom comparer function. */
316
+ equals?: keyof typeof comparer | IEqualsComparer<T>;
317
+ };
279
318
 
280
319
  /**
281
320
  * Object containing options accepted by MobX 'autorun' API as well as arguments below.
package/core/elem.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  ReactElement,
15
15
  ReactNode
16
16
  } from 'react';
17
- import {PlainObject, Some, Thunkable} from './types/Types';
17
+ import {Some, Thunkable} from './types/Types';
18
18
 
19
19
  /**
20
20
  * Alternative format for specifying React Elements in render functions. This type is designed to
@@ -39,7 +39,7 @@ import {PlainObject, Some, Thunkable} from './types/Types';
39
39
  * with this API. The '$' will be stripped from the prop name before passing it along to the
40
40
  * underlying component.
41
41
  */
42
- export type ElementSpec<P extends PlainObject> = P & {
42
+ export type ElementSpec<P> = P & {
43
43
  //---------------------------------------------
44
44
  // Enhanced attributes to support element factory
45
45
  //---------------------------------------------
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {HoistService, HoistServiceClass, Some, XH} from '@xh/hoist/core';
8
8
  import {instanceManager} from '@xh/hoist/core/impl/InstanceManager';
9
- import {throwIf, withDebug} from '@xh/hoist/utils/js';
9
+ import {throwIf} from '@xh/hoist/utils/js';
10
10
  import {camelCase, castArray} from 'lodash';
11
11
 
12
12
  /**
@@ -50,13 +50,7 @@ export async function installServicesAsync(serviceClasses: Some<HoistServiceClas
50
50
 
51
51
  async function initServicesInternalAsync(svcs: HoistService[]) {
52
52
  const promises = svcs.map(it => {
53
- return withDebug(
54
- `Initializing ${it.constructor.name}`,
55
- () => {
56
- return it.initAsync();
57
- },
58
- 'XH'
59
- );
53
+ return it.withDebug(`Initializing`, () => it.initAsync());
60
54
  });
61
55
 
62
56
  const results: any[] = await Promise.allSettled(promises),
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistBase, managed, RefreshContextModel, TaskObserver} from '../';
7
+ import {HoistBase, managed, PlainObject, RefreshContextModel, TaskObserver} from '../';
8
8
  import {LoadSpec, Loadable} from './';
9
9
  import {makeObservable, observable, runInAction} from '@xh/hoist/mobx';
10
10
  import {throwIf} from '@xh/hoist/utils/js';
@@ -55,11 +55,11 @@ export class LoadSupport extends HoistBase implements Loadable {
55
55
  return this.doLoadAsync(newSpec);
56
56
  }
57
57
 
58
- async refreshAsync(meta?: object) {
58
+ async refreshAsync(meta?: PlainObject) {
59
59
  return this.loadAsync({meta, isRefresh: true});
60
60
  }
61
61
 
62
- async autoRefreshAsync(meta?: object) {
62
+ async autoRefreshAsync(meta?: PlainObject) {
63
63
  return this.loadAsync({meta, isAutoRefresh: true});
64
64
  }
65
65
 
@@ -69,7 +69,7 @@ export abstract class HoistModel extends HoistBase implements Loadable {
69
69
  @observable
70
70
  _componentProps = {};
71
71
  _modelLookup = null;
72
- _created = Date.now;
72
+ _created = Date.now();
73
73
 
74
74
  constructor() {
75
75
  super();
package/data/Store.ts CHANGED
@@ -219,7 +219,7 @@ export class Store extends HoistBase {
219
219
  _filtered: RecordSet;
220
220
 
221
221
  private _dataDefaults = null;
222
- private _created = Date.now();
222
+ _created = Date.now();
223
223
  private _fieldMap: Map<string, Field>;
224
224
  experimental: any;
225
225
 
package/data/UrlStore.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {XH, managed, LoadSupport, LoadSpec, Loadable} from '@xh/hoist/core';
8
+ import {XH, managed, LoadSupport, LoadSpec, Loadable, PlainObject} from '@xh/hoist/core';
9
9
 
10
10
  import {Store, StoreConfig} from './Store';
11
11
 
@@ -45,10 +45,10 @@ export class UrlStore extends Store implements Loadable {
45
45
  get lastLoadException() {
46
46
  return this.loadSupport.lastLoadException;
47
47
  }
48
- async refreshAsync(meta?: object) {
48
+ async refreshAsync(meta?: PlainObject) {
49
49
  return this.loadSupport.refreshAsync(meta);
50
50
  }
51
- async autoRefreshAsync(meta?: object) {
51
+ async autoRefreshAsync(meta?: PlainObject) {
52
52
  return this.loadSupport.autoRefreshAsync(meta);
53
53
  }
54
54
  async loadAsync(loadSpec?: LoadSpec | Partial<LoadSpec>) {
@@ -13,7 +13,7 @@ import {Store} from '../Store';
13
13
  import {CompoundFilterSpec, CompoundFilterOperator, FilterTestFn} from './Types';
14
14
 
15
15
  /**
16
- * Combines multiple filters (including other nested CompoundFilters) via an AND or OR operator.
16
+ * Combines multiple filters (including other nested CompoundFilters) via an `AND` or `OR` operator.
17
17
  * Immutable.
18
18
  */
19
19
  export class CompoundFilter extends Filter {
@@ -24,7 +24,8 @@ export class CompoundFilter extends Filter {
24
24
  readonly filters: Filter[];
25
25
  readonly op: CompoundFilterOperator;
26
26
 
27
- get field() {
27
+ /** @returns the singular field this filter operates on, if consistent across all clauses. */
28
+ get field(): string {
28
29
  if (isEmpty(this.filters)) return null;
29
30
  const {field} = this.filters[0] as any;
30
31
  if (field && every(this.filters, {field})) return field;
@@ -32,7 +33,8 @@ export class CompoundFilter extends Filter {
32
33
  }
33
34
 
34
35
  /**
35
- * Constructor - not typically called by apps - create from config via `parseFilter()` instead.
36
+ * Constructor - not typically called by apps - create via {@link parseFilter} instead.
37
+ * @internal
36
38
  */
37
39
  constructor({filters, op = 'AND'}: CompoundFilterSpec) {
38
40
  super();
@@ -12,6 +12,7 @@ import {
12
12
  difference,
13
13
  escapeRegExp,
14
14
  isArray,
15
+ isEqual,
15
16
  isNil,
16
17
  isString,
17
18
  isUndefined,
@@ -27,7 +28,7 @@ import {FieldFilterOperator, FieldFilterSpec, FilterTestFn} from './Types';
27
28
  * Filters by comparing the value of a given field to one or more given candidate values using one
28
29
  * of several supported operators.
29
30
  *
30
- * Note that the comparison operators `[<,<=,>,>=]` always return false for null and undefined values,
31
+ * Note that the comparison operators `[<,<=,>,>=]` always return false for null/undefined values,
31
32
  * favoring the behavior of Excel over Javascript's implicit conversion of nullish values to 0.
32
33
  *
33
34
  * Immutable.
@@ -122,13 +123,13 @@ export class FieldFilter extends Filter {
122
123
  case '=':
123
124
  opFn = v => {
124
125
  if (isNil(v) || v === '') v = null;
125
- return value.includes(v);
126
+ return value.some(it => isEqual(v, it));
126
127
  };
127
128
  break;
128
129
  case '!=':
129
130
  opFn = v => {
130
131
  if (isNil(v) || v === '') v = null;
131
- return !value.includes(v);
132
+ return !value.some(it => isEqual(v, it));
132
133
  };
133
134
  break;
134
135
  case '>':
@@ -25,13 +25,12 @@ export abstract class Filter {
25
25
  }
26
26
 
27
27
  /**
28
- * Return a function that can be used to test a record or object.
29
- *
28
+ * @returns a function that can be used to test a record or object.
30
29
  * @param store - if provided, return will be appropriate for testing records of this store.
31
30
  * Otherwise, return will be appropriate for testing anonymous objects.
32
31
  */
33
32
  abstract getTestFn(store?: Store): FilterTestFn;
34
33
 
35
- /** True if the provided other Filter is equivalent to this instance.*/
34
+ /** @returns true if the provided other Filter is equivalent to this instance.*/
36
35
  abstract equals(other: Filter): boolean;
37
36
  }
@@ -26,7 +26,8 @@ export class FunctionFilter extends Filter {
26
26
  readonly testFn: FilterTestFn;
27
27
 
28
28
  /**
29
- * Constructor - not typically called by apps - create from config via `parseFilter()` instead.
29
+ * Constructor - not typically called by apps - create via {@link parseFilter} instead.
30
+ * @internal
30
31
  */
31
32
  constructor({key, testFn}: FunctionFilterSpec) {
32
33
  super();
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import equal from 'fast-deep-equal';
9
- import {throwIf, logDebug} from '@xh/hoist/utils/js';
9
+ import {logWarn, throwIf} from '@xh/hoist/utils/js';
10
10
  import {maxBy, isNil} from 'lodash';
11
11
  import {StoreRecord, StoreRecordId} from '../StoreRecord';
12
12
  import {Store} from '../Store';
@@ -196,7 +196,7 @@ export class RecordSet {
196
196
  remove.forEach(id => {
197
197
  if (!newRecords.has(id)) {
198
198
  missingRemoves++;
199
- logDebug(`Attempted to remove non-existent record: ${id}`, this.store);
199
+ this.store.logDebug(`Attempted to remove non-existent record: ${id}`);
200
200
  return;
201
201
  }
202
202
  this.gatherDescendantIds(id, allRemoves);
@@ -211,7 +211,7 @@ export class RecordSet {
211
211
  existing = newRecords.get(id);
212
212
  if (!existing) {
213
213
  missingUpdates++;
214
- logDebug(`Attempted to update non-existent record: ${id}`, this.store);
214
+ this.store.logDebug(`Attempted to update non-existent record: ${id}`);
215
215
  return;
216
216
  }
217
217
  newRecords.set(id, rec);
@@ -230,9 +230,9 @@ export class RecordSet {
230
230
  }
231
231
 
232
232
  if (missingRemoves > 0)
233
- console.warn(`Failed to remove ${missingRemoves} records not found by id`);
233
+ logWarn(`Failed to remove ${missingRemoves} records not found by id`, this);
234
234
  if (missingUpdates > 0)
235
- console.warn(`Failed to update ${missingUpdates} records not found by id`);
235
+ logWarn(`Failed to update ${missingUpdates} records not found by id`, this);
236
236
 
237
237
  return new RecordSet(this.store, newRecords);
238
238
  }
@@ -95,7 +95,7 @@ class ToastSourceLocalModel extends HoistModel {
95
95
  */
96
96
  async getToasterAsync(position: ToasterPosition, container: HTMLElement) {
97
97
  if (container && !isElement(container)) {
98
- console.warn('container for Toast must be a DOM element. Argument will be ignored.');
98
+ this.logWarn('Ignoring invalid containerRef for Toast - must be a DOM element');
99
99
  container = null;
100
100
  }
101
101
  const className = `xh-toast-container ${container ? 'xh-toast-container--anchored' : ''}`;
@@ -6,15 +6,17 @@
6
6
  */
7
7
 
8
8
  import '@xh/hoist/desktop/register';
9
+ import {GridModel} from '@xh/hoist/cmp/grid';
9
10
  import {RecordActionSpec} from '@xh/hoist/data';
11
+ import {RestGridModel} from '@xh/hoist/desktop/cmp/rest/RestGridModel';
10
12
  import {Icon} from '@xh/hoist/icon/Icon';
11
13
 
12
14
  export const addAction: RecordActionSpec = {
13
15
  text: 'Add',
14
16
  icon: Icon.add(),
15
17
  intent: 'success',
16
- actionFn: ({gridModel}) => gridModel.appData.restGridModel.addRecord(),
17
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
18
+ actionFn: ({gridModel}) => getRGM(gridModel).addRecord(),
19
+ displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
18
20
  testId: 'add-action-button'
19
21
  };
20
22
 
@@ -23,8 +25,8 @@ export const editAction: RecordActionSpec = {
23
25
  icon: Icon.edit(),
24
26
  intent: 'primary',
25
27
  recordsRequired: 1,
26
- actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.editRecord(record),
27
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
28
+ actionFn: ({record, gridModel}) => getRGM(gridModel).editRecord(record),
29
+ displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
28
30
  testId: 'edit-action-button'
29
31
  };
30
32
 
@@ -32,7 +34,7 @@ export const viewAction: RecordActionSpec = {
32
34
  text: 'View',
33
35
  icon: Icon.search(),
34
36
  recordsRequired: 1,
35
- actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.viewRecord(record),
37
+ actionFn: ({record, gridModel}) => getRGM(gridModel).viewRecord(record),
36
38
  testId: 'view-action-button'
37
39
  };
38
40
 
@@ -40,8 +42,8 @@ export const cloneAction: RecordActionSpec = {
40
42
  text: 'Clone',
41
43
  icon: Icon.copy(),
42
44
  recordsRequired: 1,
43
- actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.cloneRecord(record),
44
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
45
+ actionFn: ({record, gridModel}) => getRGM(gridModel).cloneRecord(record),
46
+ displayFn: ({gridModel}) => ({hidden: getRGM(gridModel).readonly}),
45
47
  testId: 'clone-action-button'
46
48
  };
47
49
 
@@ -51,8 +53,12 @@ export const deleteAction: RecordActionSpec = {
51
53
  intent: 'danger',
52
54
  recordsRequired: true,
53
55
  displayFn: ({gridModel, record}) => ({
54
- hidden: (record && record.id === null) || gridModel.appData.restGridModel.readonly // Hide this action if we are acting on a "new" record
56
+ hidden: (record && record.id === null) || getRGM(gridModel).readonly // Hide this action if we are acting on a "new" record
55
57
  }),
56
- actionFn: ({gridModel}) => gridModel.appData.restGridModel.confirmDeleteRecords(),
58
+ actionFn: ({gridModel}) => getRGM(gridModel).confirmDeleteRecords(),
57
59
  testId: 'delete-action-button'
58
60
  };
61
+
62
+ function getRGM(gridModel: GridModel): RestGridModel {
63
+ return gridModel.appData.restGridModel as RestGridModel;
64
+ }