@xh/hoist 56.0.0 → 56.2.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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 57.0.0-SNAPSHOT - unreleased
4
+
5
+ ## 56.2.0 - 2023-04-28
6
+ * Expose `margin` property on DashContainerModel.
7
+
8
+ ### ⚙️ Technical
9
+ * Optimize scrolling performance for `Grid` and `DataView`
10
+
11
+ ## 56.1.0 - 2023-04-14
12
+ * Add support for new memory management diagnostics provided by hoist-core
13
+ (requires hoist-core 16.1.0 for full operation).
14
+
15
+ ### 🐞 Bug Fixes
16
+ * Fixes bug with display/reporting of exceptions during app initialization sequence.
17
+
3
18
  ## v56.0.0 - 2023-03-29
4
19
 
5
20
  ### 🎁 New Features
@@ -10,8 +25,6 @@
10
25
  * New `FetchService.abort()` API allows manually aborting a pending fetch request.
11
26
  * Hoist exceptions have been enhanced and standardized, including new TypeScript types. The
12
27
  `Error.cause` property is now populated for wrapping exceptions.
13
- * `PanelModel` now supports a `defaultSize` property specified in percentage as well as pixels
14
- (e.g. `defaultSize: '20%'` as well as `defaultSize: 200`).
15
28
  * New `GridModel.headerMenuDisplay` config for limiting column header menu visibility to on hover.
16
29
 
17
30
  ### 💥 Breaking Changes
@@ -20,10 +33,11 @@
20
33
  * Requires AG Grid v29.0.0 or higher - update your AG Grid dependency in your app's `package.json`
21
34
  file. See the [AG Grid Changelog](https://www.ag-grid.com/changelog) for details.
22
35
  * Add a dependency on `@ag-grid-community/styles` to import new dedicated styles package.
23
- * Imports of AG Grid CSS files within your app's `Bootstrap.ts` file will also need to be updated to import styles from their new location. The recommended imports are now:
36
+ * Imports of AG Grid CSS files within your app's `Bootstrap.ts` file will also need to be
37
+ updated to import styles from their new location. The recommended imports are now:
24
38
  ```typescript
25
39
  import '@ag-grid-community/styles/ag-grid.css';
26
- import '@ag-grid-community/styles/ag-theme-balham-no-font.css';
40
+ import '@ag-grid-community/styles/ag-theme-balham.css';
27
41
  ```
28
42
  * New `xhActivityTrackingConfig` soft-configuration entry places new limits on the size of
29
43
  any `data` objects passed to `XH.track()` calls.
@@ -91,13 +91,14 @@ function renderBlurb() {
91
91
  xhLogo(),
92
92
  div(
93
93
  p(
94
- 'Built with Hoist: a plugin for rich web-application development provided by ',
94
+ 'Built with Hoist, a toolkit for rapid application development from ',
95
95
  a({
96
- href: 'http://xh.io',
96
+ href: 'https://xh.io',
97
97
  target: '_blank',
98
98
  rel: 'noopener noreferrer',
99
99
  item: 'Extremely Heavy'
100
- })
100
+ }),
101
+ '.'
101
102
  ),
102
103
  p(
103
104
  'Please contact ',
@@ -9,7 +9,8 @@ import * as Col from '@xh/hoist/cmp/grid/columns';
9
9
  import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
10
10
 
11
11
  const mbCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true})},
12
- pctCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true, label: '%'})};
12
+ pctCol = {width: 150, renderer: numberRenderer({precision: 2, withCommas: true, label: '%'})},
13
+ msCol = {width: 150, renderer: numberRenderer({precision: 0, withCommas: false})};
13
14
 
