@xh/hoist 69.0.0-SNAPSHOT.1727923437594 → 69.0.0-SNAPSHOT.1728035585537

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
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## 69.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 🐞 Bug Fixes
6
+
7
+ * Added a workaround for a bug where Panel drag resizing was broken in Safari.
8
+
9
+ ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW )
10
+ * The `INITIALIZING` AppState has been replaced with more fine-grained states (see below). This
11
+ is not expected to affect any applications.
12
+
13
+ ### 🎁 New Features
14
+
15
+ * Added new AppStates `AUTHENTICATING`, `INITIALIZING_HOIST`, and `INITIALIZING_APP` to support
16
+ more granular tracking and timing of app startup lifecycle.
17
+ * Improved the default "Loaded App" activity tracking entry with more granular data on load timing.
18
+
5
19
  ## 68.1.0 - 2024-09-27
6
20
 
7
21
  ### 🎁 New Features
@@ -99,7 +99,7 @@ export class AppContainerModel extends HoistModel {
99
99
  @managed userAgentModel = new UserAgentModel();
100
100
 
101
101
  /**
102
- * Message shown on spinner while the application is in the INITIALIZING state.
102
+ * Message shown on spinner while the application is in a pre-running state.
103
103
  * Update within `AppModel.initAsync()` to relay app-specific initialization status.
104
104
  */
105
105
  @bindable initializingLoadMaskMessage: ReactNode;
@@ -108,7 +108,7 @@ export class AppContainerModel extends HoistModel {
108
108
  * Main entry point. Initialize and render application code.
109
109
  */
110
110
  renderApp<T extends HoistAppModel>(appSpec: AppSpec<T>) {
111
- // Remove the pre-load exception handler installed by preflight.js
111
+ // Remove the preload exception handler installed by preflight.js
112
112
  window.onerror = null;
113
113
  const spinner = document.getElementById('xh-preload-spinner');
114
114
  if (spinner) spinner.style.display = 'none';
@@ -180,6 +180,7 @@ export class AppContainerModel extends HoistModel {
180
180
  await installServicesAsync([FetchService]);
181
181
 
182
182
  // Check auth, locking out, or showing login if possible
183
+ this.setAppState('AUTHENTICATING');
183
184
  XH.authModel = new this.appSpec.authModelClass();
184
185
  const isAuthenticated = await XH.authModel.completeAuthAsync();
185
186
  if (!isAuthenticated) {
@@ -206,6 +207,7 @@ export class AppContainerModel extends HoistModel {
206
207
  */
207
208
  @action
208
209
  async completeInitAsync() {
210
+ this.setAppState('INITIALIZING_HOIST');
209
211
  try {
210
212
  // Install identity service and confirm access
211
213
  await installServicesAsync(IdentityService);
@@ -215,7 +217,6 @@ export class AppContainerModel extends HoistModel {
215
217
  }
216
218
 
217
219
  // Complete initialization process
218
- this.setAppState('INITIALIZING');
219
220
  await installServicesAsync([ConfigService, LocalStorageService]);
220
221
  await installServicesAsync(TrackService);
221
222
  await installServicesAsync([EnvironmentService, PrefService, JsonBlobService]);
@@ -272,6 +273,7 @@ export class AppContainerModel extends HoistModel {
272
273
  // Delay to workaround hot-reload styling issues in dev.
273
274
  await wait(XH.isDevelopmentMode ? 300 : 1);
274
275
 
276
+ this.setAppState('INITIALIZING_APP');
275
277
  const modelClass: any = this.appSpec.modelClass;
276
278
  this.appModel = modelClass.instance = new modelClass();
277
279
  await this.appModel.initAsync();
@@ -5,10 +5,10 @@
5
5
  * Copyright © 2024 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
8
- import {action, makeObservable, observable, reaction} from '@xh/hoist/mobx';
8
+ import {action, makeObservable, observable} from '@xh/hoist/mobx';
9
9
  import {Timer} from '@xh/hoist/utils/async';
10
10
  import {getClientDeviceInfo} from '@xh/hoist/utils/js';
11
- import {isBoolean, isString} from 'lodash';
11
+ import {camelCase, isBoolean, isString, mapKeys} from 'lodash';
12
12
 
13
13
  /**
14
14
  * Support for Core Hoist Application state and loading.
@@ -24,6 +24,10 @@ export class AppStateModel extends HoistModel {
24
24
  suspendData: AppSuspendData;
25
25
  accessDeniedMessage: string = 'Access Denied';
26
26
 
27
+ private timings: Record<AppState, number> = {} as Record<AppState, number>;
28
+ private loadStarted: number = window['_xhLoadTimestamp']; // set in index.html
29
+ private lastStateChangeTime: number = this.loadStarted;
30
+
27
31
  constructor() {
28
32
  super();
29
33
  makeObservable(this);
@@ -33,10 +37,14 @@ export class AppStateModel extends HoistModel {
33
37
 
34
38
  @action
35
39
  setAppState(nextState: AppState) {
36
- if (this.state !== nextState) {
37
- this.logDebug(`AppState change`, `${this.state} → ${nextState}`);
38
- }
40
+ if (this.state === nextState) return;
41
+
42
+ const {state, timings, lastStateChangeTime} = this,
43
+ now = Date.now();
44
+ timings[state] = (timings[state] ?? 0) + now - lastStateChangeTime;
45
+ this.lastStateChangeTime = now;
39
46
  this.state = nextState;
47
+ this.logDebug(`AppState change`, `${state} → ${nextState}`);
40
48
  }
41
49
 
42
50
  suspendApp(suspendData: AppSuspendData) {
@@ -70,39 +78,25 @@ export class AppStateModel extends HoistModel {
70
78
  // Implementation
71
79
  //------------------
72
80
  private trackLoad() {
73
- let loadStarted = window['_xhLoadTimestamp'], // set in index.html
74
- loginStarted = null,
75
- loginElapsed = 0;
76
-
77
- const disposer = reaction(
78
- () => this.state,
79
- state => {
80
- const now = Date.now();
81
- switch (state) {
82
- case 'RUNNING':
83
- XH.track({
84
- category: 'App',
85
- message: `Loaded ${XH.clientAppCode}`,
86
- elapsed: now - loadStarted - loginElapsed,
87
- data: {
88
- appVersion: XH.appVersion,
89
- appBuild: XH.appBuild,
90
- locationHref: window.location.href,
91
- ...getClientDeviceInfo()
92
- },
93
- logData: ['appVersion', 'appBuild'],
94
- omit: !XH.appSpec.trackAppLoad
95
- });
96
- disposer();
97
- break;
98
- case 'LOGIN_REQUIRED':
99
- loginStarted = now;
100
- break;
101
- default:
102
- if (loginStarted) loginElapsed = now - loginStarted;
103
- }
104
- }
105
- );
81
+ const {timings, loadStarted} = this;
82
+ this.addReaction({
83
+ when: () => this.state === 'RUNNING',
84
+ run: () =>
85
+ XH.track({
86
+ category: 'App',
87
+ message: `Loaded ${XH.clientAppCode}`,
88
+ elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
89
+ data: {
90
+ appVersion: XH.appVersion,
91
+ appBuild: XH.appBuild,
92
+ locationHref: window.location.href,
93
+ timings: mapKeys(timings, (v, k) => camelCase(k)),
94
+ ...getClientDeviceInfo()
95
+ },
96
+ logData: ['appVersion', 'appBuild'],
97
+ omit: !XH.appSpec.trackAppLoad
98
+ })
99
+ });
106
100
  }
107
101
 
108
102
  //---------------------
@@ -42,7 +42,7 @@ export declare class AppContainerModel extends HoistModel {
42
42
  themeModel: ThemeModel;
43
43
  userAgentModel: UserAgentModel;
44
44
  /**
45
- * Message shown on spinner while the application is in the INITIALIZING state.
45
+ * Message shown on spinner while the application is in a pre-running state.
46
46
  * Update within `AppModel.initAsync()` to relay app-specific initialization status.
47
47
  */
48
48
  initializingLoadMaskMessage: ReactNode;
@@ -10,6 +10,9 @@ export declare class AppStateModel extends HoistModel {
10
10
  lastActivityMs: number;
11
11
  suspendData: AppSuspendData;
12
12
  accessDeniedMessage: string;
13
+ private timings;
14
+ private loadStarted;
15
+ private lastStateChangeTime;
13
16
  constructor();
14
17
  setAppState(nextState: AppState): void;
15
18
  suspendApp(suspendData: AppSuspendData): void;
@@ -3,12 +3,14 @@
3
3
  */
4
4
  export declare const AppState: Readonly<{
5
5
  PRE_AUTH: "PRE_AUTH";
6
+ AUTHENTICATING: "AUTHENTICATING";
6
7
  LOGIN_REQUIRED: "LOGIN_REQUIRED";
7
- ACCESS_DENIED: "ACCESS_DENIED";
8
- INITIALIZING: "INITIALIZING";
8
+ INITIALIZING_HOIST: "INITIALIZING_HOIST";
9
+ INITIALIZING_APP: "INITIALIZING_APP";
9
10
  RUNNING: "RUNNING";
10
11
  SUSPENDED: "SUSPENDED";
11
12
  LOAD_FAILED: "LOAD_FAILED";
13
+ ACCESS_DENIED: "ACCESS_DENIED";
12
14
  }>;
13
15
  export type AppState = (typeof AppState)[keyof typeof AppState];
14
16
  export interface AppSuspendData {
@@ -9,13 +9,18 @@
9
9
  * Enumeration of possible App States
10
10
  */
11
11
  export const AppState = Object.freeze({
12
+ // Main Flow
12
13
  PRE_AUTH: 'PRE_AUTH',
14
+ AUTHENTICATING: 'AUTHENTICATING',
13
15
  LOGIN_REQUIRED: 'LOGIN_REQUIRED',
14
- ACCESS_DENIED: 'ACCESS_DENIED',
15
- INITIALIZING: 'INITIALIZING',
16
+ INITIALIZING_HOIST: 'INITIALIZING_HOIST',
17
+ INITIALIZING_APP: 'INITIALIZING_APP',
16
18
  RUNNING: 'RUNNING',
17
19
  SUSPENDED: 'SUSPENDED',
18
- LOAD_FAILED: 'LOAD_FAILED'
20
+
21
+ // Terminal Error States.
22
+ LOAD_FAILED: 'LOAD_FAILED',
23
+ ACCESS_DENIED: 'ACCESS_DENIED'
19
24
  });
20
25
 
21
26
  // eslint-disable-next-line
@@ -104,7 +104,9 @@ export const AppContainer = hoistCmp({
104
104
  function viewForState({model}) {
105
105
  switch (XH.appState) {
106
106
  case 'PRE_AUTH':
107
- case 'INITIALIZING':
107
+ case 'AUTHENTICATING':
108
+ case 'INITIALIZING_HOIST':
109
+ case 'INITIALIZING_APP':
108
110
  return viewport(
109
111
  mask({spinner: true, isDisplayed: true, message: model.initializingLoadMaskMessage})
110
112
  );
@@ -254,7 +254,9 @@ export class DraggerModel extends HoistModel {
254
254
  }
255
255
 
256
256
  private isValidMouseEvent(e) {
257
- return e.buttons && e.buttons !== 0;
257
+ // Note: We fall back to deprecated 'which' to work around a Safari issue where `buttons`
258
+ // was not being set. We may be able to remove in the future.
259
+ return (e.buttons && e.buttons !== 0) || (e.which && e.which !== 0);
258
260
  }
259
261
 
260
262
  private isValidTouchEvent(e) {
@@ -89,7 +89,9 @@ export const AppContainer = hoistCmp({
89
89
  function viewForState({model}) {
90
90
  switch (XH.appState) {
91
91
  case 'PRE_AUTH':
92
- case 'INITIALIZING':
92
+ case 'AUTHENTICATING':
93
+ case 'INITIALIZING_HOIST':
94
+ case 'INITIALIZING_APP':
93
95
  return viewport(
94
96
  mask({spinner: true, isDisplayed: true, message: model.initializingLoadMaskMessage})
95
97
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "69.0.0-SNAPSHOT.1727923437594",
3
+ "version": "69.0.0-SNAPSHOT.1728035585537",
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",