@xh/hoist 59.4.0 → 59.5.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 (43) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/appcontainer/AppStateModel.ts +1 -1
  3. package/cmp/ag-grid/AgGridModel.ts +11 -5
  4. package/cmp/grid/Grid.ts +8 -6
  5. package/cmp/grid/impl/Utils.ts +1 -1
  6. package/cmp/relativetimestamp/RelativeTimestamp.ts +5 -2
  7. package/cmp/zoneGrid/ZoneGridModel.ts +1 -1
  8. package/core/HoistBase.ts +7 -6
  9. package/core/HoistBaseDecorators.ts +11 -6
  10. package/core/HoistComponent.ts +1 -3
  11. package/core/exception/ExceptionHandler.ts +4 -4
  12. package/core/load/LoadSupport.ts +7 -8
  13. package/data/cube/aggregate/UniqueAggregator.ts +3 -5
  14. package/desktop/cmp/button/ColChooserButton.ts +7 -5
  15. package/desktop/cmp/button/ZoneMapperButton.ts +7 -5
  16. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +2 -2
  17. package/desktop/cmp/dash/container/DashContainerModel.ts +2 -2
  18. package/desktop/cmp/dock/DockViewModel.ts +21 -11
  19. package/desktop/cmp/dock/impl/DockView.ts +4 -2
  20. package/desktop/cmp/form/FormField.ts +2 -2
  21. package/desktop/cmp/grid/editors/BooleanEditor.ts +5 -1
  22. package/desktop/cmp/input/DateInput.ts +1 -3
  23. package/desktop/cmp/panel/Panel.ts +4 -2
  24. package/desktop/cmp/panel/PanelModel.ts +5 -5
  25. package/desktop/cmp/treemap/TreeMap.ts +4 -3
  26. package/inspector/instances/InstancesModel.ts +6 -6
  27. package/mobile/cmp/button/ColAutosizeButton.ts +4 -3
  28. package/mobile/cmp/button/ColChooserButton.ts +4 -3
  29. package/mobile/cmp/button/ExpandCollapseButton.ts +4 -3
  30. package/mobile/cmp/button/ZoneMapperButton.ts +4 -3
  31. package/mobile/cmp/input/DateInput.ts +1 -1
  32. package/mobile/cmp/input/Select.ts +3 -3
  33. package/mobile/cmp/panel/Panel.ts +4 -2
  34. package/package.json +2 -2
  35. package/svc/AutoRefreshService.ts +1 -1
  36. package/svc/ChangelogService.ts +3 -3
  37. package/svc/EnvironmentService.ts +1 -1
  38. package/svc/GridExportService.ts +3 -8
  39. package/svc/IdentityService.ts +1 -1
  40. package/svc/TrackService.ts +3 -9
  41. package/utils/async/Timer.ts +2 -2
  42. package/utils/datetime/LocalDate.ts +13 -13
  43. package/utils/js/Decorators.ts +18 -3
