@xh/hoist 76.0.0-SNAPSHOT.1756219543970 → 76.0.0-SNAPSHOT.1756431654493

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
@@ -17,12 +17,15 @@
17
17
  * Handled an edge-case `ViewManager` bug where `enableDefault` changed to `false` after some user
18
18
  state had already been persisted w/users pointed at in-code default view. The manager now calls
19
19
  its configured `initialViewSpec` function as expected in this case.
20
-
21
- * `XH.restoreDefaultsAsync` will now clear basic view state. Views themselves will be preserved.
22
- Requires hoist-core v31.2
20
+ * `XH.restoreDefaultsAsync` will now clear basic ViewManager state, notably last active view. User
21
+ views themselves will be preserved. Requires `hoist-core >= 31.2`.
23
22
 
24
23
  ### ⚙️ Technical
25
24
 
25
+ * Hoist's client-side logging utilities are now governed by the new `XH.logLevel` property, defining
26
+ a logging severity threshold for the app. Default level is 'info', preventing memory usage and
27
+ performance impacts from verbose logging on level 'debug'. This level can be changed at runtime
28
+ for troubleshooting. See documentation within `LogUtils.ts` for more info.
26
29
  * Added control to trigger browser GC from app footer. Useful for troubleshooting memory issues.
27
30
  Requires running chromium-based browser via e.g. `start chrome --js-flags="--expose-gc`.
28
31
 
@@ -2,6 +2,7 @@ import { RouterModel } from '@xh/hoist/appcontainer/RouterModel';
2
2
  import { HoistAuthModel } from '@xh/hoist/core/HoistAuthModel';
3
3
  import { Store } from '@xh/hoist/data';
4
4
  import { AlertBannerService, AutoRefreshService, ChangelogService, ConfigService, EnvironmentService, FetchOptions, FetchService, GridAutosizeService, GridExportService, IdentityService, IdleService, InspectorService, JsonBlobService, LocalStorageService, PrefService, SessionStorageService, TrackService, WebSocketService, ClientHealthService } from '@xh/hoist/svc';
5
+ import { LogLevel } from '@xh/hoist/utils/js';
5
6
  import { Router, State } from 'router5';
6
7
  import { CancelFn } from 'router5/types/types/base';
7
8
  import { SetOptional } from 'type-fest';
@@ -177,6 +178,18 @@ export declare class XHApi {
177
178
  * @see HoistAuthModel.logoutAsync
178
179
  */
179
180
  logoutAsync(): Promise<void>;
181
+ /**
182
+ * Current minimum severity for Hoist log utils (default 'info').
183
+ * Messages logged via managed Hoist log utils with lower severity will be ignored.
184
+ */
185
+ get logLevel(): LogLevel;
186
+ /**
187
+ * Set the minimum severity for Hoist log utils until the page is refreshed. Optionally persist
188
+ * this adjustment to sessionStorage to maintain for the lifetime of the browser tab.
189
+ *
190
+ * Hint: call this method from the console to adjust your app's log level while troubleshooting.
191
+ */
192
+ setLogLevel(level: LogLevel, persistInSessionStorage?: boolean): void;
180
193
  /**
181
194
  * Main entry point to start the client app - initializes and renders application code.
182
195
  * Call from the app's entry-point file within your project's `/client-app/src/apps/` folder.
@@ -6,19 +6,28 @@ import { RestGridModel } from './RestGridModel';
6
6
  export interface RestGridProps extends HoistProps<RestGridModel>, Omit<PanelProps, 'model' | 'modelConfig' | 'modelRef'> {
7
7
  /**
8
8
  * This constitutes an 'escape hatch' for applications that need to get to the underlying
9
- * ag-Grid API. It should be used with care. Settings made here might be overwritten and/or
10
- * interfere with the implementation of this component and its use of the ag-Grid API.
9
+ * AG Grid API. Use with care - settings made here might be overwritten and/or interfere with
10
+ * the implementation of this component and its use of AG Grid.
11
11
  */
12
12
  agOptions?: PlainObject;
13
- /** Optional components rendered adjacent to the top toolbar's action buttons */
14
- extraToolbarItems?: Some<ReactNode> | (() => Some<ReactNode>);
15
13
  /**
16
- * Mask to render on this Component. Defaults to true, which renders a standard
17
- * Hoist mask. Also can be set to false for no mask, or passed an element
18
- * specifying a Mask instance.
14
+ * Optional components rendered adjacent to the top toolbar's action buttons.
15
+ * See also {@link tbar} to take full control of the toolbar.
19
16
  */
20
- mask?: ReactElement | boolean;
17
+ extraToolbarItems?: Some<ReactNode> | (() => Some<ReactNode>);
21
18
  /** Classname to be passed to RestForm. */
22
19
  formClassName?: string;
20
+ /**
21
+ * Mask to render on this Component. Defaults to true, which renders a standard Hoist mask.
22
+ * Set to null/false for no mask, or pass a fully customized mask element.
23
+ */
24
+ mask?: ReactElement | boolean;
25
+ /**
26
+ * A custom toolbar to be docked above the grid. Note that this supersedes the default
27
+ * toolbar, meaning the `extraToolbarItems` prop will be ignored, as will the `RestGridModel`
28
+ * configs `toolbarActions`, `filterFields`, and `showRefreshButton`. If specified as an array,
29
+ * will be passed as children to a Toolbar component.
30
+ */
31
+ tbar?: Some<ReactNode>;
23
32
  }
