@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
@@ -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, withDebug} from '@xh/hoist/utils/js';
25
+ import {logWithDebug} from '@xh/hoist/utils/js';
26
26
  import {
27
27
  createObservableRef,
28
28
  getLayoutProps,
@@ -193,13 +193,9 @@ class TreeMapLocalModel extends HoistModel {
193
193
  if (parentDims.width === 0 || parentDims.height === 0) return;
194
194
 
195
195
  assign(config.chart, parentDims, {renderTo: chartElem});
196
- withDebug(
197
- `Creating new TreeMap | ${newData.length} records`,
198
- () => {
199
- this.chart = Highcharts.chart(config);
200
- },
201
- this
202
- );
196
+ this.withDebug(['Creating new TreeMap', `${newData.length} records`], () => {
197
+ this.chart = Highcharts.chart(config);
198
+ });
203
199
  }
204
200
 
205
201
  @logWithDebug
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "59.3.2",
3
+ "version": "59.4.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",
@@ -7,7 +7,7 @@
7
7
  import {HoistService, managed, XH} from '@xh/hoist/core';
8
8
  import {Timer} from '@xh/hoist/utils/async';
9
9
  import {olderThan, ONE_SECOND, SECONDS} from '@xh/hoist/utils/datetime';
10
- import {logDebug, withDefault} from '@xh/hoist/utils/js';
10
+ import {withDefault} from '@xh/hoist/utils/js';
11
11
 
12
12
  /**
13
13
  * Service to triggers an app-wide auto-refresh (if enabled, on a configurable interval) via the
@@ -58,7 +58,7 @@ export class AutoRefreshService extends HoistService {
58
58
  // Implementation
59
59
  //------------------------
60
60
  private async onTimerAsync() {
61
- if (!this.enabled || document.hidden) return;
61
+ if (!this.enabled || !XH.pageIsVisible) return;
62
62
 
63
63
  // Wait interval after lastCompleted -- this prevents extra refreshes if user refreshes
64
64
  // manually, or loading slow. Note auto-loads skipped if any load in progress.
@@ -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
- logDebug('Triggering application auto-refresh.', this);
72
+ this.logDebug('Triggering application auto-refresh.');
73
73
  await ctx.autoRefreshAsync();
74
74
  }
75
75
  }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistService, XH, Exception, PlainObject, Thunkable, FetchResponse} from '@xh/hoist/core';
7
+ import {HoistService, XH, Exception, PlainObject, FetchResponse, LoadSpec} from '@xh/hoist/core';
8
8
  import {isLocalDate, SECONDS, ONE_MINUTE, olderThan} from '@xh/hoist/utils/datetime';
9
9
  import {throwIf} from '@xh/hoist/utils/js';
10
10
  import {StatusCodes} from 'http-status-codes';
@@ -66,7 +66,7 @@ export class FetchService extends HoistService {
66
66
  * Set default headers to be sent with all subsequent requests.
67
67
  * @param headers - to be sent with all fetch requests, or a function to generate.
68
68
  */
69
- setDefaultHeaders(headers: Thunkable<PlainObject>) {
69
+ setDefaultHeaders(headers: PlainObject | ((arg: FetchOptions) => PlainObject)) {
70
70
  this.defaultHeaders = headers;
71
71
  }
72
72
 
@@ -328,9 +328,9 @@ export interface FetchOptions {
328
328
 
329
329
  /**
330
330
  * Data to send in the request body (for POSTs/PUTs of JSON).
331
- * When using `fetch`, provide a string. Otherwise, provide a PlainObject.
331
+ * When using `fetch`, provide a string. Otherwise, provide a JSON Serializable object
332
332
  */
333
- body?: PlainObject | string;
333
+ body?: any;
334
334
 
335
335
  /**
336
336
  * Parameters to encode and append as a query string, or send with the request body
@@ -360,7 +360,7 @@ export interface FetchOptions {
360
360
  * Optional metadata about the underlying request. Passed through for downstream processing by
361
361
  * utils such as {@link ExceptionHandler}.
362
362
  */
363
- loadSpec?: PlainObject;
363
+ loadSpec?: LoadSpec;
364
364
 
365
365
  /**
366
366
  * Options to pass to the underlying fetch request.
@@ -60,24 +60,21 @@ export class GridAutosizeService extends HoistService {
60
60
  );
61
61
 
62
62
  if (!requiredWidths) {
63
- console.debug('Autosize aborted, grid data is obsolete.');
63
+ this.logDebug('Autosize aborted, grid data is obsolete.');
64
64
  return;
65
65
  }
66
66
 
67
67
  runInAction(() => {
68
68
  // 4) Set columns to their required widths.
69
69
  gridModel.applyColumnStateChanges(requiredWidths);
70
- console.debug(
71
- `Column widths autosized via GridAutosizeService (${records.length} records)`,
72
- requiredWidths
73
- );
70
+ this.logDebug(`Auto-sized columns`, `${records.length} records`, requiredWidths);
74
71
 
75
72
  // 5) Grow columns to fill any remaining space, if enabled.
76
73
  const {fillMode} = options;
77
74
  if (fillMode && fillMode !== 'none') {
78
75
  const fillWidths = this.calcFillWidths(gridModel, colIds, fillMode);
79
76
  gridModel.applyColumnStateChanges(fillWidths);
80
- console.debug('Column widths filled via GridAutosizeService', fillWidths);
77
+ this.logDebug('Auto-sized columns using fillMode', fillWidths);
81
78
  }
82
79
  });
83
80
  }
@@ -170,7 +167,7 @@ export class GridAutosizeService extends HoistService {
170
167
  available = agApi?.gridPanel?.eBodyViewport?.clientWidth;
171
168
 
172
169
  if (!agApi || !isFinite(available)) {
173
- console.warn('Grid not rendered - unable to fill columns.');
170
+ this.logWarn('Grid not rendered - unable to fill columns.');
174
171
  return [];
175
172
  }
176
173
 
@@ -97,19 +97,19 @@ export class TrackService extends HoistService {
97
97
 
98
98
  const {maxDataLength} = this.conf;
99
99
  if (params.data?.length > maxDataLength) {
100
- console.warn(
101
- `[TrackService] | Track log includes ${params.data.length} chars of JSON data | exceeds limit of ${maxDataLength} | data will not be persisted`,
100
+ this.logWarn(
101
+ `Track log includes ${params.data.length} chars of JSON data`,
102
+ `exceeds limit of ${maxDataLength}`,
103
+ 'data will not be persisted',
102
104
  options.data
103
105
  );
104
106
  params.data = null;
105
107
  }
106
108
 
107
109
  const elapsedStr = params.elapsed != null ? `${params.elapsed}ms` : null,
108
- consoleMsg = ['[Track]', params.category, params.msg, elapsedStr]
109
- .filter(it => it != null)
110
- .join(' | ');
110
+ consoleMsgs = [params.category, params.msg, elapsedStr].filter(it => it != null);
111
111
 
112
- console.log(consoleMsg);
112
+ this.logInfo(...consoleMsgs);
113
113
 
114
114
  await XH.fetchJson({url: 'xh/track', params});
115
115
  } catch (e) {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {HoistService, XH} from '@xh/hoist/core';
8
8
  import {Icon} from '@xh/hoist/icon';
9
- import {action, observable, makeObservable} from '@xh/hoist/mobx';
9
+ import {action, makeObservable, observable} from '@xh/hoist/mobx';
10
10
  import {Timer} from '@xh/hoist/utils/async';
11
11
  import {SECONDS} from '@xh/hoist/utils/datetime';
12
12
  import {throwIf} from '@xh/hoist/utils/js';
@@ -55,11 +55,11 @@ export class WebSocketService extends HoistService {
55
55
  return !!this.channelKey;
56
56
  }
57
57
 
58
- /** set to true to log all sent/received messages - very chatty. */
58
+ /** Set to true to log all sent/received messages - very chatty. */
59
59
  logMessages: boolean = false;
60
60
 
61
- private _timer;
62
- private _socket;
61
+ private _timer: Timer;
62
+ private _socket: WebSocket;
63
63
  private _subsByTopic = {};
64
64
 
65
65
  enabled: boolean = XH.appSpec.webSocketsEnabled;
@@ -72,9 +72,8 @@ export class WebSocketService extends HoistService {
72
72
  override async initAsync() {
73
73
  if (!this.enabled) return;
74
74
  if (XH.environmentService.get('webSocketsEnabled') === false) {
75
- console.error(
76
- 'WebSockets have been enabled on this client app, but are disabled on the server. ' +
77
- 'Please adjust your server-side configuration to use WebSockets.'
75
+ this.logError(
76
+ `WebSockets enabled on this client app but disabled on server. Adjust your server-side config.`
78
77
  );
79
78
  this.enabled = false;
80
79
  return;
@@ -151,7 +150,7 @@ export class WebSocketService extends HoistService {
151
150
  };
152
151
  this._socket = s;
153
152
  } catch (e) {
154
- console.error('Failure creating WebSocket in WebSocketService', e);
153
+ this.logError('Failure creating WebSocket', e);
155
154
  }
156
155
 
157
156
  this.updateConnectedStatus();
@@ -170,7 +169,7 @@ export class WebSocketService extends HoistService {
170
169
  if (this.connected) {
171
170
  this.sendMessage({topic: this.HEARTBEAT_TOPIC, data: 'ping'});
172
171
  } else {
173
- console.warn('Heartbeat found websocket not connected - attempting to reconnect.');
172
+ this.logWarn('Heartbeat found websocket not connected - attempting to reconnect...');
174
173
  this.disconnect();
175
174
  this.connect();
176
175
  }
@@ -185,17 +184,17 @@ export class WebSocketService extends HoistService {
185
184
  // Socket events impl
186
185
  //------------------------
187
186
  onOpen(ev) {
188
- console.debug('WebSocket connection opened', ev);
187
+ this.logDebug('WebSocket connection opened', ev);
189
188
  this.updateConnectedStatus();
190
189
  }
191
190
 
192
191
  onClose(ev) {
193
- console.debug('WebSocket connection closed', ev);
192
+ this.logDebug('WebSocket connection closed', ev);
194
193
  this.updateConnectedStatus();
195
194
  }
196
195
 
197
196
  onError(ev) {
198
- console.error('WebSocket connection error', ev);
197
+ this.logError('WebSocket connection error', ev);
199
198
  this.updateConnectedStatus();
200
199
  }
201
200
 
@@ -221,7 +220,7 @@ export class WebSocketService extends HoistService {
221
220
 
222
221
  this.notifySubscribers(msg);
223
222
  } catch (e) {
224
- console.error('Error decoding websocket message', rawMsg, e);
223
+ this.logError('Error decoding websocket message', rawMsg, e);
225
224
  }
226
225
  this.updateConnectedStatus();
227
226
  }
@@ -236,7 +235,7 @@ export class WebSocketService extends HoistService {
236
235
  try {
237
236
  sub.fn(message);
238
237
  } catch (e) {
239
- console.error(`Failure in subscription handler for topic ${message.topic}`, e);
238
+ this.logError(`Handler for topic ${message.topic} threw`, e);
240
239
  }
241
240
  });
242
241
  }
@@ -287,7 +286,7 @@ export class WebSocketService extends HoistService {
287
286
  }
288
287
 
289
288
  maybeLogMessage(...args) {
290
- if (this.logMessages) console.log(...args);
289
+ if (this.logMessages) this.logDebug(args);
291
290
  }
292
291
  }
293
292
 
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {XH} from '@xh/hoist/core';
7
8
  import {wait} from '@xh/hoist/promise';
8
9
 
9
10
  /**
@@ -15,7 +16,7 @@ import {wait} from '@xh/hoist/promise';
15
16
  * allowing ongoing rendering of UI updates (e.g. load masks) and generally keeping the browser
16
17
  * event loop running.
17
18
  *
18
- * Note that if the browser tab is hidden (i.e. document.hidden is true) this loop will be executed
19
+ * Note that if the content tab is hidden (i.e. `!XH.pageIsVisible`) this loop will be executed
19
20
  * without pauses. In this case the pauses would be unduly large due to throttling of the event
20
21
  * loop by the browser, and there is no user benefit to avoiding blocking the main thread.
21
22
  *
@@ -55,7 +56,7 @@ export async function whileAsync(
55
56
  const {waitAfter = 50, waitFor = 0} = opts ?? {};
56
57
 
57
58
  // Fallback to basic loop when doc hidden: no user benefit, and throttling causes outsize waits
58
- if (document.hidden) {
59
+ if (!XH.pageIsVisible) {
59
60
  while (conditionFn()) fn();
60
61
  return;
61
62
  }
@@ -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 {throwIf} from '@xh/hoist/utils/js';
10
+ import {logWarn, throwIf} from '@xh/hoist/utils/js';
11
11
  import {isBoolean, isFinite, isFunction, isNil, isString, pull} from 'lodash';
12
12
 
13
13
  /**
@@ -152,8 +152,9 @@ export class Timer {
152
152
  if (ret > 0 && ret < min) {
153
153
  if (!warnedIntervals.has(ret)) {
154
154
  warnedIntervals.add(ret);
155
- console.warn(
156
- `Timer interval of ${ret}ms requested - forcing to min interval of ${min}ms.`
155
+ logWarn(
156
+ `Interval of ${ret}ms requested - forcing to min interval of ${min}ms.`,
157
+ this
157
158
  );
158
159
  }
159
160
  ret = min;
@@ -10,8 +10,7 @@ import {pick} from 'lodash';
10
10
  * Extract information (if available) about the client browser's window, screen, and network speed.
11
11
  */
12
12
  export function getClientDeviceInfo() {
13
- const data: any = pick(
14
- window,
13
+ const data: any = pick(window, [
15
14
  'screen',
16
15
  'devicePixelRatio',
17
16
  'screenX',
@@ -20,10 +19,10 @@ export function getClientDeviceInfo() {
20
19
  'innerHeight',
21
20
  'outerWidth',
22
21
  'outerHeight'
23
- );
22
+ ]);
23
+
24
24
  if (data.screen) {
25
- data.screen = pick(
26
- data.screen,
25
+ data.screen = pick(data.screen, [
27
26
  'availWidth',
28
27
  'availHeight',
29
28
  'width',
@@ -33,14 +32,15 @@ export function getClientDeviceInfo() {
33
32
  'availLeft',
34
33
  'availTop',
35
34
  'orientation'
36
- );
35
+ ]);
37
36
  if (data.screen.orientation) {
38
- data.screen.orientation = pick(data.screen.orientation, 'angle', 'type');
37
+ data.screen.orientation = pick(data.screen.orientation, ['angle', 'type']);
39
38
  }
40
39
  }
40
+
41
41
  const nav = window.navigator as any;
42
42
  if (nav.connection) {
43
- data.connection = pick(nav.connection, 'downlink', 'effectiveType', 'rtt');
43
+ data.connection = pick(nav.connection, ['downlink', 'effectiveType', 'rtt']);
44
44
  }
45
45
 
46
46
  return data;
@@ -12,7 +12,7 @@ import {
12
12
  isEmpty,
13
13
  isFunction,
14
14
  isObject,
15
- isObjectLike,
15
+ isPlainObject,
16
16
  isUndefined,
17
17
  mixin,
18
18
  uniq,
@@ -53,22 +53,23 @@ export function withDefault<T>(...args: T[]): T {
53
53
  }
54
54
 
55
55
  /**
56
- * Recursively freeze an object, preventing future modifications. Not all objects are supported -
57
- * FREEZABLE_TYPES limits what we will attempt to freeze to a whitelist of types known to be safely
58
- * freezable without side effects. This avoids freezing other types of objects where this routine
56
+ * Recursively freeze an object, preventing future modifications. Only the specific declared
57
+ * input types will be frozen. This avoids freezing other types of objects where this routine
59
58
  * could be problematic - e.g. application or library classes (such as `moment`!) which rely on
60
59
  * their internal state remaining mutable to function.
61
60
  */
62
- const FREEZABLE_TYPES: Set<String> = new Set(['Object', 'Array', 'Map', 'Set']);
63
- export function deepFreeze(obj: object) {
64
- if (!isObjectLike(obj) || !FREEZABLE_TYPES.has(obj.constructor.name)) return obj;
61
+ export function deepFreeze<
62
+ T extends Record<string, unknown> | Array<unknown> | Map<unknown, unknown> | Set<unknown>
63
+ >(obj: T): Readonly<T> {
64
+ if (!(isPlainObject(obj) || isArray(obj) || obj instanceof Map || obj instanceof Set)) {
65
+ return obj;
66
+ }
65
67
 
66
68
  const propNames = Object.getOwnPropertyNames(obj);
67
69
  for (const name of propNames) {
68
70
  deepFreeze(obj[name]);
69
71
  }
70
-
71
- return Object.freeze(obj);
72
+ return Object.freeze<T>(obj);
72
73
  }
73
74
 
74
75
  /**
@@ -4,10 +4,14 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {Some} from '@xh/hoist/core';
7
8
  import {castArray, isString} from 'lodash';
9
+ import {intersperse} from './LangUtils';
10
+
11
+ export type LogSource = string | {displayName: string} | {constructor: {name: string}};
8
12
 
9
13
  /**
10
- * Track a function execution with console.log.
14
+ * Time and log execution of a function to `console.info()`.
11
15
  *
12
16
  * This method will log the provided message(s) with timing information in a single message *after*
13
17
  * the tracked function returns.
@@ -20,61 +24,78 @@ import {castArray, isString} from 'lodash';
20
24
  * @param fn - function to execute
21
25
  * @param source - class, function or string to label the source of the message
22
26
  */
23
- export function withInfo<T>(msgs: string[] | string, fn: () => T, source?: any): T {
27
+ export function withInfo<T>(msgs: Some<unknown>, fn: () => T, source?: LogSource): T {
24
28
  return loggedDo(msgs, fn, source, 'info');
25
29
  }
26
30
 
27
31
  /**
28
- * Track a function execution with console.debug.
32
+ * Time and log execution of a function to `console.debug()`.
29
33
  * @see withInfo
30
34
  */
31
- export function withDebug<T>(msgs: string[] | string, fn: () => T, source?: any): T {
35
+ export function withDebug<T>(msgs: Some<unknown>, fn: () => T, source?: LogSource): T {
32
36
  return loggedDo(msgs, fn, source, 'debug');
33
37
  }
34
38
 
35
39
  /**
36
- * Log a message with console.log.
37
- *
40
+ * Write to `console.log()` with standardized formatting and source info.
38
41
  * @param msgs - message(s) to output
39
42
  * @param source - class, function or string to label the source of the message
40
43
  */
41
- export function logInfo(msgs: string[] | string, source?: any) {
44
+ export function logInfo(msgs: Some<unknown>, source?: LogSource) {
42
45
  return loggedDo(msgs, null, source, 'info');
43
46
  }
44
47
 
45
48
  /**
46
- * Log a message with console.debug.
47
- * @see logInfo
49
+ * Write to `console.debug()` with standardized formatting and source info.
50
+ * @param msgs - message(s) to output
51
+ * @param source - class, function or string to label the source of the message
48
52
  */
49
- export function logDebug(msgs: string[] | string, source?: any) {
53
+ export function logDebug(msgs: Some<unknown>, source?: LogSource) {
50
54
  return loggedDo(msgs, null, source, 'debug');
51
55
  }
52
56
 
57
+ /**
58
+ * Write to `console.error()` with standardized formatting and source info.
59
+ * @param msgs - message(s) to output
60
+ * @param source - class, function or string to label the source of the message
61
+ */
62
+ export function logError(msgs: Some<unknown>, source?: LogSource) {
63
+ return loggedDo(msgs, null, source, 'error');
64
+ }
65
+
66
+ /**
67
+ * Write to `console.warn()` with standardized formatting and source info.
68
+ * @param msgs - message(s) to output
69
+ * @param source - class, function or string to label the source of the message
70
+ */
71
+ export function logWarn(msgs: Some<unknown>, source?: LogSource) {
72
+ return loggedDo(msgs, null, source, 'warn');
73
+ }
74
+
53
75
  //----------------------------------
54
76
  // Implementation
55
77
  //----------------------------------
56
- function loggedDo(msgs, fn, source, level) {
57
- source = parseSource(source);
58
- msgs = castArray(msgs);
59
- const msg = msgs.join(' | ');
78
+ function loggedDo<T>(messages: Some<unknown>, fn: () => T, source: LogSource, level: LogLevel) {
79
+ let src = parseSource(source);
80
+ let msgs = castArray(messages);
60
81
 
61
82
  // Support simple message only.
62
83
  if (!fn) {
63
- writeLog(msg, source, level);
84
+ writeLog(msgs, src, level);
64
85
  return;
65
86
  }
66
87
 
67
88
  // Otherwise, wrap the call to the provided fn.
68
- let start, ret;
89
+ let start: number, ret: T;
69
90
  const logCompletion = () => {
70
91
  const elapsed = Date.now() - start;
71
- writeLog(`${msg} | ${elapsed}ms`, source, level);
92
+ writeLog([...msgs, `${elapsed}ms`], src, level);
72
93
  },
73
94
  logException = e => {
74
95
  const elapsed = Date.now() - start;
75
96
  writeLog(
76
- `${msg} | failed - ${e.message ?? e.name ?? 'Unknown error'} | ${elapsed}ms`,
77
- source,
97
+ [...msgs, `failed - ${e.message ?? e.name ?? 'Unknown error'}`, `${elapsed}ms`, e],
98
+ src,
78
99
  level
79
100
  );
80
101
  };
@@ -96,14 +117,33 @@ function loggedDo(msgs, fn, source, level) {
96
117
  return ret;
97
118
  }
98
119
 
99
- function parseSource(source) {
120
+ function parseSource(source: LogSource): string {
121
+ if (!source) return null;
100
122
  if (isString(source)) return source;
101
- if (source?.displayName) return source.displayName;
102
- if (source?.constructor) return source.constructor.name;
103
- return '';
123
+ if (source['displayName']) return source['displayName'];
124
+ if (source.constructor) return source.constructor.name;
125
+ return null;
104
126
  }
105
127
 
106
- function writeLog(msg, source, level) {
107
- if (source) msg = `[${source}] ${msg}`;
108
- level === 'info' ? console.log(msg) : console.debug(msg);
128
+ function writeLog(msgs: unknown[], src: string, level: LogLevel) {
129
+ if (src) msgs = [`[${src}]`, ...msgs];
130
+
131
+ msgs = intersperse(msgs, '|');
132
+
133
+ switch (level) {
134
+ case 'error':
135
+ console.error(...msgs);
136
+ break;
137
+ case 'warn':
138
+ console.warn(...msgs);
139
+ break;
140
+ case 'debug':
141
+ console.debug(...msgs);
142
+ break;
143
+ case 'info':
144
+ console.log(...msgs);
145
+ break;
146
+ }
109
147
  }
148
+
149
+ type LogLevel = 'error' | 'warn' | 'info' | 'debug';
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistProps, LayoutProps, PlainObject} from '@xh/hoist/core';
7
+ import {HoistProps, LayoutProps} from '@xh/hoist/core';
8
8
  import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash';
9
9
 
10
10
  /**
@@ -40,9 +40,9 @@ import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash';
40
40
  * that afforded by the underlying flexbox styles. In particular, it accepts flex and sizing props
41
41
  * as raw numbers rather than strings.
42
42
  */
43
- export function getLayoutProps(props: PlainObject): LayoutProps {
43
+ export function getLayoutProps(props: HoistProps): LayoutProps {
44
44
  // Harvest all keys of interest
45
- const ret: LayoutProps = pick(props, allKeys);
45
+ const ret: LayoutProps = pick(props, allKeys) as LayoutProps;
46
46
 
47
47
  // flexXXX: convert raw number to string
48
48
  const flexConfig = pick(ret, flexKeys);