package/CHANGELOG.md CHANGED
@@ -1,11 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 59.5.0 - 2023-12-11
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Added new `dialogWidth` and `dialogHeight` configs to `DockViewModel`.
8
+
9
+ ### 🐞 Bug Fixes
10
+
11
+ * Fixed serialization of expand/collapse state within `AgGridModel`, which was badly broken and
12
+ could trigger long browser hangs for grids with > 2 levels of nesting and numeric record IDs.
13
+ * Fixed `UniqueAggregator` to properly check equality for `Date` fields.
14
+ * Pinned `react-grid-layout@1.4.3` to avoid v1.4.4 bugs affecting `DashCanvas` interactions
15
+ (see https://github.com/react-grid-layout/react-grid-layout/issues/1990).
16
+
3
17
  ## 59.4.0 - 2023-11-28
4
18
 
5
19
  ### 💥 Breaking Changes
6
20
 
7
21
  * The constructors for `ColumnGroup` no long accept arbitrary rest (e.g `...rest`)
8
- arguments for applying app-specific data to the object. Instead, use the new `appData` property.
22
+ arguments for applying app-specific data to the object. Instead, use the new `appData` property.
9
23
 
10
24
  ### ⚙️ Technical
11
25
 
@@ -34,7 +34,7 @@ export class AppStateModel extends HoistModel {
34
34
  @action
35
35
  setAppState(nextState: AppState) {
36
36
  if (this.state !== nextState) {
37
- this.logDebug(`AppState change: ${this.state} → ${nextState}`);
37
+ this.logDebug(`AppState change`, `${this.state} → ${nextState}`);
38
38
  }
39
39
  this.state = nextState;
40
40
  }
@@ -19,7 +19,7 @@ import {
19
19
  isEqual,
20
20
  isNil,
21
21
  partition,
22
- set,
22
+ setWith,
23
23
  startCase
24
24
  } from 'lodash';
25
25
  import {GridSorter, GridSorterLike} from '../grid/GridSorter';
@@ -466,8 +466,12 @@ export class AgGridModel extends HoistModel {
466
466
  this._prevSortBy = sortBy;
467
467
  }
468
468
 
469
- /** @returns the current row expansion state of the grid in a serializable form. */
470
- getExpandState(): any {
469
+ /**
470
+ * @returns the current row expansion state of the grid in a serializable form.
471
+ * Returned object has keys for StoreRecordIds of top-level, expanded records and values
472
+ * of either `true` or an object with keys of StoreRecordIds of expanded child records.
473
+ */
474
+ getExpandState(): PlainObject {
471
475
  this.throwIfNotReady();
472
476
 
473
477
  const expandState = {};
@@ -486,8 +490,10 @@ export class AgGridModel extends HoistModel {
486
490
  return;
487
491
  }
488
492
 
493
+ // Note use of setWith + customizer - required to ensure that nested nodes are
494
+ // serialized as objects - see https://github.com/xh/hoist-react/issues/3550.
489
495
  const path = this.getGroupNodePath(node);
490
- set(expandState, path, true);
496
+ setWith(expandState, path, true, () => ({}));
491
497
  }
492
498
  });
493
499
 
@@ -498,7 +504,7 @@ export class AgGridModel extends HoistModel {
498
504
  * Sets the grid row expansion state
499
505
  * @param expandState - grid expand state retrieved via getExpandState()
500
506
  */
501
- setExpandState(expandState: any) {
507
+ setExpandState(expandState: PlainObject) {
502
508
  this.throwIfNotReady();
503
509
 
504
510
  const {agApi} = this;
package/cmp/grid/Grid.ts CHANGED
@@ -634,9 +634,8 @@ export class GridLocalModel extends HoistModel {
634
634
  let transaction = null;
635
635
  if (prevCount !== 0) {
636
636
  transaction = this.genTransaction(newRs, prevRs);
637
- this.logDebug(this.transactionLogStr(transaction));
638
-
639
637
  if (!this.transactionIsEmpty(transaction)) {
638
+ this.logDebug(...this.genTxnLogMsgs(transaction));
640
639
  agApi.applyTransaction(transaction);
641
640
  }
642
641
  } else {
@@ -698,10 +697,13 @@ export class GridLocalModel extends HoistModel {
698
697
  return isEmpty(t.update) && isEmpty(t.add) && isEmpty(t.remove);
699
698
  }
700
699
 
701
- transactionLogStr(t) {
702
- return `[update: ${t.update ? t.update.length : 0} | add: ${
703
- t.add ? t.add.length : 0
704
- } | remove: ${t.remove ? t.remove.length : 0}]`;
700
+ private genTxnLogMsgs(t): string[] {
701
+ const {add, update, remove} = t;
702
+ return [
703
+ `update: ${update ? update.length : 0}`,
704
+ `add: ${add ? add.length : 0}`,
705
+ `remove: ${remove ? remove.length : 0}`
706
+ ];
705
707
  }
706
708
 
707
709
  //------------------------
@@ -19,7 +19,7 @@ export function managedRenderer<T extends ColumnRenderer | GroupRowRenderer>(
19
19
  try {
20
20
  return fn.apply(null, arguments);
21
21
  } catch (e) {
22
- console.warn(`Renderer for '${identifier}' has thrown an error.`, e);
22
+ console.warn(`Renderer for '${identifier}' has thrown an error`, e);
23
23
  return '#ERROR';
24
24
  }
25
25
  } as unknown as T;
@@ -20,7 +20,7 @@ import {fmtCompactDate, fmtDateTime} from '@xh/hoist/format';
20
20
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
21
21
  import {Timer} from '@xh/hoist/utils/async';
22
22
  import {DAYS, HOURS, LocalDate, SECONDS} from '@xh/hoist/utils/datetime';
23
- import {withDefault} from '@xh/hoist/utils/js';
23
+ import {logWarn, withDefault} from '@xh/hoist/utils/js';
24
24
 
25
25
  interface RelativeTimestampProps extends HoistProps, BoxProps {
26
26
  /**
@@ -231,7 +231,10 @@ function doFormat(timestamp: Date | number, opts: RelativeTimestampOptions): str
231
231
 
232
232
  // 1) Degenerate cases
233
233
  if (isFuture && !allowFuture) {
234
- console.warn(`Unexpected future date provided for timestamp: ${elapsed}ms in the future.`);
234
+ logWarn(
235
+ `Unexpected future date provided for timestamp: ${elapsed}ms in the future.`,
236
+ RelativeTimestamp
237
+ );
235
238
  return '[????]';
236
239
  }
237
240
 
@@ -620,7 +620,7 @@ export class ZoneGridModel extends HoistModel {
620
620
  ret[zone] = this.parseZoneMapping(zone, rawMapping);
621
621
  } catch (e) {
622
622
  if (strict) throw e;
623
- console.warn(e.message);
623
+ this.logWarn(e.message);
624
624
  ret[zone] = this._defaultState.mappings[zone];
625
625
  }
626
626
  });
package/core/HoistBase.ts CHANGED
@@ -140,12 +140,12 @@ export abstract class HoistBase {
140
140
  addReaction(...specs: ReactionSpec<any>[]): IReactionDisposer | IReactionDisposer[] {
141
141
  const disposers = specs.map(s => {
142
142
  if (!s) return null;
143
- let {track, when, run, debounce, ...opts} = s;
143
+ let {track, when, run, debounce, ...rest} = s;
144
144
  throwIf(
145
145
  (track && when) || (!track && !when),
146
146
  "Must specify either 'track' or 'when' in addReaction."
147
147
  );
148
- opts = parseReactionOptions(opts);
148
+ const opts = parseReactionOptions(rest);
149
149
  run = bindAndDebounce(this, run, debounce);
150
150
 
151
151
  const disposer = track ? mobxReaction(track, run, opts) : mobxWhen(when, run, opts);
@@ -267,9 +267,10 @@ export abstract class HoistBase {
267
267
  run: data => provider.write(data)
268
268
  });
269
269
  } catch (e) {
270
- console.error(
270
+ this.logError(
271
271
  `Failed to configure Persistence for '${property}'. Be sure to fully specify ` +
272
- `'persistWith' on this object or in the method call.`
272
+ `'persistWith' on this object or in the method call`,
273
+ e
273
274
  );
274
275
  }
275
276
  }
@@ -293,7 +294,7 @@ export abstract class HoistBase {
293
294
  /**
294
295
  * Object containing options accepted by MobX 'reaction' API as well as arguments below.
295
296
  */
296
- export type ReactionSpec<T = any> = IReactionOptions<T, any> & {
297
+ export interface ReactionSpec<T = any> extends Omit<IReactionOptions<T, any>, 'equals'> {
297
298
  /**
298
299
  * Function returning data to observe - first arg to the underlying reaction() call.
299
300
  * Specify this or `when`.
@@ -314,7 +315,7 @@ export type ReactionSpec<T = any> = IReactionOptions<T, any> & {
314
315
 
315
316
  /** Specify a default from {@link comparer} or a custom comparer function. */
316
317
  equals?: keyof typeof comparer | IEqualsComparer<T>;
317
- };
318
+ }
318
319
 
319
320
  /**
320
321
  * Object containing options accepted by MobX 'autorun' API as well as arguments below.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {cloneDeep, isUndefined} from 'lodash';
8
8
  import {wait} from '../promise';
9
- import {throwIf} from '../utils/js';
9
+ import {logError, throwIf} from '../utils/js';
10
10
  import {HoistBaseClass, PersistenceProvider, PersistOptions} from './';
11
11
 
12
12
  /**
@@ -66,9 +66,10 @@ function createPersistDescriptor(
66
66
  '@persist decorator should be applied to an instance of HoistBase'
67
67
  );
68
68
  if (descriptor.get || descriptor.set) {
69
- console.error(
69
+ logError(
70
70
  `Error defining ${property} : @persist or @persistWith should be defined closest ` +
71
- `to property, and after mobx annotation e.g. '@bindable @persist ${property}'`
71
+ `to property, and after mobx annotation e.g. '@bindable @persist ${property}'`,
72
+ target
72
73
  );
73
74
  return descriptor;
74
75
  }
@@ -89,9 +90,13 @@ function createPersistDescriptor(
89
90
  });
90
91
  });
91
92
  } catch (e) {
92
- console.error(
93
- `Failed to configure Persistence for '${property}'. Be sure to fully specify ` +
94
- `'persistWith' on this object or annotation.`
93
+ logError(
94
+ [
95
+ `Failed to configure Persistence for '${property}'. Be sure to fully specify ` +
96
+ `'persistWith' on this object or annotation`,
97
+ e
98
+ ],
99
+ target
95
100
  );
96
101
  }
97
102
 
@@ -282,9 +282,7 @@ function wrapWithModel(render: RenderFn, cfg: Config): RenderFn {
282
282
  if (!model && !spec.optional && spec instanceof UsesSpec) {
283
283
  console.error(`
284
284
  Failed to find model with selector '${formatSelector(spec.selector)}' for
285
- component '${
286
- cfg.displayName
287
- }'. Ensure the proper model is available via context, or
285
+ component '${cfg.displayName}'. Ensure the proper model is available via context, or
288
286
  specify explicitly using the 'model' prop.
289
287
  `);
290
288
  return cmpErrDisplay({...getLayoutProps(props), item: 'No model found'});
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {Exception} from './Exception';
8
8
  import {fragment, span} from '@xh/hoist/cmp/layout';
9
- import {stripTags} from '@xh/hoist/utils/js';
9
+ import {logError, logWarn, stripTags} from '@xh/hoist/utils/js';
10
10
  import {Icon} from '@xh/hoist/icon';
11
11
  import {forOwn, has, isArray, isNil, isObject, omitBy, pick, set} from 'lodash';
12
12
  import {HoistException, PlainObject, XH} from '../';
@@ -189,7 +189,7 @@ export class ExceptionHandler {
189
189
  username = XH.getUsername();
190
190
 
191
191
  if (!username) {
192
- console.warn('Error report cannot be submitted to UI server - user unknown');
192
+ logWarn('Error report cannot be submitted to UI server - user unknown', this);
193
193
  return false;
194
194
  }
195
195
 
@@ -206,7 +206,7 @@ export class ExceptionHandler {
206
206
  });
207
207
  return true;
208
208
  } catch (e) {
209
- console.error('Exception while submitting error report to UI server', e);
209
+ logError(['Exception while submitting error report to UI server', e], this);
210
210
  return false;
211
211
  }
212
212
  }
@@ -261,7 +261,7 @@ export class ExceptionHandler {
261
261
  return stripTags(JSON.stringify(ret, null, 4));
262
262
  } catch (e) {
263
263
  const message = 'Failed to serialize error';
264
- console.error(message, exception, e);
264
+ logError([message, exception, e], this);
265
265
  return JSON.stringify({message}, null, 4);
266
266
  }
267
267
  }
@@ -7,8 +7,8 @@
7
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
- import {throwIf} from '@xh/hoist/utils/js';
11
- import {isPlainObject} from 'lodash';
10
+ import {logDebug, logError, throwIf} from '@xh/hoist/utils/js';
11
+ import {isPlainObject, pull} from 'lodash';
12
12
 
13
13
  /**
14
14
  * Provides support for objects that participate in Hoist's loading/refresh lifecycle.
@@ -98,18 +98,17 @@ export class LoadSupport extends HoistBase implements Loadable {
98
98
  if (target instanceof RefreshContextModel) return;
99
99
 
100
100
  const elapsed = this.lastLoadCompleted.getTime() - this.lastLoadRequested.getTime(),
101
- msg = `[${target.constructor.name}] | ${loadSpec.typeDisplay} | ${
102
- exception ? 'failed | ' : ''
103
- }${elapsed}ms`;
101
+ status = exception ? 'failed' : null,
102
+ msg = pull([loadSpec.typeDisplay, status, `${elapsed}ms`, exception], null);
104
103
 
105
104
  if (exception) {
106
105
  if (exception.isRoutine) {
107
- console.debug(msg, exception);
106
+ logDebug(msg, target);
108
107
  } else {
109
- console.error(msg, exception);
108
+ logError(msg, target);
110
109
  }
111
110
  } else {
112
- console.debug(msg);
111
+ logDebug(msg, target);
113
112
  }
114
113
  });
115
114
  }
@@ -4,20 +4,18 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
-
8
7
  import {Aggregator} from './Aggregator';
9
- import {isEmpty} from 'lodash';
8
+ import {isEmpty, isEqual} from 'lodash';
10
9
 
11
10
  export class UniqueAggregator extends Aggregator {
12
11
  override aggregate(rows, fieldName) {
13
12
  if (isEmpty(rows)) return null;
14
-
15
13
  const val = rows[0].data[fieldName];
16
- return rows.every(it => it.data[fieldName] === val) ? val : null;
14
+ return rows.every(it => isEqual(it.data[fieldName], val)) ? val : null;
17
15
  }
18
16
 
19
17
  override replace(rows, currAgg, update, context) {
20
18
  const {newValue} = update;
21
- return rows.length === 1 || newValue === currAgg ? newValue : null;
19
+ return rows.length === 1 || isEqual(newValue, currAgg) ? newValue : null;
22
20
  }
23
21
  }
@@ -12,7 +12,7 @@ import {ColChooserModel} from '@xh/hoist/desktop/cmp/grid/impl/colchooser/ColCho
12
12
  import '@xh/hoist/desktop/register';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {popover, Position} from '@xh/hoist/kit/blueprint';
15
- import {stopPropagation, withDefault} from '@xh/hoist/utils/js';
15
+ import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js';
16
16
  import {button, ButtonProps} from './Button';
17
17
 
18
18
  export interface ColChooserButtonProps extends ButtonProps {
@@ -40,15 +40,17 @@ export const [ColChooserButton, colChooserButton] = hoistCmp.withFactory<ColChoo
40
40
  const colChooserModel = gridModel?.colChooserModel as ColChooserModel;
41
41
 
42
42
  if (!gridModel) {
43
- console.error(
44
- "No GridModel available to ColChooserButton. Provide via a 'gridModel' prop, or context."
43
+ logError(
44
+ "No GridModel available. Provide via a 'gridModel' prop, or context.",
45
+ ColChooserButton
45
46
  );
46
47
  disabled = true;
47
48
  }
48
49
 
49
50
  if (!colChooserModel) {
50
- console.error(
51
- 'No ColChooserModel available on bound GridModel - enable via GridModel.colChooserModel config.'
51
+ logError(
52
+ 'No ColChooserModel available on bound GridModel - enable via GridModel.colChooserModel config.',
53
+ ColChooserButton
52
54
  );
53
55
  disabled = true;
54
56
  }
@@ -12,7 +12,7 @@ import {ZoneMapperModel} from '@xh/hoist/cmp/zoneGrid/impl/ZoneMapperModel';
12
12
  import {zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapper';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {popover, Position} from '@xh/hoist/kit/blueprint';
15
- import {stopPropagation, withDefault} from '@xh/hoist/utils/js';
15
+ import {logError, stopPropagation, withDefault} from '@xh/hoist/utils/js';
16
16
  import {button, ButtonProps} from './Button';
17
17
 
18
18
  export interface ZoneMapperButtonProps extends ButtonProps {
@@ -37,15 +37,17 @@ export const [ZoneMapperButton, zoneMapperButton] = hoistCmp.withFactory<ZoneMap
37
37
  const mapperModel = zoneGridModel?.mapperModel as ZoneMapperModel;
38
38
 
39
39
  if (!zoneGridModel) {
40
- console.error(
41
- "No ZoneGridModel available to ZoneMapperButton. Provide via a 'zoneGridModel' prop, or context."
40
+ logError(
41
+ "No ZoneGridModel available. Provide via a 'zoneGridModel' prop, or context.",
42
+ ZoneMapperButton
42
43
  );
43
44
  disabled = true;
44
45
  }
45
46
 
46
47
  if (!mapperModel) {
47
- console.error(
48
- 'No ZoneMapperModel available on bound ZoneGridModel - enable via ZoneGridModel.zoneMapperModel config.'
48
+ logError(
49
+ 'No ZoneMapperModel available on bound ZoneGridModel - enable via ZoneGridModel.zoneMapperModel config.',
50
+ ZoneMapperButton
49
51
  );
50
52
  disabled = true;
51
53
  }
@@ -189,7 +189,7 @@ export class DashCanvasModel extends DashModel<
189
189
  this.provider = PersistenceProvider.create({path: 'dashCanvas', ...persistWith});
190
190
  persistState = this.provider.read();
191
191
  } catch (e) {
192
- console.error(e);
192
+ this.logError(e);
193
193
  XH.safeDestroy(this.provider);
194
194
  this.provider = null;
195
195
  }
@@ -419,7 +419,7 @@ export class DashCanvasModel extends DashModel<
419
419
  if (this.hasSpec(viewSpecId)) {
420
420
  this.addViewInternal(viewSpecId, state);
421
421
  } else {
422
- console.warn(`Unknown viewSpecId [${viewSpecId}] found in state - skipping.`);
422
+ this.logWarn(`Unknown viewSpecId [${viewSpecId}] found in state - skipping.`);
423
423
  }
424
424
  });
425
425
  } finally {
@@ -195,7 +195,7 @@ export class DashContainerModel extends DashModel<
195
195
  this.provider = PersistenceProvider.create({path: 'dashContainer', ...persistWith});
196
196
  persistState = this.provider.read();
197
197
  } catch (e) {
198
- console.error(e);
198
+ this.logError(e);
199
199
  XH.safeDestroy(this.provider);
200
200
  this.provider = null;
201
201
  }
@@ -238,7 +238,7 @@ export class DashContainerModel extends DashModel<
238
238
  async loadStateAsync(state) {
239
239
  const containerEl = this.containerRef.current;
240
240
  if (!containerEl) {
241
- console.warn(
241
+ this.logWarn(
242
242
  'DashboardContainer not yet rendered - cannot update state - change will be discarded!'
243
243
  );
244
244
  return;
@@ -33,12 +33,16 @@ export interface DockViewConfig {
33
33
  icon?: ReactElement;
34
34
  /** Content to be rendered by this DockView. */
35
35
  content: Content;
36
- /** Width in pixels. If not set, width will be determined by the content. */
37
- width?: number;
38
- /** Height in pixels. If not set, height will be determined by the content. */
39
- height?: number;
40
- /** Width of collapsed header in pixels. If not set, width will be determined by the length of the title. */
41
- collapsedWidth?: number;
36
+ /** Width: if not set, will be determined by content. */
37
+ width?: string | number;
38
+ /** Height: if not set, will be determined by content. */
39
+ height?: string | number;
40
+ /** Width of collapsed header. If not set, width will be determined by the length of the title. */
41
+ collapsedWidth?: string | number;
42
+ /** Width when displayed in a modal dialog. If not set, will fall back on default `width`. */
43
+ dialogWidth?: string | number;
44
+ /** Height when displayed in a modal dialog. If not set, will fall back on default `height`. */
45
+ dialogHeight?: string | number;
42
46
  /** Strategy for rendering this DockView. If null, will default to its container's mode. */
43
47
  renderMode?: RenderMode;
44
48
  /** Strategy for refreshing this DockView. If null, will default to its container's mode. */
@@ -72,9 +76,11 @@ export class DockViewModel extends HoistModel {
72
76
  @observable docked: boolean;
73
77
  @observable collapsed: boolean;
74
78
  content: Content;
75
- width: number;
76
- height: number;
77
- collapsedWidth: number;
79
+ width: string | number;
80
+ height: string | number;
81
+ collapsedWidth: string | number;
82
+ dialogWidth: string | number;
83
+ dialogHeight: string | number;
78
84
  allowClose: boolean;
79
85
  allowDialog: boolean;
80
86
  onClose?: () => Awaitable<boolean | void>;
@@ -107,6 +113,8 @@ export class DockViewModel extends HoistModel {
107
113
  width,
108
114
  height,
109
115
  collapsedWidth,
116
+ dialogWidth,
117
+ dialogHeight,
110
118
  refreshMode,
111
119
  renderMode,
112
120
  docked = true,
@@ -127,6 +135,8 @@ export class DockViewModel extends HoistModel {
127
135
  this.width = width;
128
136
  this.height = height;
129
137
  this.collapsedWidth = collapsedWidth;
138
+ this.dialogWidth = dialogWidth ?? width;
139
+ this.dialogHeight = dialogHeight ?? height;
130
140
 
131
141
  this.docked = docked;
132
142
  this.collapsed = collapsed;
@@ -140,8 +150,8 @@ export class DockViewModel extends HoistModel {
140
150
  this.refreshContextModel = new ManagedRefreshContextModel(this);
141
151
 
142
152
  this.modalSupportModel = new ModalSupportModel({
143
- width: width ?? null,
144
- height: height ?? null,
153
+ width: dialogWidth ?? null,
154
+ height: dialogHeight ?? null,
145
155
  defaultModal: !docked,
146
156
  canOutsideClickClose: false
147
157
  });
@@ -36,6 +36,8 @@ export const dockView = hoistCmp.factory<DockViewProps>({
36
36
  width,
37
37
  height,
38
38
  collapsedWidth,
39
+ dialogWidth,
40
+ dialogHeight,
39
41
  collapsed,
40
42
  docked,
41
43
  isActive,
@@ -67,8 +69,8 @@ export const dockView = hoistCmp.factory<DockViewProps>({
67
69
  return modalSupport({
68
70
  model: model.modalSupportModel,
69
71
  item: vbox({
70
- width: collapsed ? collapsedWidth : width,
71
- height: !collapsed ? height : undefined,
72
+ width: collapsed ? collapsedWidth : docked ? width : dialogWidth,
73
+ height: collapsed ? undefined : docked ? height : dialogHeight,
72
74
  className: classNames(className, `xh-dock-view--${suffix}`),
73
75
  items: [header, body]
74
76
  })
@@ -15,7 +15,7 @@ import {fmtDate, fmtDateTime, fmtJson, fmtNumber} from '@xh/hoist/format';
15
15
  import {Icon} from '@xh/hoist/icon';
16
16
  import {tooltip} from '@xh/hoist/kit/blueprint';
17
17
  import {isLocalDate} from '@xh/hoist/utils/datetime';
18
- import {errorIf, getTestId, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
18
+ import {errorIf, getTestId, logWarn, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
19
19
  import {getLayoutProps, getReactElementName, useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
20
20
  import classNames from 'classnames';
21
21
  import {isBoolean, isDate, isEmpty, isFinite, isNil, isUndefined, kebabCase} from 'lodash';
@@ -107,7 +107,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
107
107
  model = model ?? (formModel && field ? formModel.fields[field] : null);
108
108
 
109
109
  if (!model) {
110
- console.warn(`Unable to bind FormField to field "${field}" on backing FormModel`);
110
+ logWarn(`Unable to bind FormField to field "${field}" on backing FormModel`, FormField);
111
111
  }
112
112
 
113
113
  // Model related props
@@ -12,6 +12,7 @@ import {useImperativeHandle} from 'react';
12
12
  import {EditorProps} from './EditorProps';
13
13
  import './Editors.scss';
14
14
  import {useInlineEditorModel} from './impl/InlineEditorModel';
15
+ import {logWarn} from '@xh/hoist/utils/js';
15
16
 
16
17
  export interface BooleanEditorProps extends EditorProps<CheckboxProps> {
17
18
  /**
@@ -35,7 +36,10 @@ export const [BooleanEditor, booleanEditor] = hoistCmp.withFactory<BooleanEditor
35
36
  quickToggle = quickToggle ?? !fullRowEditing;
36
37
 
37
38
  if (quickToggle && fullRowEditing) {
38
- console.warn("'quickToggle' prop ignored for GridModel with full row editing.");
39
+ logWarn(
40
+ "'quickToggle' prop ignored for GridModel with full row editing.",
41
+ BooleanEditor
42
+ );
39
43
  quickToggle = false;
40
44
  }
41
45
 
@@ -319,9 +319,7 @@ class DateInputModel extends HoistInputModel {
319
319
  if (date) {
320
320
  date = this.applyPrecision(date);
321
321
  } else {
322
- console.debug(
323
- 'DateInput value exceeded max/minDate bounds on change - reset to null.'
324
- );
322
+ this.logDebug('Value exceeded max/minDate bounds on change - reset to null.');
325
323
  }
326
324
  }
327
325
  this.noteValueChange(date);
@@ -23,6 +23,7 @@ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
23
23
  import {useContextMenu, useHotkeys} from '@xh/hoist/desktop/hooks';
24
24
  import '@xh/hoist/desktop/register';
25
25
  import {HotkeyConfig} from '@xh/hoist/kit/blueprint';
26
+ import {logWarn} from '@xh/hoist/utils/js';
26
27
  import {splitLayoutProps} from '@xh/hoist/utils/react';
27
28
  import {castArray, omitBy} from 'lodash';
28
29
  import {Children, isValidElement, ReactElement, ReactNode, useLayoutEffect, useRef} from 'react';
@@ -256,8 +257,9 @@ function parseLoadDecorator(prop: any, name: string, contextModel: HoistModel) {
256
257
  if (prop === 'onLoad') {
257
258
  const loadModel = contextModel?.loadModel;
258
259
  if (!loadModel) {
259
- console.warn(
260
- `Cannot use 'onLoad' for '${name}' - the linked context model must enable LoadSupport to support this feature.`
260
+ logWarn(
261
+ `Cannot use 'onLoad' for '${name}' - the linked context model must enable LoadSupport to support this feature.`,
262
+ Panel
261
263
  );
262
264
  return null;
263
265
  }
@@ -213,7 +213,7 @@ export class PanelModel extends HoistModel {
213
213
  : defaultSize;
214
214
 
215
215
  if ((collapsible || resizable) && (isNil(defaultSize) || isNil(side))) {
216
- console.error(
216
+ this.logError(
217
217
  "Must specify 'defaultSize' and 'side' for a collapsible or resizable PanelModel. Panel sizing disabled."
218
218
  );
219
219
  collapsible = false;
@@ -221,12 +221,12 @@ export class PanelModel extends HoistModel {
221
221
  }
222
222
 
223
223
  if (!isNil(maxSize) && maxSize < minSize) {
224
- console.error("'maxSize' must be greater than 'minSize'. No 'maxSize' will be set.");
224
+ this.logError("'maxSize' must be greater than 'minSize'. No 'maxSize' will be set.");
225
225
  maxSize = null;
226
226
  }
227
227
 
228
228
  if (resizable && !resizeWhileDragging && !showSplitter) {
229
- console.error(
229
+ this.logError(
230
230
  "Must not set 'showSplitter = false' for a resizable PanelModel unless 'resizeWhileDragging` is enabled. Panel sizing disabled."
231
231
  );
232
232
  resizable = false;
@@ -277,7 +277,7 @@ export class PanelModel extends HoistModel {
277
277
  this.provider = PersistenceProvider.create({path: 'panel', ...persistWith});
278
278
  state = this.provider.read() ?? this.legacyState();
279
279
  } catch (e) {
280
- console.error(e);
280
+ this.logError(e);
281
281
  XH.safeDestroy(this.provider);
282
282
  this.provider = null;
283
283
  }
@@ -408,7 +408,7 @@ export class PanelModel extends HoistModel {
408
408
  return data;
409
409
  }
410
410
  } catch (e) {
411
- console.error('Failed reading legacy state');
411
+ this.logError('Failed reading legacy state', e);
412
412
  }
413
413
  }
414
414
  return null;
@@ -22,7 +22,7 @@ import {mask} from '@xh/hoist/desktop/cmp/mask';
22
22
  import '@xh/hoist/desktop/register';
23
23
  import {Highcharts} from '@xh/hoist/kit/highcharts';
24
24
  import {wait} from '@xh/hoist/promise';
25
- import {logWithDebug} from '@xh/hoist/utils/js';
25
+ import {logError, logWithDebug} from '@xh/hoist/utils/js';
26
26
  import {
27
27
  createObservableRef,
28
28
  getLayoutProps,
@@ -53,9 +53,10 @@ export const [TreeMap, treeMap] = hoistCmp.withFactory<TreeMapProps>({
53
53
 
54
54
  render({model, className, testId, ...props}, ref) {
55
55
  if (!Highcharts) {
56
- console.error(
56
+ logError(
57
57
  'Highcharts has not been imported in to this application. Please import and ' +
58
- 'register in Bootstrap.js. See Toolbox for an example.'
58
+ 'register in Bootstrap.js. See Toolbox for an example.',
59
+ TreeMap
59
60
  );
60
61
  return 'Highcharts not available';
61
62
  }
@@ -369,8 +369,8 @@ export class InstancesModel extends HoistModel {
369
369
  const displayGroup = inst.isHoistService
370
370
  ? 'Services'
371
371
  : inst.isStore
372
- ? 'Stores'
373
- : 'Models';
372
+ ? 'Stores'
373
+ : 'Models';
374
374
  data.push({...inst, displayGroup});
375
375
  });
376
376
 
@@ -464,8 +464,8 @@ export class InstancesModel extends HoistModel {
464
464
  isGetter && !isLoadedGetter
465
465
  ? 'get(?)'
466
466
  : isProxy
467
- ? 'Proxy'
468
- : v?.constructor?.name ?? typeof v;
467
+ ? 'Proxy'
468
+ : v?.constructor?.name ?? typeof v;
469
469
 
470
470
  return {
471
471
  id: `${xhId}-${property}${fromWatchlistItem ? '-wl' : ''}`,
@@ -479,8 +479,8 @@ export class InstancesModel extends HoistModel {
479
479
  isHoistModel || isHoistService || isStore
480
480
  ? v.xhId
481
481
  : isProxy
482
- ? '[cannot render]'
483
- : v,
482
+ ? '[cannot render]'
483
+ : v,
484
484
  valueType,
485
485
  isOwnProperty,
486
486
  isObservable,
@@ -10,7 +10,7 @@ import {hoistCmp, useContextModel} from '@xh/hoist/core';
10
10
  import {Icon} from '@xh/hoist/icon';
11
11
  import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
12
12
  import '@xh/hoist/mobile/register';
13
- import {withDefault} from '@xh/hoist/utils/js';
13
+ import {logError, withDefault} from '@xh/hoist/utils/js';
14
14
 
15
15
  export interface ColAutosizeButtonProps extends ButtonProps {
16
16
  /** GridModel of the grid for which this button should autosize columns. */
@@ -31,8 +31,9 @@ export const [ColAutosizeButton, colAutosizeButton] = hoistCmp.withFactory<ColAu
31
31
  gridModel = withDefault(gridModel, useContextModel(GridModel));
32
32
 
33
33
  if (!gridModel?.autosizeEnabled) {
34
- console.error(
35
- "No GridModel available with autosize enabled. Provide via a 'gridModel' prop, or context."
34
+ logError(
35
+ "No GridModel available with autosize enabled. Provide via a 'gridModel' prop, or context.",
36
+ ColAutosizeButton
36
37
  );
37
38
  return button({icon, disabled: true, ...props});
38
39
  }
@@ -9,7 +9,7 @@ import {hoistCmp, useContextModel} from '@xh/hoist/core';
9
9
  import {Icon} from '@xh/hoist/icon';
10
10
  import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
11
11
  import '@xh/hoist/mobile/register';
12
- import {withDefault} from '@xh/hoist/utils/js';
12
+ import {logError, withDefault} from '@xh/hoist/utils/js';
13
13
 
14
14
  export interface ColChooserButtonProps extends ButtonProps {
15
15
  /** GridModel of the grid for which this button should show a chooser. */
@@ -30,8 +30,9 @@ export const [ColChooserButton, colChooserButton] = hoistCmp.withFactory<ColChoo
30
30
  gridModel = withDefault(gridModel, useContextModel(GridModel));
31
31
 
32
32
  if (!gridModel) {
33
- console.error(
34
- "No GridModel available to ColChooserButton. Provide via a 'gridModel' prop, or context."
33
+ logError(
34
+ "No GridModel available. Provide via a 'gridModel' prop, or context.",
35
+ ColChooserButton
35
36
  );
36
37
  return button({icon, disabled: true, ...props});
37
38
  }
@@ -9,7 +9,7 @@ import {hoistCmp, useContextModel} from '@xh/hoist/core';
9
9
  import {Icon} from '@xh/hoist/icon';
10
10
  import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
11
11
  import '@xh/hoist/mobile/register';
12
- import {withDefault} from '@xh/hoist/utils/js';
12
+ import {logError, withDefault} from '@xh/hoist/utils/js';
13
13
  import {isEmpty} from 'lodash';
14
14
 
15
15
  export interface ExpandCollapseButtonProps extends ButtonProps {
@@ -28,8 +28,9 @@ export const [ExpandCollapseButton, expandCollapseButton] =
28
28
  gridModel = withDefault(gridModel, useContextModel(GridModel));
29
29
 
30
30
  if (!gridModel) {
31
- console.error(
32
- "No GridModel available. Provide via a 'gridModel' prop, or context."
31
+ logError(
32
+ "No GridModel available. Provide via a 'gridModel' prop, or context.",
33
+ ExpandCollapseButton
33
34
  );
34
35
  return button({icon: Icon.expand(), disabled: true, ...props});
35
36
  }
@@ -8,7 +8,7 @@ import {hoistCmp, useContextModel} from '@xh/hoist/core';
8
8
  import {ZoneGridModel} from '../../../cmp/zoneGrid';
9
9
  import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
10
10
  import {Icon} from '@xh/hoist/icon';
11
- import {withDefault} from '@xh/hoist/utils/js';
11
+ import {logError, withDefault} from '@xh/hoist/utils/js';
12
12
  import '@xh/hoist/mobile/register';
13
13
 
14
14
  export interface ZoneMapperButtonProps extends ButtonProps {
@@ -28,8 +28,9 @@ export const [ZoneMapperButton, zoneMapperButton] = hoistCmp.withFactory<ZoneMap
28
28
  zoneGridModel = withDefault(zoneGridModel, useContextModel(ZoneGridModel));
29
29
 
30
30
  if (!zoneGridModel) {
31
- console.error(
32
- "No ZoneGridModel available to ZoneMapperButton. Provide via a 'zoneGridModel' prop, or context."
31
+ logError(
32
+ "No ZoneGridModel available. Provide via a 'zoneGridModel' prop, or context.",
33
+ ZoneMapperButton
33
34
  );
34
35
  return button({icon, disabled: true, ...props});
35
36
  }
@@ -161,7 +161,7 @@ class DateInputModel extends HoistInputModel {
161
161
  if (date && this.isOutsideRange(date)) {
162
162
  // Dates outside of min/max constraints are reset to null.
163
163
  date = null;
164
- console.debug('DateInput value exceeded max/minDate bounds on change - reset to null.');
164
+ this.logDebug('Value exceeded max/minDate bounds on change - reset to null.');
165
165
  }
166
166
  this.noteValueChange(date ? date.toDate() : null);
167
167
  };
@@ -272,8 +272,8 @@ class SelectInputModel extends HoistInputModel {
272
272
  ? reactAsyncCreatableSelect
273
273
  : reactAsyncSelect
274
274
  : creatableMode
275
- ? reactCreatableSelect
276
- : reactSelect;
275
+ ? reactCreatableSelect
276
+ : reactSelect;
277
277
  }
278
278
 
279
279
  @action
@@ -468,7 +468,7 @@ class SelectInputModel extends HoistInputModel {
468
468
  return matchOpts;
469
469
  })
470
470
  .catch(e => {
471
- console.error(e);
471
+ this.logError(e);
472
472
  throw e;
473
473
  });
474
474
  };
@@ -24,6 +24,7 @@ import {omitBy} from 'lodash';
24
24
  import {isValidElement, ReactNode, ReactElement} from 'react';
25
25
  import {panelHeader} from './impl/PanelHeader';
26
26
  import './Panel.scss';
27
+ import {logWarn} from '@xh/hoist/utils/js';
27
28
 
28
29
  export interface PanelProps extends HoistProps, Omit<BoxProps, 'title'> {
29
30
  /** A toolbar to be docked at the bottom of the panel. */
@@ -141,8 +142,9 @@ function parseLoadDecorator(prop, name, contextModel) {
141
142
  if (prop === 'onLoad') {
142
143
  const loadModel = contextModel?.loadModel;
143
144
  if (!loadModel) {
144
- console.warn(
145
- `Cannot use 'onLoad' for '${name}'. Context model does not implement loading.`
145
+ logWarn(
146
+ `Cannot use 'onLoad' for '${name}'. Context model does not implement loading.`,
147
+ Panel
146
148
  );
147
149
  return null;
148
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "59.4.0",
3
+ "version": "59.5.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",
@@ -65,7 +65,7 @@
65
65
  "react-beautiful-dnd": "~13.1.0",
66
66
  "react-dates": "~21.8.0",
67
67
  "react-dropzone": "~10.2.2",
68
- "react-grid-layout": "^1.3.4",
68
+ "react-grid-layout": "1.4.3",
69
69
  "react-markdown": "^8.0.7",
70
70
  "react-onsenui": "~1.13.2",
71
71
  "react-popper": "~2.3.0",
@@ -69,7 +69,7 @@ export class AutoRefreshService extends HoistService {
69
69
  pendingLoad = lastRequested && lastRequested > lastCompleted;
70
70
 
71
71
  if (!pendingLoad && olderThan(last, this.interval * SECONDS)) {
72
- this.logDebug('Triggering application auto-refresh.');
72
+ this.logDebug('Triggering application auto-refresh');
73
73
  await ctx.autoRefreshAsync();
74
74
  }
75
75
  }
@@ -96,12 +96,12 @@ export class ChangelogService extends HoistService {
96
96
  const {latestAvailableVersion, LAST_READ_PREF_KEY} = this;
97
97
 
98
98
  if (includes(latestAvailableVersion, 'SNAPSHOT')) {
99
- console.warn('Unable to mark changelog as read when latest version is SNAPSHOT.');
99
+ this.logWarn('Unable to mark changelog as read when latest version is SNAPSHOT.');
100
100
  return;
101
101
  }
102
102
 
103
103
  if (!latestAvailableVersion || !XH.prefService.hasKey(LAST_READ_PREF_KEY)) {
104
- console.warn(
104
+ this.logWarn(
105
105
  'Unable to mark changelog as read - latest version or required pref not found.'
106
106
  );
107
107
  return;
@@ -147,7 +147,7 @@ export class ChangelogService extends HoistService {
147
147
 
148
148
  return versions;
149
149
  } catch (e) {
150
- console.error(
150
+ this.logError(
151
151
  'Error parsing changelog JSON into versions - changelog will not be available',
152
152
  e
153
153
  );
@@ -136,7 +136,7 @@ export class EnvironmentService extends HoistService {
136
136
  // prompted to refresh.
137
137
  const clientVersion = this.get('clientVersion');
138
138
  if (appVersion !== clientVersion) {
139
- console.warn(
139
+ this.logWarn(
140
140
  `Version mismatch detected between client and server - ${clientVersion} vs ${appVersion}`
141
141
  );
142
142
  }
@@ -327,13 +327,8 @@ export class GridExportService extends HoistService {
327
327
  : it.exportName;
328
328
 
329
329
  if (!isString(ret)) {
330
- console.warn(
331
- 'Tried to export column ' +
332
- it.colId +
333
- ' with an invalid "exportName", ' +
334
- 'probably caused by setting "headerName" to a React element. Please specify an ' +
335
- 'appropriate "exportName". Defaulting to ' +
336
- it.colId
330
+ this.logWarn(
331
+ `Tried to export column '${it.colId}' with an invalid "exportName", probably caused by setting "headerName" to a React element. Please specify an appropriate "exportName". Defaulting to '${it.colId}'`
337
332
  );
338
333
  ret = it.colId;
339
334
  }
@@ -345,7 +340,7 @@ export class GridExportService extends HoistService {
345
340
  const headerCounts = countBy(headers.map(it => it.toLowerCase())),
346
341
  dupeHeaders = keys(pickBy(headerCounts, it => it > 1));
347
342
  if (type === 'excelTable' && !isEmpty(dupeHeaders)) {
348
- console.warn(
343
+ this.logWarn(
349
344
  'Excel tables require unique headers on each column. Consider using the "exportName" property to ensure unique headers. Duplicate headers: ',
350
345
  dupeHeaders
351
346
  );
@@ -62,7 +62,7 @@ export class IdentityService extends HoistService {
62
62
  try {
63
63
  await XH.appModel?.logoutAsync();
64
64
  } catch (e) {
65
- console.error('Error calling XH.appModel.logoutAsync()', e);
65
+ this.logError('Error calling XH.appModel.logoutAsync()', e);
66
66
  }
67
67
  return XH.fetchJson({url: 'xh/logout'})
68
68
  .then(() => XH.reloadApp())
@@ -45,19 +45,13 @@ export class TrackService extends HoistService {
45
45
 
46
46
  // Short-circuit if disabled...
47
47
  if (!this.enabled) {
48
- console.debug(
49
- '[TrackService] | Activity tracking disabled - activity will not be tracked.',
50
- options
51
- );
48
+ this.logDebug('Activity tracking disabled - activity will not be tracked.', options);
52
49
  return;
53
50
  }
54
51
 
55
52
  // ...or invalid request (with warning for developer)...
56
53
  if (!options.message) {
57
- console.warn(
58
- '[TrackService] | Required message not provided - activity will not be tracked.',
59
- options
60
- );
54
+ this.logWarn('Required message not provided - activity will not be tracked.', options);
61
55
  return;
62
56
  }
63
57
 
@@ -113,7 +107,7 @@ export class TrackService extends HoistService {
113
107
 
114
108
  await XH.fetchJson({url: 'xh/track', params});
115
109
  } catch (e) {
116
- console.error(`[TrackService] | Failed to persist track log`, options, e);
110
+ this.logError('Failed to persist track log', options, e);
117
111
  }
118
112
  }
119
113
  }
@@ -7,7 +7,7 @@
7
7
  import {XH} from '@xh/hoist/core';
8
8
  import {wait} from '@xh/hoist/promise';
9
9
  import {MILLISECONDS, MINUTES, olderThan} from '@xh/hoist/utils/datetime';
10
- import {logWarn, throwIf} from '@xh/hoist/utils/js';
10
+ import {logError, logWarn, throwIf} from '@xh/hoist/utils/js';
11
11
  import {isBoolean, isFinite, isFunction, isNil, isString, pull} from 'lodash';
12
12
 
13
13
  /**
@@ -122,7 +122,7 @@ export class Timer {
122
122
  try {
123
123
  await (this.internalRunFn() as any).timeout(this.timeoutMs);
124
124
  } catch (e) {
125
- console.error('Error executing timer:', e);
125
+ logError(['Error executing timer', e], this);
126
126
  }
127
127
  this.isRunning = false;
128
128
  this.lastRun = new Date();
@@ -5,8 +5,8 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {XH} from '@xh/hoist/core';
8
- import {throwIf, computeOnce} from '@xh/hoist/utils/js';
9
- import {isString, isNil} from 'lodash';
8
+ import {computeOnce, throwIf} from '@xh/hoist/utils/js';
9
+ import {isNil, isString} from 'lodash';
10
10
  import moment, {Moment, MomentInput} from 'moment';
11
11
 
12
12
  /**
@@ -25,9 +25,9 @@ export class LocalDate {
25
25
  private static _instances = new Map();
26
26
  static VALID_UNITS = ['year', 'quarter', 'month', 'week', 'day', 'date'];
27
27
 
28
- private _isoString;
29
- private _moment;
30
- private _date;
28
+ private _isoString: string;
29
+ private _moment: Moment;
30
+ private _date: Date;
31
31
 
32
32
  //------------------------
33
33
  // Factories
@@ -99,8 +99,8 @@ export class LocalDate {
99
99
  }
100
100
 
101
101
  /** Is the input value a local Date? */
102
- static isLocalDate(v: any): boolean {
103
- return v?.isLocalDate;
102
+ static isLocalDate(val: any): boolean {
103
+ return !!val?.isLocalDate;
104
104
  }
105
105
 
106
106
  //--------------------
@@ -168,15 +168,15 @@ export class LocalDate {
168
168
  //----------------
169
169
  // Core overrides.
170
170
  //----------------
171
- toString() {
171
+ toString(): string {
172
172
  return this._isoString;
173
173
  }
174
174
 
175
- toJSON() {
175
+ toJSON(): string {
176
176
  return this._isoString;
177
177
  }
178
178
 
179
- valueOf() {
179
+ valueOf(): string {
180
180
  return this._isoString;
181
181
  }
182
182
 
@@ -281,7 +281,7 @@ export class LocalDate {
281
281
  return this.isWeekday ? this : this.previousWeekday();
282
282
  }
283
283
 
284
- diff(other, unit = 'days'): number {
284
+ diff(other: LocalDate, unit: moment.unitOfTime.Diff = 'days'): number {
285
285
  this.ensureUnitValid(unit);
286
286
  return this._moment.diff(other._moment, unit);
287
287
  }
@@ -290,7 +290,7 @@ export class LocalDate {
290
290
  // Implementation
291
291
  //-------------------
292
292
  /** @internal - use one of the static factory methods instead. */
293
- private constructor(s) {
293
+ private constructor(s: string) {
294
294
  const m = moment(s, 'YYYY-MM-DD', true);
295
295
  throwIf(
296
296
  !m.isValid(),
@@ -315,6 +315,6 @@ export class LocalDate {
315
315
  * Is the input value a local Date?
316
316
  * Convenience alias for LocalDate.isLocalDate()
317
317
  */
318
- export function isLocalDate(val): val is LocalDate {
318
+ export function isLocalDate(val: any): val is LocalDate {
319
319
  return !!val?.isLocalDate;
320
320
  }
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import {XH} from '@xh/hoist/core';
8
8
  import {debounce, isFunction} from 'lodash';
9
- import {throwIf, getOrCreate, warnIf} from './LangUtils';
10
- import {withDebug} from './LogUtils';
9
+ import {getOrCreate, throwIf, warnIf} from './LangUtils';
10
+ import {withDebug, withInfo} from './LogUtils';
11
11
 
12
12
  /**
13
13
  * Decorates a class method so that it is debounced by the specified duration.
@@ -53,7 +53,22 @@ export function computeOnce(target, key, descriptor) {
53
53
  }
54
54
 
55
55
  /**
56
- * Modify a method so that its execution is tracked and timed with a debug message.
56
+ * Modify a method so that its execution is tracked and timed with a log message on the console.
57
+ * @see withInfo
58
+ */
59
+ export function logWithInfo(target, key, descriptor) {
60
+ const {value} = descriptor;
61
+ throwIf(!isFunction(value), '@logWithInfo must be applied to a class method.');
62
+ return {
63
+ ...descriptor,
64
+ value: function (...args) {
65
+ return withInfo(key, () => value.apply(this, args), this);
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Modify a method so that its execution is tracked and timed with a debug message on the console.
57
72
  * @see withDebug
58
73
  */
59
74
  export function logWithDebug(target, key, descriptor) {