@xh/hoist 71.0.0-SNAPSHOT.1736198397776 → 71.0.0-SNAPSHOT.1736263796812

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.
@@ -15,7 +15,7 @@ export declare class DataAccess<T> {
15
15
  state: ViewUserState;
16
16
  }>;
17
17
  /** Fetch the latest version of a view. */
18
- fetchViewAsync(info: ViewInfo): Promise<View<T>>;
18
+ fetchViewAsync(token: string): Promise<View<T>>;
19
19
  /** Create a new view, owned by the current user.*/
20
20
  createViewAsync(spec: ViewCreateSpec): Promise<View<T>>;
21
21
  /** Update all aspects of a view's metadata.*/
@@ -183,7 +183,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
183
183
  */
184
184
  private constructor();
185
185
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
186
- selectViewAsync(info: ViewInfo, opts?: {
186
+ selectViewAsync(view: string | ViewInfo, opts?: {
187
187
  alertUnsavedChanges: boolean;
188
188
  }): Promise<void>;
189
189
  saveAsAsync(spec: ViewCreateSpec): Promise<void>;
@@ -7,5 +7,7 @@ export declare class ViewManagerLocalModel extends HoistModel {
7
7
  readonly manageDialogModel: ManageDialogModel;
8
8
  readonly saveAsDialogModel: SaveAsDialogModel;
9
9
  isVisible: boolean;
10
+ saveAsync(): Promise<void>;
11
+ revertAsync(): Promise<void>;
10
12
  constructor(parent: ViewManagerModel);
11
13
  }
@@ -48,14 +48,14 @@ export class DataAccess<T> {
48
48
  }
49
49
 
50
50
  /** Fetch the latest version of a view. */
51
- async fetchViewAsync(info: ViewInfo): Promise<View<T>> {
51
+ async fetchViewAsync(token: string): Promise<View<T>> {
52
52
  const {model} = this;
53
- if (!info) return View.createDefault(model);
53
+ if (!token) return View.createDefault(model);
54
54
  try {
55
- const raw = await XH.fetchJson({url: 'xhView/get', params: {token: info.token}});
55
+ const raw = await XH.fetchJson({url: 'xhView/get', params: {token}});
56
56
  return View.fromBlob(raw, model);
57
57
  } catch (e) {
58
- throw XH.exception({message: `Unable to fetch ${info.typedName}`, cause: e});
58
+ throw XH.exception({message: `Unable to fetch view with token ${token}`, cause: e});
59
59
  }
60
60
  }
61
61
 
@@ -5,6 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {SECONDS} from '@xh/hoist/utils/datetime';
8
+ import {throwIf} from '@xh/hoist/utils/js';
8
9
  import {ViewManagerModel} from './ViewManagerModel';
9
10
  import {JsonBlob} from '@xh/hoist/svc';
10
11
  import {PlainObject, XH} from '@xh/hoist/core';
@@ -57,6 +58,8 @@ export class ViewInfo {
57
58
  readonly model: ViewManagerModel;
58
59
 
59
60
  constructor(blob: JsonBlob, model: ViewManagerModel) {
61
+ throwIf(blob.type !== model.type, 'View type does not match ViewManager type.');
62
+
60
63
  this.token = blob.token;
61
64
  this.type = blob.type;
62
65
  this.name = blob.name;
@@ -21,7 +21,7 @@ import {fmtDateTime} from '@xh/hoist/format';
21
21
  import {action, bindable, makeObservable, observable, comparer, runInAction} from '@xh/hoist/mobx';
22
22
  import {olderThan, SECONDS} from '@xh/hoist/utils/datetime';
23
23
  import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
24
- import {find, isEqual, isNil, isNull, isUndefined, lowerCase, uniqBy} from 'lodash';
24
+ import {find, isEqual, isNil, isNull, isObject, isUndefined, lowerCase, uniqBy} from 'lodash';
25
25
  import {ReactNode} from 'react';
26
26
  import {ViewInfo} from './ViewInfo';
27
27
  import {View} from './View';
@@ -337,7 +337,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
337
337
  if (!view.isDefault) {
338
338
  const latestInfo = find(views, {token: view.token});
339
339
  if (latestInfo && latestInfo.lastUpdated > view.lastUpdated) {
340
- this.loadViewAsync(latestInfo, this.pendingValue);
340
+ this.loadViewAsync(view.token, this.pendingValue);
341
341
  }
342
342
  }
343
343
  } catch (e) {
@@ -346,7 +346,12 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
346
346
  }
347
347
  }
348
348
 
349
- async selectViewAsync(info: ViewInfo, opts = {alertUnsavedChanges: true}): Promise<void> {
349
+ async selectViewAsync(
350
+ view: string | ViewInfo,
351
+ opts = {alertUnsavedChanges: true}
352
+ ): Promise<void> {
353
+ const token = isObject(view) ? view.token : view;
354
+
350
355
  // ensure any pending auto-save gets completed
351
356
  if (this.isValueDirty && this.isViewAutoSavable) {
352
357
  await this.maybeAutoSaveAsync();
@@ -362,7 +367,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
362
367
  return;
363
368
  }
364
369
 
365
- await this.loadViewAsync(info).catch(e => this.handleException(e));
370
+ return this.loadViewAsync(token);
366
371
  }
367
372
 
368
373
  async saveAsAsync(spec: ViewCreateSpec): Promise<void> {
@@ -380,26 +385,22 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
380
385
  return;
381
386
  }
382
387
  const {pendingValue, view, dataAccess} = this;
383
- try {
384
- if (!(await this.maybeConfirmSaveAsync(view, pendingValue))) {
385
- return;
386
- }
387
- const updated = await dataAccess
388
- .updateViewValueAsync(view, pendingValue.value)
389
- .linkTo(this.saveTask);
390
388
 
391
- this.setAsView(updated);
392
- this.noteSuccess(`Saved ${view.typedName}`);
393
- } catch (e) {
394
- this.handleException(e, {
395
- message: `Failed to save ${view.typedName}. If this persists consider \`Save As...\`.`
396
- });
389
+ if (!(await this.maybeConfirmSaveAsync(view, pendingValue))) {
390
+ return;
397
391
  }
392
+ const updated = await dataAccess
393
+ .updateViewValueAsync(view, pendingValue.value)
394
+ .linkTo(this.saveTask);
395
+
396
+ this.setAsView(updated);
397
+ this.noteSuccess(`Saved ${view.typedName}`);
398
+
398
399
  this.refreshAsync();
399
400
  }
400
401
 
401
402
  async resetAsync(): Promise<void> {
402
- await this.loadViewAsync(this.view.info).catch(e => this.handleException(e));
403
+ return this.loadViewAsync(this.view.token);
403
404
  }
404
405
 
405
406
  //--------------------------------
@@ -483,7 +484,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
483
484
  const {views} = this;
484
485
 
485
486
  if (toDelete.some(view => view.isCurrentView) && !views.some(view => view.isCurrentView)) {
486
- await this.loadViewAsync(this.initialViewSpec?.(views));
487
+ await this.loadViewAsync(this.initialViewSpec?.(views)?.token);
487
488
  }
488
489
 
489
490
  if (exception) throw exception;
@@ -569,13 +570,13 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
569
570
  }
570
571
 
571
572
  private async loadViewAsync(
572
- info: ViewInfo,
573
+ token: string,
573
574
  pendingValue: PendingValue<T> = null
574
575
  ): Promise<void> {
575
576
  return this.dataAccess
576
- .fetchViewAsync(info)
577
+ .fetchViewAsync(token)
577
578
  .thenAction(latest => {
578
- this.setAsView(latest, pendingValue?.token == info?.token ? pendingValue : null);
579
+ this.setAsView(latest, pendingValue?.token == token ? pendingValue : null);
579
580
  this.providers.forEach(it => it.pushStateToTarget());
580
581
  this.lastPushed = Date.now();
581
582
  })
@@ -649,7 +650,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
649
650
 
650
651
  private async maybeConfirmSaveAsync(view: View, pendingValue: PendingValue<T>) {
651
652
  // Get latest from server for reference
652
- const latest = await this.dataAccess.fetchViewAsync(view.info),
653
+ const latest = await this.dataAccess.fetchViewAsync(view.token),
653
654
  isGlobal = latest.isGlobal,
654
655
  isStale = latest.lastUpdated > pendingValue.baseUpdated;
655
656
  if (!isStale && !isGlobal) return true;
@@ -118,7 +118,7 @@ const menuButton = hoistCmp.factory<ViewManagerLocalModel>({
118
118
  const saveButton = hoistCmp.factory<ViewManagerLocalModel>({
119
119
  render({model, mode, ...rest}) {
120
120
  if (hideStateButton(model, mode)) return null;
121
- const {parent, saveAsDialogModel} = model,
121
+ const {parent} = model,
122
122
  {typeDisplayName, isLoading, isValueDirty} = parent;
123
123
  return button({
124
124
  className: 'xh-view-manager__save-button',
@@ -126,9 +126,7 @@ const saveButton = hoistCmp.factory<ViewManagerLocalModel>({
126
126
  tooltip: `Save changes to this ${typeDisplayName}`,
127
127
  intent: 'primary',
128
128
  disabled: !isValueDirty || isLoading,
129
- onClick: () => {
130
- parent.isViewSavable ? parent.saveAsync() : saveAsDialogModel.open();
131
- },
129
+ onClick: () => model.saveAsync(),
132
130
  ...rest
133
131
  });
134
132
  }
@@ -144,7 +142,7 @@ const revertButton = hoistCmp.factory<ViewManagerLocalModel>({
144
142
  tooltip: `Revert changes to this ${typeDisplayName}`,
145
143
  intent: 'danger',
146
144
  disabled: !isValueDirty || isLoading,
147
- onClick: () => model.parent.resetAsync(),
145
+ onClick: () => model.revertAsync(),
148
146
  ...rest
149
147
  });
150
148
  }
@@ -23,6 +23,24 @@ export class ViewManagerLocalModel extends HoistModel {
23
23
  @bindable
24
24
  isVisible = true;
25
25
 
26
+ async saveAsync() {
27
+ const {parent} = this,
28
+ {view} = parent;
29
+
30
+ if (!parent.isViewSavable) {
31
+ this.saveAsDialogModel.open();
32
+ return;
33
+ }
34
+
35
+ return parent.saveAsync().catchDefault({
36
+ message: `Failed to save ${view.typedName}. If this persists consider \`Save As...\`.`
37
+ });
38
+ }
39
+
40
+ async revertAsync() {
41
+ return this.parent.resetAsync().catchDefault();
42
+ }
43
+
26
44
  constructor(parent: ViewManagerModel) {
27
45
  super();
28
46
  makeObservable(this);
@@ -63,7 +63,7 @@ function getNavMenuItems(model: ViewManagerModel): ReactNode[] {
63
63
  className: 'xh-view-manager__menu-item',
64
64
  icon: view.isDefault ? Icon.check() : Icon.placeholder(),
65
65
  text: `Default ${startCase(typeDisplayName)}`,
66
- onClick: () => model.selectViewAsync(null)
66
+ onClick: () => model.selectViewAsync(null).catchDefault()
67
67
  })
68
68
  );
69
69
  }
@@ -89,7 +89,7 @@ function getOtherMenuItems(model: ViewManagerLocalModel): ReactNode[] {
89
89
  icon: Icon.save(),
90
90
  text: 'Save',
91
91
  disabled: !isViewSavable || !isValueDirty,
92
- onClick: () => parent.saveAsync()
92
+ onClick: () => model.saveAsync()
93
93
  }),
94
94
  menuItem({
95
95
  icon: Icon.placeholder(),
@@ -100,7 +100,7 @@ function getOtherMenuItems(model: ViewManagerLocalModel): ReactNode[] {
100
100
  icon: Icon.reset(),
101
101
  text: `Revert`,
102
102
  disabled: !isValueDirty,
103
- onClick: () => parent.resetAsync()
103
+ onClick: () => model.revertAsync()
104
104
  }),
105
105
  menuDivider({omit: !enableAutoSave}),
106
106
  menuItem({
@@ -168,6 +168,6 @@ function viewMenuItem(view: ViewInfo, model: ViewManagerModel): ReactNode {
168
168
  text: view.name,
169
169
  title: title.join(' | '),
170
170
  icon,
171
- onClick: () => model.selectViewAsync(view)
171
+ onClick: () => model.selectViewAsync(view).catchDefault()
172
172
  });
173
173
  }
@@ -86,7 +86,7 @@ export class ManageDialogModel extends HoistModel {
86
86
  }
87
87
 
88
88
  activateSelectedViewAndClose() {
89
- this.viewManagerModel.selectViewAsync(this.selectedView);
89
+ this.viewManagerModel.selectViewAsync(this.selectedView).catchDefault();
90
90
  this.close();
91
91
  }
92
92
 
@@ -128,7 +128,7 @@ const formButtons = hoistCmp.factory<ViewPanelModel>({
128
128
  });
129
129
  }
130
130
 
131
- const {enableGlobal, globalDisplayName, manageGlobal, typeDisplayName, globalViews} =
131
+ const {enableGlobal, globalDisplayName, manageGlobal, globalViews} =
132
132
  parent.viewManagerModel;
133
133
  return vbox({
134
134
  style: {gap: 10, alignItems: 'center'},
@@ -144,7 +144,7 @@ const formButtons = hoistCmp.factory<ViewPanelModel>({
144
144
  onClick: () => parent.togglePinned([view])
145
145
  }),
146
146
  button({
147
- text: `Promote to ${capitalize(globalDisplayName)} ${typeDisplayName}`,
147
+ text: `Promote to ${capitalize(globalDisplayName)}`,
148
148
  icon: Icon.globe(),
149
149
  width: 200,
150
150
  outlined: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "71.0.0-SNAPSHOT.1736198397776",
3
+ "version": "71.0.0-SNAPSHOT.1736263796812",
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",