24
33
  export declare const RestGrid: import("react").FC<RestGridProps>, restGrid: import("@xh/hoist/core").ElementFactory<RestGridProps>;
@@ -1,4 +1,20 @@
1
1
  import { Some } from '@xh/hoist/core';
2
+ /**
3
+ * Utility functions providing managed, structured logging to Hoist apps.
4
+ *
5
+ * Essentially a wrapper around the browser console supporting logging levels, timing, and
6
+ * miscellaneous Hoist display conventions.
7
+ *
8
+ * Objects extending `HoistBase` need not import these functions directly, as they are available
9
+ * via delegates on `HoistBase`.
10
+ *
11
+ * Hoist sets its minimum severity level to 'info' by default. This prevents performance or
12
+ * memory impacts that might result from verbose debug logging. This can be adjusted by calling
13
+ * XH.logLevel from the console.
14
+ */
15
+ /** Severity Level for log statement */
16
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
17
+ /** Object identifying the source of log statement. Typically, a javascript class */
2
18
  export type LogSource = string | {
3
19
  displayName: string;
4
20
  } | {
@@ -6,6 +22,20 @@ export type LogSource = string | {
6
22
  name: string;
7
23
  };
8
24
  };
25
+ /**
26
+ * Current minimum severity for Hoist log utils (default 'info').
27
+ * Messages logged via managed Hoist log utils with lower severity will be ignored.
28
+ *
29
+ * @internal - use public `XH.logLevel`.
30
+ */
31
+ export declare function getLogLevel(): LogLevel;
32
+ /**
33
+ * Set the minimum severity for Hoist log utils until the page is refreshed. Optionally persist
34
+ * this adjustment to sessionStorage to maintain for the lifetime of the browser tab.
35
+ *
36
+ * @internal - use public `XH.setLogLevel()`.
37
+ */
38
+ export declare function setLogLevel(level: LogLevel, persistInSessionStorage?: boolean): void;
9
39
  /**
10
40
  * Time and log execution of a function to `console.info()`.
11
41
  *
@@ -50,5 +80,3 @@ export declare function logError(msgs: Some<unknown>, source?: LogSource): unkno
50
80
  * @param source - class, function or string to label the source of the message
51
81
  */
52
82
  export declare function logWarn(msgs: Some<unknown>, source?: LogSource): unknown;