14
15
  export const metricUnit: ColumnSpec = {
15
16
  field: {name: 'metricUnit', type: 'string'},
@@ -84,11 +85,38 @@ export const usedHeapMb: ColumnSpec = {
84
85
  ...mbCol
85
86
  };
86
87
 
87
- export const usedPctTotal: ColumnSpec = {
88
+ export const usedPctMax: ColumnSpec = {
88
89
  field: {
89
- name: 'usedPctTotal',
90
+ name: 'usedPctMax',
90
91
  type: 'number',
91
- displayName: 'Used (pct Total)'
92
+ displayName: 'Used (pct Max)'
93
+ },
94
+ ...pctCol
95
+ };
96
+
97
+ export const avgCollectionTime: ColumnSpec = {
98
+ field: {
99
+ name: 'avgCollectionTime',
100
+ type: 'number',
101
+ displayName: 'Avg (ms)'
102
+ },
103
+ ...msCol
104
+ };
105
+
106
+ export const collectionCount: ColumnSpec = {
107
+ field: {
108
+ name: 'collectionCount',
109
+ type: 'number',
110
+ displayName: '# GCs'
111
+ },
112
+ ...msCol
113
+ };
114
+
115
+ export const pctCollectionTime: ColumnSpec = {
116
+ field: {
117
+ name: 'pctCollectionTime',
118
+ type: 'number',
119
+ displayName: '% Time in GC'
92
120
  },
93
121
  ...pctCol
94
122
  };
@@ -7,7 +7,9 @@
7
7
  import {ChartModel} from '@xh/hoist/cmp/chart';
8
8
  import {GridModel} from '@xh/hoist/cmp/grid';
9
9
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
10
+ import {lengthIs, required} from '@xh/hoist/data';
10
11
  import {fmtTime} from '@xh/hoist/format';
12
+ import {Icon} from '@xh/hoist/icon';
11
13
  import {forOwn, sortBy} from 'lodash';
12
14
  import * as MCol from '../../monitor/MonitorColumns';
13
15
 
@@ -15,6 +17,14 @@ export class MemoryMonitorModel extends HoistModel {
15
17
  @managed gridModel: GridModel;
16
18
  @managed chartModel: ChartModel;
17
19
 
20
+ get enabled(): boolean {
21
+ return this.conf.enabled;
22
+ }
23
+
24
+ get heapDumpDir(): string {
25
+ return this.conf.heapDumpDir;
26
+ }
27
+
18
28
  constructor() {
19
29
  super();
20
30
 
@@ -24,13 +34,25 @@ export class MemoryMonitorModel extends HoistModel {
24
34
  filterModel: true,
25
35
  store: {idSpec: 'timestamp'},
26
36
  colDefaults: {filterable: true},
37
+ headerMenuDisplay: 'hover',
27
38
  columns: [
28
39
  MCol.timestamp,
29
- MCol.totalHeapMb,
30
- MCol.maxHeapMb,
31
- MCol.freeHeapMb,
32
- MCol.usedHeapMb,
33
- MCol.usedPctTotal
40
+ {
41
+ groupId: 'heap',
42
+ headerAlign: 'center',
43
+ children: [
44
+ MCol.totalHeapMb,
45
+ MCol.maxHeapMb,
46
+ MCol.freeHeapMb,
47
+ MCol.usedHeapMb,
48
+ MCol.usedPctMax
49
+ ]
50
+ },
51
+ {
52
+ groupId: 'GC',
53
+ headerAlign: 'center',
54
+ children: [MCol.collectionCount, MCol.avgCollectionTime, MCol.pctCollectionTime]
55
+ }
34
56
  ]
35
57
  });
36
58
 
@@ -58,12 +80,14 @@ export class MemoryMonitorModel extends HoistModel {
58
80
  yAxis: [
59
81
  {
60
82
  floor: 0,
61
- title: {text: 'JVM Heap (mb)'}
83
+ height: '20%',
84
+ title: {text: 'GC Avg (ms)'}
62
85
  },
63
86
  {
64
- opposite: true,
65
- linkedTo: 0,
66
- title: {text: undefined}
87
+ floor: 0,
88
+ top: '30%',
89
+ height: '70%',
90
+ title: {text: 'Heap (mb)'}
67
91
  }
68
92
  ],
69
93
  tooltip: {outside: true, shared: true}
@@ -91,34 +115,46 @@ export class MemoryMonitorModel extends HoistModel {
91
115
  // Process further for chart series.
92
116
  const maxSeries = [],
93
117
  totalSeries = [],
94
- usedSeries = [];
118
+ usedSeries = [],
119
+ avgGCSeries = [];
95
120
 
96
121
  snaps.forEach(snap => {
97
122
  maxSeries.push([snap.timestamp, snap.maxHeapMb]);
98
123
  totalSeries.push([snap.timestamp, snap.totalHeapMb]);
99
124
  usedSeries.push([snap.timestamp, snap.usedHeapMb]);
125
+
126
+ avgGCSeries.push([snap.timestamp, snap.avgCollectionTime]);
100
127
  });
101
128
 
102
129
  chartModel.setSeries([
103
130
  {
104
- name: 'Max',
131
+ name: 'GC Avg',
132
+ data: avgGCSeries,
133
+ step: true,
134
+ yAxis: 0
135
+ },
136
+ {
137
+ name: 'Heap Max',
105
138
  data: maxSeries,
106
139
  color: '#ef6c00',
107
- step: true
140
+ step: true,
141
+ yAxis: 1
108
142
  },
109
143
  {
110
- name: 'Total',
144
+ name: 'Heap Total',
111
145
  data: totalSeries,
112
146
  color: '#1976d2',
113
- step: true
147
+ step: true,
148
+ yAxis: 1
114
149
  },
115
150
  {
116
- name: 'Used',
151
+ name: 'Heap Used',
117
152
  type: 'area',
118
153
  data: usedSeries,
119
154
  color: '#bd7c7c',
120
155
  fillOpacity: 0.3,
121
- lineWidth: 1
156
+ lineWidth: 1,
157
+ yAxis: 1
122
158
  }
123
159
  ]);
124
160
  } catch (e) {
@@ -145,4 +181,32 @@ export class MemoryMonitorModel extends HoistModel {
145
181
  XH.handleException(e);
146
182
  }
147
183
  }
184
+
185
+ async dumpHeapAsync() {
186
+ try {
187
+ const appEnv = XH.getEnv('appEnvironment').toLowerCase(),
188
+ filename = await XH.prompt({
189
+ title: 'Dump Heap',
190
+ icon: Icon.fileArchive(),
191
+ message: `Specify a filename for the heap dump (to be saved in ${this.heapDumpDir})`,
192
+ input: {
193
+ rules: [required, lengthIs({min: 3, max: 250})],
194
+ initialValue: `${XH.appCode}_${appEnv}_${Date.now()}.hprof`
195
+ }
196
+ });
197
+ if (!filename) return;
198
+ await XH.fetchJson({
199
+ url: 'memoryMonitorAdmin/dumpHeap',
200
+ params: {filename}
201
+ }).linkTo(this.loadModel);
202
+ await this.loadAsync();
203
+ XH.successToast('Heap dumped successfully to ' + filename);
204
+ } catch (e) {
205
+ XH.handleException(e);
206
+ }
207
+ }
208
+
209
+ private get conf() {
210
+ return XH.getConf('xhMemoryMonitoringConfig', {heapDumpDir: null, enabled: true});
211
+ }
148
212
  }
@@ -10,15 +10,24 @@ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
10
10
  import {filler} from '@xh/hoist/cmp/layout';
11
11
  import {creates, hoistCmp} from '@xh/hoist/core';
12
12
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
13
+ import {errorMessage} from '@xh/hoist/desktop/cmp/error';
13
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
14
15
  import {Icon} from '@xh/hoist/icon';
15
16
  import {AppModel} from '@xh/hoist/admin/AppModel';
17
+ import {isNil} from 'lodash';
16
18
 
17
19
  export const memoryMonitorPanel = hoistCmp.factory({
18
20
  model: creates(MemoryMonitorModel),
19
21
 
20
22
  render({model}) {
21
- const {readonly} = AppModel;
23
+ if (!model.enabled) {
24
+ return errorMessage({
25
+ error: 'Memory Monitoring disabled via xhMemoryMonitoringConfig.'
26
+ });
27
+ }
28
+
29
+ const {readonly} = AppModel,
30
+ dumpDisabled = isNil(model.heapDumpDir);
22
31
  return panel({
23
32
  tbar: [
24
33
  button({
@@ -27,6 +36,7 @@ export const memoryMonitorPanel = hoistCmp.factory({
27
36
  omit: readonly,
28
37
  onClick: () => model.takeSnapshotAsync()
29
38
  }),
39
+ '-',
30
40
  button({
31
41
  text: 'Request GC',
32
42
  icon: Icon.trash(),
@@ -34,6 +44,17 @@ export const memoryMonitorPanel = hoistCmp.factory({
34
44
  omit: readonly,
35
45
  onClick: () => model.requestGcAsync()
36
46
  }),
47
+ button({
48
+ text: 'Dump Heap',
49
+ icon: Icon.fileArchive(),
50
+ intent: 'danger',
51
+ omit: readonly,
52
+ disabled: dumpDisabled,
53
+ tooltip: dumpDisabled
54
+ ? 'Missing required config xhMemoryMonitoringConfig.heapDumpDir'
55
+ : null,
56
+ onClick: () => model.dumpHeapAsync()
57
+ }),
37
58
  filler(),
38
59
  gridCountLabel({unit: 'snapshot'}),
39
60
  '-',
@@ -35,13 +35,18 @@ export interface AgGridProps extends HoistProps<AgGridModel>, GridOptions, Layou
35
35
  * via the `model` prop to control additional Hoist customizations.
36
36
  *
37
37
  * This component complements and contrasts with the primary Hoist `Grid` class, which provides a
38
- * significantly more managed and opinionated wrapper around ag-Grid and a number of Hoist-specific
38
+ * significantly more managed and opinionated use of ag-Grid and a number of Hoist-specific
39
39
  * extensions and customizations. That fully managed component is expected to cover the majority of
40
40
  * use cases within Hoist apps and is recommended as the primary grid class within the toolkit.
41
41
  *
42
42
  * This wrapper is provided for advanced usages of grid that wish to leverage features of the
43
43
  * underlying component not yet supported by the Hoist layer - most notably pivoting - where the
44
44
  * managed option would conflict with or complicate access to those features.
45
+ *
46
+ * Note that this component uses the ag-Grid `getRowHeight` prop to provide the grid with row
47
+ * heights. As of 4/2023, this may cause scrolling to be slow in large data sets, and
48
+ * applications may wish to set this prop to `null` and use either a fixed `rowWidth` property, or
49
+ * an explicit per-row setting instead. See GridModel for a more efficient, data aware approach.
45
50
  */
46
51
  export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
47
52
  displayName: 'AgGrid',
@@ -125,8 +130,10 @@ class AgGridLocalModel extends HoistModel {
125
130
  @lookup(AgGridModel) model: AgGridModel;
126
131
 
127
132
  get headerHeight() {
128
- const {hideHeaders, sizingMode} = this.model;
129
- return hideHeaders ? 0 : (AgGrid as any).getHeaderHeightForSizingMode(sizingMode);
133
+ const {hideHeaders, sizingMode} = this.model,
134
+ AgGridCmp = AgGrid as any;
135
+
136
+ return hideHeaders ? 0 : AgGridCmp.getHeaderHeightForSizingMode(sizingMode);
130
137
  }
131
138
 
132
139
  override onLinked() {
@@ -141,15 +148,20 @@ class AgGridLocalModel extends HoistModel {
141
148
  }
142
149
  }
143
150
 
151
+ getRowHeight = ({node}) => {
152
+ const {sizingMode} = this.model,
153
+ {groupDisplayType} = this.componentProps,
154
+ AgGridCmp = AgGrid as any;
155
+ return node.group && groupDisplayType === 'groupRows'
156
+ ? AgGridCmp.getGroupRowHeightForSizingMode(sizingMode)
157
+ : AgGridCmp.getRowHeightForSizingMode(sizingMode);
158
+ };
159
+
144
160
  noteGridReady = agParams => {
145
161
  this.model.handleGridReady(agParams);
146
162
  this.componentProps.onGridReady?.(agParams);
147
163
  };
148
164
 
149
- getRowHeight = () => {
150
- return (AgGrid as any).getRowHeightForSizingMode(this.model.sizingMode);
151
- };
152
-
153
165
  override destroy() {
154
166
  this.model?.handleGridUnmount();
155
167
  super.destroy();
@@ -60,7 +60,7 @@ export interface FilterChooserConfig {
60
60
  fieldSpecs?: Array<FilterChooserFieldSpecConfig | string>;
61
61
 
62
62
  /** Default properties to be assigned to all FilterChooserFieldSpecs created by this model. */
63
- fieldSpecDefaults?: FilterChooserFieldSpecConfig;
63
+ fieldSpecDefaults?: Partial<FilterChooserFieldSpecConfig>;
64
64
 
65
65
  /**
66
66
  * Store or cube View that should actually be filtered as this model's value changes.
@@ -460,7 +460,7 @@ export class FilterChooserModel extends HoistModel {
460
460
  //--------------------------------
461
461
  parseFieldSpecs(
462
462
  specs: Array<FilterChooserFieldSpecConfig | string>,
463
- fieldSpecDefaults: FilterChooserFieldSpecConfig
463
+ fieldSpecDefaults: Partial<FilterChooserFieldSpecConfig>
464
464
  ): Array<FilterChooserFieldSpec> {
465
465
  const {valueSource} = this;
466
466
 
package/cmp/form/Form.ts CHANGED
@@ -4,14 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
- import {
8
- BoxProps,
9
- DefaultHoistProps,
10
- elementFactory,
11
- hoistCmp,
12
- HoistProps,
13
- uses
14
- } from '@xh/hoist/core';
7
+ import {DefaultHoistProps, elementFactory, hoistCmp, HoistProps, uses} from '@xh/hoist/core';
15
8
  import equal from 'fast-deep-equal';
16
9
  import {createContext, useContext} from 'react';
17
10
  import {useCached} from '@xh/hoist/utils/react';
@@ -31,7 +24,7 @@ export interface FormContextType {
31
24
  export const FormContext = createContext<FormContextType>({});
32
25
  const formContextProvider = elementFactory(FormContext.Provider);
33
26
 
34
- export interface FormProps extends HoistProps<FormModel>, BoxProps {
27
+ export interface FormProps extends HoistProps<FormModel> {
35
28
  /**
36
29
  * Defaults for certain props on child/nested FormFields.
37
30
  * @see FormField (note there are both desktop and mobile implementations).
@@ -79,7 +79,7 @@ export class FormModel extends HoistModel {
79
79
  *
80
80
  * See {@link getData} instead if you need to get or react to the values of *any/all* fields.
81
81
  */
82
- get values(): Record<string, any> {
82
+ get values(): PlainObject {
83
83
  return this.valuesProxy;
84
84
  }
85
85
 
package/cmp/grid/Grid.ts CHANGED
@@ -8,7 +8,6 @@ import composeRefs from '@seznam/compose-react-refs';
8
8
  import {agGrid, AgGrid} from '@xh/hoist/cmp/ag-grid';
9
9
  import {getTreeStyleClasses} from '@xh/hoist/cmp/grid';
10
10
  import {getAgGridMenuItems} from '@xh/hoist/cmp/grid/impl/MenuSupport';
11
- import {Column} from './columns/Column';
12
11
  import {div, fragment, frame} from '@xh/hoist/cmp/layout';
13
12
  import {
14
13
  hoistCmp,
@@ -17,7 +16,6 @@ import {
17
16
  LayoutProps,
18
17
  lookup,
19
18
  PlainObject,
20
- SizingMode,
21
19
  useLocalModel,
22
20
  uses,
23
21
  XH
@@ -137,23 +135,9 @@ class GridLocalModel extends HoistModel {
137
135
  private model: GridModel;
138
136
  agOptions: GridOptions;
139
137
  viewRef = createRef<HTMLElement>();
140
- private fixedRowHeight: number;
141
138
  private rowKeyNavSupport: RowKeyNavSupport;
142
139
  private prevRs: RecordSet;
143
140
 
144
- getRowHeight(node) {
145
- const {model, agOptions} = this,
146
- {sizingMode, groupRowHeight} = model,
147
- {groupDisplayType} = agOptions;
148
-
149
- if (node?.group) {
150
- return groupRowHeight ?? groupDisplayType === 'groupRows'
151
- ? (AgGrid as any).getGroupRowHeightForSizingMode(sizingMode)
152
- : (AgGrid as any).getRowHeightForSizingMode(sizingMode);
153
- }
154
- return this.fixedRowHeight;
155
- }
156
-
157
141
  /** @returns true if any root-level records have children */
158
142
  @computed
159
143
  get isHierarchical(): boolean {
@@ -220,7 +204,7 @@ class GridLocalModel extends HoistModel {
220
204
  suppressRowClickSelection: !selModel.isEnabled,
221
205
  isRowSelectable: () => selModel.isEnabled,
222
206
  tooltipShowDelay: 0,
223
- getRowHeight: ({node}) => this.getRowHeight(node),
207
+ getRowHeight: this.defaultGetRowHeight,
224
208
  getRowClass: ({data}) => (model.rowClassFn ? model.rowClassFn(data) : null),
225
209
  rowClassRules: model.rowClassRules,
226
210
  noRowsOverlayComponent: observer(() => div(this.emptyText)),
@@ -367,20 +351,78 @@ class GridLocalModel extends HoistModel {
367
351
  };
368
352
  }
369
353
 
354
+ //----------------------
355
+ // Row Height Management
356
+ //----------------------
357
+ @computed
358
+ get calculatedRowHeight() {
359
+ const {model} = this,
360
+ AgGridCmp = AgGrid as any;
361
+ return max([
362
+ AgGridCmp.getRowHeightForSizingMode(model.sizingMode),
363
+ maxBy(model.getVisibleLeafColumns(), 'rowHeight')?.rowHeight
364
+ ]);
365
+ }
366
+
367
+ @computed
368
+ get calculatedGroupRowHeight() {
369
+ const {sizingMode, groupRowHeight} = this.model,
370
+ {groupDisplayType} = this.agOptions,
371
+ AgGridCmp = AgGrid as any;
372
+ return groupRowHeight ?? groupDisplayType === 'groupRows'
373
+ ? AgGridCmp.getGroupRowHeightForSizingMode(sizingMode)
374
+ : AgGridCmp.getRowHeightForSizingMode(sizingMode);
375
+ }
376
+
377
+ defaultGetRowHeight = ({node}) => {
378
+ return node.group ? this.calculatedGroupRowHeight : this.calculatedRowHeight;
379
+ };
380
+
370
381
  rowHeightReaction() {
371
- const {model} = this;
372
382
  return {
373
- track: () => [model.getVisibleLeafColumns(), model.sizingMode],
374
- run: ([visibleCols, sizingMode]: [Column[], SizingMode]) => {
375
- this.fixedRowHeight = max([
376
- (AgGrid as any).getRowHeightForSizingMode(sizingMode),
377
- maxBy(visibleCols, 'rowHeight')?.rowHeight
378
- ]);
383
+ track: () => [
384
+ this.useScrollOptimization,
385
+ this.calculatedRowHeight,
386
+ this.calculatedGroupRowHeight
387
+ ],
388
+ run: () => {
389
+ const {agApi} = this.model;
390
+ if (!agApi) return;
391
+ agApi.resetRowHeights();
392
+ this.applyScrollOptimization();
379
393
  },
380
- fireImmediately: true
394
+ debounce: 1
381
395
  };
382
396
  }
383
397
 
398
+ @computed
399
+ get useScrollOptimization() {
400
+ // When true, we preemptively evaluate and assign functional row heights after data loading.
401
+ // This improves slow scrolling but means function not guaranteed to be re-called
402
+ // when node is rendered in viewport.
403
+ const {model, agOptions} = this;
404
+ return (
405
+ agOptions.getRowHeight &&
406
+ !agOptions.rowHeight &&
407
+ !model.getVisibleLeafColumns().some(c => c.autoHeight) &&
408
+ model.experimental.useScrollOptimization !== false
409
+ );
410
+ }
411
+
412
+ applyScrollOptimization() {
413
+ if (!this.useScrollOptimization) return;
414
+ const {agApi, agColumnApi} = this.model,
415
+ {getRowHeight} = this.agOptions,
416
+ params = {api: agApi, columnApi: agColumnApi, context: null} as any;
417
+
418
+ agApi.forEachNode(node => {
419
+ params.node = node;
420
+ params.data = node.data;
421
+ node.setRowHeight(getRowHeight(params));
422
+ });
423
+ agApi.onRowHeightChanged();
424
+ }
425
+
384
426
  columnsReaction() {
385
427
  const {model} = this;
386
428
  return {
@@ -627,6 +669,7 @@ class GridLocalModel extends HoistModel {
627
669
  model.noteAgExpandStateChange();
628
670
 
629
671
  this.prevRs = newRs;
672
+ this.applyScrollOptimization();
630
673
  }
631
674
 
632
675
  syncSelection() {
@@ -69,7 +69,7 @@ async function initServicesInternalAsync(svcs: HoistService[]) {
69
69
  it.name = svcs[idx].constructor.name;
70
70
  });
71
71
 
72
- throw this.exception({
72
+ throw XH.exception({
73
73
  message: [
74
74
  'Failed to initialize services: ',
75
75
  ...errs.map(it => it.reason.message + ' (' + it.name + ')')
@@ -207,5 +207,5 @@ export class RecordAction {
207
207
 
208
208
  interface DisplayFnData extends ActionFnData {
209
209
  /** Default display config for the action */
210
- defaultConfig?: Record<string, any>;
210
+ defaultConfig?: PlainObject;
211
211
  }
package/data/Store.ts CHANGED
@@ -118,7 +118,7 @@ export interface StoreConfig {
118
118
  * Flags for experimental features. These features are designed for early client-access and
119
119
  * testing, but are not yet part of the Hoist API.
120
120
  */
121
- experimental?: Record<string, any>;
121
+ experimental?: PlainObject;
122
122
  }
123
123
 
124
124
  /**
@@ -51,7 +51,7 @@ export class StoreRecord {
51
51
  * This object has the same form as `data`. If this record has not been locally modified, this
52
52
  * property will point to the same object as `data`.
53
53
  */
54
- readonly committedData: Record<string, any>;
54
+ readonly committedData: PlainObject;
55
55
 
56
56
  /**
57
57
  * Unique ID for representing record within ag-Grid node API.
@@ -171,7 +171,7 @@ export class StoreRecord {
171
171
  * Unlike 'data', the object returned by this method contains an 'own' property for every
172
172
  * Field in the Store. Useful for cloning/iterating over all values (including defaults).
173
173
  */
174
- getValues(): Record<string, any> {
174
+ getValues(): PlainObject {
175
175
  const ret = {id: this.id};
176
176
  this.fields.forEach(({name}) => {
177
177
  ret[name] = this.data[name];
package/data/cube/Cube.ts CHANGED
@@ -205,7 +205,7 @@ export class Cube extends HoistBase {
205
205
  * @param rawData - flat array of lowest/leaf level data rows.
206
206
  * @param info - optional metadata to associate with this cube/dataset.
207
207
  */
208
- async loadDataAsync(rawData: PlainObject[], info: Record<string, any> = {}): Promise<void> {
208
+ async loadDataAsync(rawData: PlainObject[], info: PlainObject = {}): Promise<void> {
209
209
  this.store.loadData(rawData);
210
210
  this.setInfo(info);
211
211
  await forEachAsync(this._connectedViews, v => v.noteCubeLoaded());
@@ -248,7 +248,7 @@ export class Cube extends HoistBase {
248
248
  * Populate the metadata associated with this cube.
249
249
  * @param infoUpdates - new key-value pairs to be applied to existing info on this cube.
250
250
  */
251
- updateInfo(infoUpdates: Record<string, any> = {}) {
251
+ updateInfo(infoUpdates: PlainObject = {}) {
252
252
  this.setInfo({...this.info, ...infoUpdates});
253
253
  this._connectedViews.forEach(v => v.noteCubeUpdated(null));
254
254
  }
@@ -257,7 +257,7 @@ export class Cube extends HoistBase {
257
257
  // Implementation
258
258
  //---------------------
259
259
  @action
260
- private setInfo(info: Record<string, any>) {
260
+ private setInfo(info: PlainObject) {
261
261
  this.info = Object.freeze(info);
262
262
  }
263
263
 
@@ -5,6 +5,7 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
+ import {PlainObject} from '@xh/hoist/core';
8
9
  import {
9
10
  Aggregator,
10
11
  AverageAggregator,
@@ -64,11 +65,7 @@ export interface CubeFieldSpec extends FieldSpec {
64
65
  * @param value - value of record on dimension
65
66
  * @param appliedDims - *all* applied dimension values for this record
66
67
  */
67
- export type CanAggregateFn = (
68
- dimension: string,
69
- value: any,
70
- appliedDims: Record<string, any>
71
- ) => boolean;
68
+ export type CanAggregateFn = (dimension: string, value: any, appliedDims: PlainObject) => boolean;
72
69
 
73
70
  /**
74
71
  * Metadata used to define a measure or dimension in Cube. For properties present on raw data source
package/data/cube/View.ts CHANGED
@@ -78,7 +78,7 @@ export class View extends HoistBase {
78
78
 
79
79
  /** Cube info associated with this View when last updated. */
80
80
  @observable.ref
81
- info: Record<string, any> = null;
81
+ info: PlainObject = null;
82
82
 
83
83
  /** timestamp (ms) of the last time this view's data was changed. */
84
84
  @observable
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
- import {Awaitable, Some} from '../../core';
7
+ import {Awaitable, PlainObject, Some} from '../../core';
8
8
  import {castArray} from 'lodash';
9
9
  import {StoreRecord} from '../StoreRecord';
10
10
  import {BaseFieldModel} from '../../cmp/form';
@@ -32,7 +32,7 @@ export class Rule {
32
32
  */
33
33
  export type Constraint<T = any> = (
34
34
  fieldState: FieldState<T>,
35
- allValues: Record<string, any>
35
+ allValues: PlainObject
36
36
  ) => Awaitable<Some<string>>;
37
37
 
38
38
  /**
@@ -43,7 +43,7 @@ export type Constraint<T = any> = (
43
43
  * @param allValues - current values for all fields in form or record, keyed by field name.
44
44
  * @returns true if this rule is currently active.
45
45
  */
46
- export type When = (entity: any, allValues: Record<string, any>) => boolean;
46
+ export type When = (entity: any, allValues: PlainObject) => boolean;
47
47
 
48
48
  export interface FieldState<T = any> {
49
49
  /** Current value of the field */
@@ -9,6 +9,7 @@ import {
9
9
  managed,
10
10
  ManagedRefreshContextModel,
11
11
  MenuItemLike,
12
+ PlainObject,
12
13
  RefreshMode,
13
14
  RenderMode
14
15
  } from '@xh/hoist/core';
@@ -18,7 +19,7 @@ import {throwIf} from '@xh/hoist/utils/js';
18
19
  import {ReactElement} from 'react';
19
20
  import {DashViewSpec} from './DashViewSpec';
20
21
 
21
- export type DashViewState = Record<string, any>;
22
+ export type DashViewState = PlainObject;
22
23
 
23
24
  /**
24
25
  * Model for a content item within a DashContainer or DashCanvas.
@@ -7,6 +7,7 @@
7
7
  import {
8
8
  managed,
9
9
  PersistenceProvider,
10
+ PlainObject,
10
11
  RefreshMode,
11
12
  RenderMode,
12
13
  TaskObserver,
@@ -46,11 +47,14 @@ export interface DashContainerConfig extends DashConfig<DashContainerViewSpec, D
46
47
  /** True to include a button in each stack header showing the dash context menu. */
47
48
  showMenuButton?: boolean;
48
49
 
50
+ /** Between items in pixels. */
51
+ margin?: number;
52
+
49
53
  /**
50
54
  * Custom settings to be passed to the GoldenLayout instance.
51
55
  * @see http://golden-layout.com/docs/Config.html
52
56
  */
53
- goldenLayoutSettings?: Record<string, any>;
57
+ goldenLayoutSettings?: PlainObject;
54
58
  }
55
59
 
56
60
  /**
@@ -124,7 +128,8 @@ export class DashContainerModel extends DashModel<
124
128
  //-----------------------------
125
129
  renderMode: RenderMode;
126
130
  refreshMode: RefreshMode;
127
- goldenLayoutSettings: Record<string, any>;
131
+ goldenLayoutSettings: PlainObject;
132
+ margin: number;
128
133
 
129
134
  get isEmpty(): boolean {
130
135
  return this.goldenLayout && this.viewModels.length === 0;
@@ -148,6 +153,7 @@ export class DashContainerModel extends DashModel<
148
153
  contentLocked = false,
149
154
  renameLocked = false,
150
155
  showMenuButton = false,
156
+ margin = 6,
151
157
  goldenLayoutSettings,
152
158
  persistWith = null,
153
159
  emptyText = 'No views have been added to the container.',
@@ -176,6 +182,7 @@ export class DashContainerModel extends DashModel<
176
182
  this.contentLocked = contentLocked;
177
183
  this.renameLocked = renameLocked;
178
184
  this.showMenuButton = showMenuButton;
185
+ this.margin = margin;
179
186
  this.goldenLayoutSettings = goldenLayoutSettings;
180
187
  this.emptyText = emptyText;
181
188
  this.addViewButtonText = addViewButtonText;
@@ -570,7 +577,7 @@ export class DashContainerModel extends DashModel<
570
577
  ...this.goldenLayoutSettings
571
578
  },
572
579
  dimensions: {
573
- borderWidth: 6,
580
+ borderWidth: this.margin,
574
581
  headerHeight: 25
575
582
  }
576
583
  },
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, frame, p} from '@xh/hoist/cmp/layout';
8
- import {hoistCmp, HoistProps} from '@xh/hoist/core';
8
+ import {hoistCmp, HoistProps, PlainObject} from '@xh/hoist/core';
9
9
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {isEmpty, isNil, isString} from 'lodash';
@@ -28,7 +28,7 @@ export interface ErrorMessageProps extends HoistProps {
28
28
  * Error to display. If undefined, this component will look for an error property on its model.
29
29
  * If no error is found, this component will not be displayed.
30
30
  */
31
- error?: Error | string | Record<string, any>;
31
+ error?: Error | string | PlainObject;
32
32
  /**
33
33
  * Message to display for the error.
34
34
  * Defaults to the error, or any 'message' property contained within it.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
8
  import {box, div, filler, fragment, frame, hbox, label, span, vbox} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, HoistProps, LayoutProps, managed, XH} from '@xh/hoist/core';
9
+ import {hoistCmp, HoistProps, LayoutProps, managed, PlainObject, XH} from '@xh/hoist/core';
10
10
  import {button} from '@xh/hoist/desktop/cmp/button';
11
11
  import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard';
12
12
  import {textInput} from '@xh/hoist/desktop/cmp/input/TextInput';
@@ -50,7 +50,7 @@ export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps
50
50
  * Configuration object with any properties supported by the CodeMirror API.
51
51
  * @see {@link https://codemirror.net/doc/manual.html#api_configuration|CodeMirror Docs}
52
52
  */
53
- editorProps?: Record<string, any>;
53
+ editorProps?: PlainObject;
54
54
 
55
55
  /**
56
56
  * True to enable case-insensitive searching within the input. Default false, except in
@@ -257,7 +257,7 @@ export class TreeMapModel extends HoistModel {
257
257
  }
258
258
 
259
259
  @computed
260
- get expandState(): Record<string, any> {
260
+ get expandState(): PlainObject {
261
261
  const {gridModel} = this;
262
262
  return gridModel?.treeMode ? gridModel.expandState : {};
263
263
  }
@@ -38,6 +38,7 @@ export type {
38
38
  HeaderValueGetterParams,
39
39
  ICellRendererParams,
40
40
  ITooltipParams,
41
+ IRowNode,
41
42
  RowClassParams,
42
43
  ValueGetterParams,
43
44
  ValueSetterParams,
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, frame, p} from '@xh/hoist/cmp/layout';
8
- import {hoistCmp, HoistProps} from '@xh/hoist/core';
8
+ import {hoistCmp, HoistProps, PlainObject} from '@xh/hoist/core';
9
9
  import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
10
10
  import '@xh/hoist/mobile/register';
11
11
  import {isEmpty, isNil, isString} from 'lodash';
@@ -28,7 +28,7 @@ export interface ErrorMessageProps extends HoistProps {
28
28
  * Error to display. If undefined, this component will look for an error property on its model.
29
29
  * If no error is found, this component will not be displayed.
30
30
  */
31
- error?: Error | string | Record<string, any>;
31
+ error?: Error | string | PlainObject;
32
32
  /**
33
33
  * Message to display for the error.
34
34
  * Defaults to the error, or any 'message' property contained within it.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
8
  import {div} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, HoistProps, StyleProps, LayoutProps, HSide} from '@xh/hoist/core';
9
+ import {hoistCmp, HoistProps, StyleProps, LayoutProps, HSide, PlainObject} from '@xh/hoist/core';
10
10
  import {fmtDate} from '@xh/hoist/format';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {singleDatePicker} from '@xh/hoist/kit/react-dates';
@@ -65,7 +65,7 @@ export interface DateInputProps extends HoistProps, HoistInputProps, StyleProps,
65
65
  placeholder?: string;
66
66
 
67
67
  /** Props passed to SingleDatePicker component, as per SingleDatePicker docs. */
68
- singleDatePickerProps?: Record<string, any>;
68
+ singleDatePickerProps?: PlainObject;
69
69
 
70
70
  /** Alignment of entry text within control, default 'left'. */
71
71
  textAlign?: HSide;
@@ -5,7 +5,15 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, fragment} from '@xh/hoist/cmp/layout';
8
- import {Content, hoistCmp, HoistModel, HoistProps, useLocalModel, XH} from '@xh/hoist/core';
8
+ import {
9
+ Content,
10
+ hoistCmp,
11
+ HoistModel,
12
+ HoistProps,
13
+ PlainObject,
14
+ useLocalModel,
15
+ XH
16
+ } from '@xh/hoist/core';
9
17
  import '@xh/hoist/mobile/register';
10
18
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
11
19
  import {createObservableRef, elementFromContent} from '@xh/hoist/utils/react';
@@ -59,7 +67,7 @@ export interface PopoverProps extends HoistProps {
59
67
  popoverClassName?: string;
60
68
 
61
69
  /** Escape hatch to provide additional options to the PopperJS implementation */
62
- popperOptions?: Record<string, any>;
70
+ popperOptions?: PlainObject;
63
71
  }
64
72
 
65
73
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "56.0.0",
3
+ "version": "56.2.0",
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",
@@ -236,7 +236,7 @@ export function pluralize(s: string, count?: number, includeCount?: boolean): st
236
236
  }
237
237
 
238
238
  /**
239
- * Returns the number with an ordinal suffix (ie. 1 => '1st', 11 => '11th').
239
+ * Returns the number with an ordinal suffix (i.e. 1 becomes '1st', 11 becomes '11th').
240
240
  *
241
241
  * @param n - the number to ordinalize
242
242
  */