53
- /** Parse a LogSource in to a canonical string label. */
54
- export declare function parseSource(source: LogSource): string;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import {Column, ColumnGroup, ColumnRenderer, GroupRowRenderer} from '@xh/hoist/cmp/grid';
8
8
  import {HeaderClassParams} from '@xh/hoist/kit/ag-grid';
9
+ import {logWarn} from '@xh/hoist/utils/js';
9
10
  import {castArray, isFunction} from 'lodash';
10
11
 
11
12
  /** @internal */
@@ -18,7 +19,7 @@ export function managedRenderer<T extends ColumnRenderer | GroupRowRenderer>(
18
19
  try {
19
20
  return fn.apply(null, arguments);
20
21
  } catch (e) {
21
- console.warn(`Renderer for '${identifier}' has thrown an error`, e);
22
+ logWarn([`Renderer for '${identifier}' has thrown an error`, e]);
22
23
  return '#ERROR';
23
24
  }
24
25
  } as unknown as T;
@@ -25,7 +25,7 @@ import {
25
25
  formatSelector,
26
26
  HoistModel
27
27
  } from './model';
28
- import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js';
28
+ import {logError, throwIf, warnIf, withDefault} from '@xh/hoist/utils/js';
29
29
  import {getLayoutProps, useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
30
30
  import classNames from 'classnames';
31
31
  import {isFunction, isPlainObject, isObject} from 'lodash';
@@ -285,11 +285,11 @@ function wrapWithModel(render: RenderFn, cfg: Config): RenderFn {
285
285
 
286
286
  // 2) Validate
287
287
  if (!model && !spec.optional && spec instanceof UsesSpec) {
288
- console.error(`
289
- Failed to find model with selector '${formatSelector(spec.selector)}' for
290
- component '${cfg.displayName}'. Ensure the proper model is available via context, or
291
- specify explicitly using the 'model' prop.
292
- `);
288
+ logError(
289
+ `Failed to find model with selector '${formatSelector(spec.selector)}'. Ensure the
290
+ proper model is available via context, or specify using the 'model' prop.`,
291
+ cfg.displayName
292
+ );
293
293
  return cmpErrDisplay({...getLayoutProps(props), item: 'No model found'});
294
294
  }
295
295
 
@@ -408,10 +408,11 @@ function lookupModel(props: HoistProps, modelLookup: ModelLookup, cfg: Config):
408
408
  // 2) props - instance
409
409
  if (model) {
410
410
  if (!model.isHoistModel || !model.matchesSelector(selector, true)) {
411
- console.error(
412
- `Incorrect model passed to '${cfg.displayName}'.
411
+ logError(
412
+ `Incorrect model passed.
413
413
  Expected: ${formatSelector(selector)}
414
- Received: ${model.constructor.name}`
414
+ Received: ${model.constructor.name}`,
415
+ cfg.displayName
415
416
  );
416
417
  model = null;
417
418
  }
package/core/XH.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  WebSocketService,
32
32
  ClientHealthService
33
33
  } from '@xh/hoist/svc';
34
+ import {getLogLevel, setLogLevel, LogLevel} from '@xh/hoist/utils/js';
34
35
  import {camelCase, flatten, isString, uniqueId} from 'lodash';
35
36
  import {Router, State} from 'router5';
36
37
  import {CancelFn} from 'router5/types/types/base';
@@ -360,6 +361,24 @@ export class XHApi {
360
361
  this.reloadApp();
361
362
  }
362
363
 
364
+ /**
365
+ * Current minimum severity for Hoist log utils (default 'info').
366
+ * Messages logged via managed Hoist log utils with lower severity will be ignored.
367
+ */
368
+ get logLevel(): LogLevel {
369
+ return getLogLevel();
370
+ }
371
+
372
+ /**
373
+ * Set the minimum severity for Hoist log utils until the page is refreshed. Optionally persist
374
+ * this adjustment to sessionStorage to maintain for the lifetime of the browser tab.
375
+ *
376
+ * Hint: call this method from the console to adjust your app's log level while troubleshooting.
377
+ */
378
+ setLogLevel(level: LogLevel, persistInSessionStorage: boolean = false) {
379
+ setLogLevel(level, persistInSessionStorage);
380
+ }
381
+
363
382
  //----------------------
364
383
  // App lifecycle support
365
384
  //----------------------
@@ -312,7 +312,7 @@ export class ExceptionHandler {
312
312
  }
313
313
 
314
314
  private logException(e: HoistException, opts: ExceptionHandlerOptions) {
315
- return opts.showAsError ? console.error(opts.message, e) : console.debug(opts.message);
315
+ return opts.showAsError ? logError([opts.message, e], this) : logDebug(opts.message, this);
316
316
  }
317
317
 
318
318
  private parseOptions(
@@ -135,7 +135,7 @@ export async function loadAllAsync(objs: Loadable[], loadSpec?: LoadSpec | any)
135
135
  ret = await Promise.allSettled(promises);
136
136
 
137
137
  ret.filter(it => it.status === 'rejected').forEach((err: any) =>
138
- console.error('Failed to Load Object', err.reason)
138
+ logError(['Failed to Load Object', err.reason])
139
139
  );
140
140
 
141
141
  return ret;
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {Some} from '@xh/hoist/core';
9
9
  import {CompoundFilter, FunctionFilter} from '@xh/hoist/data';
10
+ import {logError} from '@xh/hoist/utils/js';
10
11
  import {castArray, flatMap, groupBy, isArray, isFunction} from 'lodash';
11
12
  import {FieldFilter} from './FieldFilter';
12
13
  import {Filter} from './Filter';
@@ -55,7 +56,7 @@ export function parseFilter(spec: FilterLike): Filter {
55
56
  }
56
57
  }
57
58
 
58
- console.error('Unable to identify filter type:', s);
59
+ logError(['Unable to identify filter type:', s]);
59
60
  return null;
60
61
  }
61
62
 
@@ -7,7 +7,7 @@
7
7
  import {PlainObject} from '@xh/hoist/core';
8
8
  import {DashContainerModel} from '@xh/hoist/desktop/cmp/dash';
9
9
  import {serializeIcon} from '@xh/hoist/icon';
10
- import {throwIf} from '@xh/hoist/utils/js';
10
+ import {logDebug, throwIf} from '@xh/hoist/utils/js';
11
11
  import {isArray, isEmpty, isFinite, isNil, isPlainObject, isString, round} from 'lodash';
12
12
  import {DashContainerViewSpec} from '../DashContainerViewSpec';
13
13
  import GoldenLayout, {ContentItem} from 'golden-layout';
@@ -119,8 +119,9 @@ function convertStateToGLInner(items = [], viewSpecs = [], containerSize, contai
119
119
  const viewSpec = viewSpecs.find(v => v.id === item.id);
120
120
 
121
121
  if (!viewSpec) {
122
- console.debug(
123
- `Attempted to load non-existent or omitted view from state: ${item.id}`
122
+ logDebug(
123
+ `Attempted to load non-existent or omitted view from state: ${item.id}`,
124
+ 'DashContainer'
124
125
  );
125
126
  return null;
126
127
  }
@@ -10,8 +10,10 @@ import {fragment} from '@xh/hoist/cmp/layout';
10
10
  import {hoistCmp, HoistProps, PlainObject, Some, uses} from '@xh/hoist/core';
11
11
  import {MaskProps} from '@xh/hoist/cmp/mask';
12
12
  import {panel, PanelProps} from '@xh/hoist/desktop/cmp/panel';
13
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
13
14
  import '@xh/hoist/desktop/register';
14
15
  import {getTestId} from '@xh/hoist/utils/js';
16
+ import {isArray} from 'lodash';
15
17
  import {cloneElement, isValidElement, ReactElement, ReactNode} from 'react';
16
18
 
17
19
  import {restForm} from './impl/RestForm';
@@ -23,23 +25,33 @@ export interface RestGridProps
23
25
  Omit<PanelProps, 'model' | 'modelConfig' | 'modelRef'> {
24
26
  /**
25
27
  * This constitutes an 'escape hatch' for applications that need to get to the underlying
26
- * ag-Grid API. It should be used with care. Settings made here might be overwritten and/or
27
- * interfere with the implementation of this component and its use of the ag-Grid API.
28
+ * AG Grid API. Use with care - settings made here might be overwritten and/or interfere with
29
+ * the implementation of this component and its use of AG Grid.
28
30
  */
29
31
  agOptions?: PlainObject;
30
32
 
31
- /** Optional components rendered adjacent to the top toolbar's action buttons */
33
+ /**
34
+ * Optional components rendered adjacent to the top toolbar's action buttons.
35
+ * See also {@link tbar} to take full control of the toolbar.
36
+ */
32
37
  extraToolbarItems?: Some<ReactNode> | (() => Some<ReactNode>);
33
38
 
39
+ /** Classname to be passed to RestForm. */
40
+ formClassName?: string;
41
+
34
42
  /**
35
- * Mask to render on this Component. Defaults to true, which renders a standard
36
- * Hoist mask. Also can be set to false for no mask, or passed an element
37
- * specifying a Mask instance.
43
+ * Mask to render on this Component. Defaults to true, which renders a standard Hoist mask.
44
+ * Set to null/false for no mask, or pass a fully customized mask element.
38
45
  */
39
46
  mask?: ReactElement | boolean;
40
47
 
41
- /** Classname to be passed to RestForm. */
42
- formClassName?: string;
48
+ /**
49
+ * A custom toolbar to be docked above the grid. Note that this supersedes the default
50
+ * toolbar, meaning the `extraToolbarItems` prop will be ignored, as will the `RestGridModel`
51
+ * configs `toolbarActions`, `filterFields`, and `showRefreshButton`. If specified as an array,
52
+ * will be passed as children to a Toolbar component.
53
+ */
54
+ tbar?: Some<ReactNode>;
43
55
  }
44
56
 
45
57
  export const [RestGrid, restGrid] = hoistCmp.withFactory<RestGridProps>({
@@ -51,6 +63,7 @@ export const [RestGrid, restGrid] = hoistCmp.withFactory<RestGridProps>({
51
63
  const {
52
64
  model,
53
65
  extraToolbarItems,
66
+ tbar,
54
67
  mask = true,
55
68
  agOptions,
56
69
  formClassName,
@@ -63,7 +76,7 @@ export const [RestGrid, restGrid] = hoistCmp.withFactory<RestGridProps>({
63
76
  panel({
64
77
  ref,
65
78
  ...restProps,
66
- tbar: restGridToolbar({model, extraToolbarItems, testId}),
79
+ tbar: innerToolbar({model, tbar, extraToolbarItems, testId}),
67
80
  item: grid({model: gridModel, agOptions, testId: getTestId(testId, 'grid')}),
68
81
  mask: getMaskFromProp(model, mask)
69
82
  }),
@@ -76,6 +89,14 @@ export const [RestGrid, restGrid] = hoistCmp.withFactory<RestGridProps>({
76
89
  }
77
90
  });
78
91
 
92
+ const innerToolbar = hoistCmp.factory({
93
+ render({model, tbar, extraToolbarItems, testId}) {
94
+ if (isArray(tbar)) return toolbar(tbar);
95
+ if (tbar) return tbar;
96
+ return restGridToolbar({model, extraToolbarItems, testId});
97
+ }
98
+ });
99
+
79
100
  function getMaskFromProp(model, mask) {
80
101
  if (isValidElement(mask)) {
81
102
  mask = cloneElement<MaskProps>(mask, {bind: model.loadModel});
@@ -130,7 +130,7 @@ export class InstancesModel extends HoistModel {
130
130
  instance = this.getInstance(xhId);
131
131
 
132
132
  if (!instance) {
133
- console.warn(`Instance with xhId ${xhId} no longer alive - cannot be logged`);
133
+ this.logWarn(`Instance with xhId ${xhId} no longer alive - cannot be logged`);
134
134
  } else {
135
135
  console.log(`[${xhId}]`, instance);
136
136
  XH.toast({
@@ -147,7 +147,7 @@ export class InstancesModel extends HoistModel {
147
147
  instance = this.getInstance(instanceXhId);
148
148
 
149
149
  if (!instance) {
150
- console.warn(`Instance ${instanceDisplayName} no longer alive - cannot be logged`);
150
+ this.logWarn(`Instance ${instanceDisplayName} no longer alive - cannot be logged`);
151
151
  } else {
152
152
  console.log(`[${instanceDisplayName}].${property}`, instance[property]);
153
153
  XH.toast({
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {checkVersion} from '@xh/hoist/utils/js/VersionUtils';
8
+ import {checkVersion, logError} from '@xh/hoist/utils/js';
9
9
 
10
10
  /**
11
11
  * The exports below are ag-Grid components provided at runtime by applications.
@@ -57,7 +57,7 @@ const MAX_VERSION = '31.*.*';
57
57
  */
58
58
  export function installAgGrid(ComponentReactWrapper, version: string) {
59
59
  if (!checkVersion(version, MIN_VERSION, MAX_VERSION)) {
60
- console.error(
60
+ logError(
61
61
  `This version of Hoist requires an ag-Grid version between ${MIN_VERSION} and ` +
62
62
  `${MAX_VERSION}. Version ${version} detected. ag-Grid will be unavailable.`
63
63
  );
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {checkVersion} from '@xh/hoist/utils/js/VersionUtils';
8
+ import {checkVersion, logError} from '@xh/hoist/utils/js';
9
9
 
10
10
  export let Highcharts = null;
11
11
 
@@ -19,7 +19,7 @@ const MAX_VERSION = '11.*.*';
19
19
  export function installHighcharts(HighchartsImpl) {
20
20
  const {version} = HighchartsImpl;
21
21
  if (!checkVersion(version, MIN_VERSION, MAX_VERSION)) {
22
- console.error(
22
+ logError(
23
23
  `This version of Hoist requires a Highcharts version between ${MIN_VERSION} and ` +
24
24
  `${MAX_VERSION}. Version ${version} detected. Highcharts will be unavailable.`
25
25
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "76.0.0-SNAPSHOT.1756219543970",
3
+ "version": "76.0.0-SNAPSHOT.1756431654493",
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",
@@ -305,16 +305,3 @@ const enhancePromise = promisePrototype => {
305
305
 
306
306
  // Enhance canonical Promises.
307
307
  enhancePromise(Promise.prototype);
308
-
309
- // MS Edge returns a "native Promise" from async functions that won't get the enhancements above.
310
- // Check to see if we're in such an environment and enhance that prototype as well.
311
- // @see https://github.com/xh/hoist-react/issues/1411
312
- const asyncFnReturn = (async () => {})();
313
- if (!(asyncFnReturn instanceof Promise)) {
314
- console.debug(
315
- '"Native" Promise return detected as return from async function - enhancing prototype'
316
- );
317
-
318
- // @ts-ignore
319
- enhancePromise(asyncFnReturn.__proto__);
320
- }
@@ -261,7 +261,7 @@ export class FetchService extends HoistService {
261
261
  const {correlationIdHeaderKey} = this;
262
262
  if (opts.correlationId) {
263
263
  if (headers[correlationIdHeaderKey]) {
264
- console.warn(
264
+ this.logWarn(
265
265
  `Header ${correlationIdHeaderKey} value already set within FetchOptions.`
266
266
  );
267
267
  } else {