@xh/hoist 71.0.0-SNAPSHOT.1733372979467 → 71.0.0-SNAPSHOT.1733425883722

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.
@@ -17,12 +17,16 @@ export declare class View<T extends PlainObject = PlainObject> {
17
17
  * state of the components is captured.
18
18
  */
19
19
  readonly value: Partial<T>;
20
+ private readonly model;
21
+ get name(): string;
22
+ get token(): string;
23
+ get type(): string;
20
24
  get isDefault(): boolean;
21
25
  get isGlobal(): boolean;
22
26
  get lastUpdated(): number;
23
- get token(): string;
27
+ get typedName(): string;
24
28
  static fromBlob<T>(blob: JsonBlob, model: ViewManagerModel): View<T>;
25
- static createDefault<T>(): View<T>;
29
+ static createDefault<T>(model: ViewManagerModel): View<T>;
26
30
  withUpdatedValue(value: Partial<T>): View<T>;
27
- constructor(info: ViewInfo, value: Partial<T>);
31
+ constructor(info: ViewInfo, value: Partial<T>, model: ViewManagerModel);
28
32
  }
@@ -6,7 +6,7 @@ import { JsonBlob } from '@xh/hoist/svc';
6
6
  export declare class ViewInfo {
7
7
  /** Unique Id */
8
8
  readonly token: string;
9
- /** App-defined type discriminator, as per {@link ViewManagerConfig.viewType}. */
9
+ /** App-defined type discriminator, as per {@link ViewManagerConfig.type}. */
10
10
  readonly type: string;
11
11
  /** User-supplied descriptive name. */
12
12
  readonly name: string;
@@ -47,10 +47,10 @@ export interface ViewManagerConfig {
47
47
  * different viewManagers to be added to your app in the future - e.g. `portfolioGridView` or
48
48
  * `tradeBlotterDashboard`.
49
49
  */
50
- viewType: string;
50
+ type: string;
51
51
  /**
52
52
  * Optional user-facing display name for the view type, displayed in the ViewManager menu
53
- * and associated management dialogs and prompts. Defaulted from `viewType` if not provided.
53
+ * and associated management dialogs and prompts. Defaulted from `type` if not provided.
54
54
  */
55
55
  typeDisplayName?: string;
56
56
  /**
@@ -96,7 +96,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
96
96
  static createAsync(config: ViewManagerConfig): Promise<ViewManagerModel>;
97
97
  /** Immutable configuration for this model. */
98
98
  persistWith: ViewManagerPersistOptions;
99
- readonly viewType: string;
99
+ readonly type: string;
100
100
  readonly typeDisplayName: string;
101
101
  readonly globalDisplayName: string;
102
102
  readonly enableAutoSave: boolean;
@@ -156,7 +156,6 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
156
156
  * initial load before binding to persistable components.
157
157
  */
158
158
  private constructor();
159
- private initAsync;
160
159
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
161
160
  selectViewAsync(info: ViewInfo): Promise<void>;
162
161
  saveAsync(): Promise<void>;
@@ -171,6 +170,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
171
170
  openManageDialog(): void;
172
171
  closeManageDialog(): void;
173
172
  validateViewNameAsync(name: string, existing?: ViewInfo): Promise<string>;
173
+ private initAsync;
174
174
  private loadViewAsync;
175
175
  private maybeAutoSaveAsync;
176
176
  private setAsView;
@@ -13,6 +13,8 @@ export declare class ViewToBlobApi<T> {
13
13
  fetchViewInfosAsync(): Promise<ViewInfo[]>;
14
14
  fetchViewAsync(info: ViewInfo): Promise<View<T>>;
15
15
  createViewAsync(name: string, description: string, value: PlainObject): Promise<View<T>>;
16
- updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean): Promise<void>;
16
+ updateViewInfoAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean): Promise<View<T>>;
17
+ updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>>;
17
18
  deleteViewAsync(view: ViewInfo): Promise<void>;
19
+ private trackChange;
18
20
  }
@@ -23,7 +23,7 @@ export class SaveAsDialogModel extends HoistModel {
23
23
  private resolveOpen: (value: View) => void;
24
24
 
25
25
  get type(): string {
26
- return this.parent.viewType;
26
+ return this.parent.type;
27
27
  }
28
28
 
29
29
  get typeDisplayName(): string {
@@ -21,6 +21,20 @@ export class View<T extends PlainObject = PlainObject> {
21
21
  */
22
22
  readonly value: Partial<T> = null;
23
23
 
24
+ private readonly model: ViewManagerModel;
25
+
26
+ get name(): string {
27
+ return this.info?.name ?? 'Default';
28
+ }
29
+
30
+ get token(): string {
31
+ return this.info?.token ?? null;
32
+ }
33
+
34
+ get type(): string {
35
+ return this.model.type;
36
+ }
37
+
24
38
  get isDefault(): boolean {
25
39
  return !this.info;
26
40
  }
@@ -33,24 +47,25 @@ export class View<T extends PlainObject = PlainObject> {
33
47
  return this.info?.lastUpdated ?? null;
34
48
  }
35
49
 
36
- get token(): string {
37
- return this.info?.token ?? null;
50
+ get typedName(): string {
51
+ return `${this.model.typeDisplayName} '${this.name}'`;
38
52
  }
39
53
 
40
54
  static fromBlob<T>(blob: JsonBlob, model: ViewManagerModel): View<T> {
41
- return new View(new ViewInfo(blob, model), blob.value);
55
+ return new View(new ViewInfo(blob, model), blob.value, model);
42
56
  }
43
57
 
44
- static createDefault<T>(): View<T> {
45
- return new View(null, {});
58
+ static createDefault<T>(model: ViewManagerModel): View<T> {
59
+ return new View(null, {}, model);
46
60
  }
47
61
 
48
62
  withUpdatedValue(value: Partial<T>): View<T> {
49
- return new View(this.info, value);
63
+ return new View(this.info, value, this.model);
50
64
  }
51
65
 
52
- constructor(info: ViewInfo, value: Partial<T>) {
66
+ constructor(info: ViewInfo, value: Partial<T>, model: ViewManagerModel) {
53
67
  this.info = info;
54
68
  this.value = value;
69
+ this.model = model;
55
70
  }
56
71
  }
@@ -9,7 +9,7 @@ export class ViewInfo {
9
9
  /** Unique Id */
10
10
  readonly token: string;
11
11
 
12
- /** App-defined type discriminator, as per {@link ViewManagerConfig.viewType}. */
12
+ /** App-defined type discriminator, as per {@link ViewManagerConfig.type}. */
13
13
  readonly type: string;
14
14
 
15
15
  /** User-supplied descriptive name. */
@@ -82,11 +82,11 @@ export interface ViewManagerConfig {
82
82
  * different viewManagers to be added to your app in the future - e.g. `portfolioGridView` or
83
83
  * `tradeBlotterDashboard`.
84
84
  */
85
- viewType: string;
85
+ type: string;
86
86
 
87
87
  /**
88
88
  * Optional user-facing display name for the view type, displayed in the ViewManager menu
89
- * and associated management dialogs and prompts. Defaulted from `viewType` if not provided.
89
+ * and associated management dialogs and prompts. Defaulted from `type` if not provided.
90
90
  */
91
91
  typeDisplayName?: string;
92
92
 
@@ -141,7 +141,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
141
141
 
142
142
  /** Immutable configuration for this model. */
143
143
  declare persistWith: ViewManagerPersistOptions;
144
- readonly viewType: string;
144
+ readonly type: string;
145
145
  readonly typeDisplayName: string;
146
146
  readonly globalDisplayName: string;
147
147
  readonly enableAutoSave: boolean;
@@ -250,7 +250,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
250
250
  * initial load before binding to persistable components.
251
251
  */
252
252
  private constructor({
253
- viewType,
253
+ type,
254
254
  persistWith,
255
255
  typeDisplayName,
256
256
  globalDisplayName = 'global',
@@ -269,8 +269,8 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
269
269
  "ViewManagerModel requires 'initialViewSpec' if `enableDefault` is false."
270
270
  );
271
271
 
272
- this.viewType = viewType;
273
- this.typeDisplayName = lowerCase(typeDisplayName ?? genDisplayName(viewType));
272
+ this.type = type;
273
+ this.typeDisplayName = lowerCase(typeDisplayName ?? genDisplayName(type));
274
274
  this.globalDisplayName = globalDisplayName;
275
275
  this.persistWith = persistWith;
276
276
  this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
@@ -291,34 +291,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
291
291
  this.api = new ViewToBlobApi(this);
292
292
  }
293
293
 
294
- private async initAsync() {
295
- try {
296
- const views = await this.api.fetchViewInfosAsync();
297
- runInAction(() => (this.views = views));
298
-
299
- if (this.persistWith) {
300
- this.initPersist(this.persistWith);
301
- await when(() => !this.selectTask.isPending);
302
- }
303
-
304
- // If the initial view not initialized from persistence, assign it.
305
- if (!this.view) {
306
- await this.loadViewAsync(this.initialViewSpec?.(views), this.pendingValue);
307
- }
308
- } catch (e) {
309
- // Always ensure at least default view is installed.
310
- if (!this.view) this.loadViewAsync(null, this.pendingValue);
311
-
312
- this.handleException(e, {showAlert: false, logOnServer: true});
313
- }
314
-
315
- this.addReaction({
316
- track: () => [this.pendingValue, this.autoSave],
317
- run: () => this.maybeAutoSaveAsync(),
318
- debounce: 5 * SECONDS
319
- });
320
- }
321
-
322
294
  override async doLoadAsync(loadSpec: LoadSpec) {
323
295
  try {
324
296
  // 1) Update all view info
@@ -358,21 +330,20 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
358
330
  this.logError('Unexpected conditions for call to save, skipping');
359
331
  return;
360
332
  }
361
- const {pendingValue} = this,
362
- {info} = this.view;
333
+ const {pendingValue, view, api} = this;
363
334
  try {
364
- if (!(await this.maybeConfirmSaveAsync(info, pendingValue))) {
335
+ if (!(await this.maybeConfirmSaveAsync(view, pendingValue))) {
365
336
  return;
366
337
  }
367
- const update = await XH.jsonBlobService
368
- .updateAsync(info.token, {value: pendingValue.value})
338
+ const updated = await api
339
+ .updateViewValueAsync(view, pendingValue.value)
369
340
  .linkTo(this.saveTask);
370
341
 
371
- this.setAsView(View.fromBlob(update, this));
372
- this.noteSuccess(`Saved ${info.typedName}`);
342
+ this.setAsView(updated);
343
+ this.noteSuccess(`Saved ${view.typedName}`);
373
344
  } catch (e) {
374
345
  this.handleException(e, {
375
- message: `Failed to save ${info.typedName}. If this persists consider \`Save As...\`.`
346
+ message: `Failed to save ${view.typedName}. If this persists consider \`Save As...\`.`
376
347
  });
377
348
  }
378
349
  this.refreshAsync();
@@ -382,7 +353,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
382
353
  const view = (await this.saveAsDialogModel.openAsync()) as View<T>;
383
354
  if (view) {
384
355
  this.setAsView(view);
385
- this.noteSuccess(`Saved ${view.info.typedName}`);
356
+ this.noteSuccess(`Saved ${view.typedName}`);
386
357
  }
387
358
  this.refreshAsync();
388
359
  }
@@ -468,6 +439,34 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
468
439
  //------------------
469
440
  // Implementation
470
441
  //------------------
442
+ private async initAsync() {
443
+ try {
444
+ const views = await this.api.fetchViewInfosAsync();
445
+ runInAction(() => (this.views = views));
446
+
447
+ if (this.persistWith) {
448
+ this.initPersist(this.persistWith);
449
+ await when(() => !this.selectTask.isPending);
450
+ }
451
+
452
+ // If the initial view not initialized from persistence, assign it.
453
+ if (!this.view) {
454
+ await this.loadViewAsync(this.initialViewSpec?.(views), this.pendingValue);
455
+ }
456
+ } catch (e) {
457
+ // Always ensure at least default view is installed.
458
+ if (!this.view) this.loadViewAsync(null, this.pendingValue);
459
+
460
+ this.handleException(e, {showAlert: false, logOnServer: true});
461
+ }
462
+
463
+ this.addReaction({
464
+ track: () => [this.pendingValue, this.autoSave],
465
+ run: () => this.maybeAutoSaveAsync(),
466
+ debounce: 5 * SECONDS
467
+ });
468
+ }
469
+
471
470
  private async loadViewAsync(
472
471
  info: ViewInfo,
473
472
  pendingValue: PendingValue<T> = null
@@ -483,18 +482,19 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
483
482
  }
484
483
 
485
484
  private async maybeAutoSaveAsync() {
486
- const {pendingValue, isViewAutoSavable, view} = this;
485
+ const {pendingValue, isViewAutoSavable, view, api} = this;
487
486
  if (isViewAutoSavable && pendingValue) {
488
487
  try {
489
- const raw = await XH.jsonBlobService
490
- .updateAsync(view.token, {value: pendingValue.value})
488
+ const updated = await api
489
+ .updateViewValueAsync(view, pendingValue.value)
491
490
  .linkTo(this.saveTask);
492
- this.setAsView(View.fromBlob(raw, this));
491
+
492
+ this.setAsView(updated);
493
493
  } catch (e) {
494
494
  // TODO: How to alert but avoid for flaky or spam when user editing a deleted view
495
495
  // Keep count and alert server and user once at count n?
496
496
  XH.handleException(e, {
497
- message: `Failing AutoSave for ${this.view.info.typedName}`,
497
+ message: `Failing AutoSave for ${view.info.typedName}`,
498
498
  showAlert: false,
499
499
  logOnServer: false
500
500
  });
@@ -542,16 +542,16 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
542
542
  });
543
543
  }
544
544
 
545
- private async maybeConfirmSaveAsync(info: ViewInfo, pendingValue: PendingValue<T>) {
545
+ private async maybeConfirmSaveAsync(view: View, pendingValue: PendingValue<T>) {
546
546
  // Get latest from server for reference
547
- const latest = await this.api.fetchViewAsync(info),
547
+ const latest = await this.api.fetchViewAsync(view.info),
548
548
  isGlobal = latest.isGlobal,
549
549
  isStale = latest.lastUpdated > pendingValue.baseUpdated;
550
550
  if (!isStale && !isGlobal) return true;
551
551
 
552
552
  const latestInfo = latest.info,
553
553
  {typeDisplayName, globalDisplayName} = this,
554
- msgs: ReactNode[] = [`Save ${info.typedName}?`];
554
+ msgs: ReactNode[] = [`Save ${view.typedName}?`];
555
555
  if (isGlobal) {
556
556
  msgs.push(
557
557
  span(
@@ -30,7 +30,7 @@ export class ViewToBlobApi<T> {
30
30
  const {owner} = this;
31
31
  try {
32
32
  const blobs = await XH.jsonBlobService.listAsync({
33
- type: owner.viewType,
33
+ type: owner.type,
34
34
  includeValue: false
35
35
  });
36
36
  return blobs.map(b => new ViewInfo(b, owner));
@@ -43,7 +43,7 @@ export class ViewToBlobApi<T> {
43
43
  }
44
44
 
45
45
  async fetchViewAsync(info: ViewInfo): Promise<View<T>> {
46
- if (!info) return View.createDefault();
46
+ if (!info) return View.createDefault(this.owner);
47
47
  try {
48
48
  const blob = await XH.jsonBlobService.getAsync(info.token);
49
49
  return View.fromBlob(blob, this.owner);
@@ -59,34 +59,69 @@ export class ViewToBlobApi<T> {
59
59
  const {owner} = this;
60
60
  try {
61
61
  const blob = await XH.jsonBlobService.createAsync({
62
- type: owner.viewType,
62
+ type: owner.type,
63
63
  name: name.trim(),
64
64
  description: description?.trim(),
65
65
  value
66
66
  });
67
- return View.fromBlob(blob, owner);
67
+ const ret = View.fromBlob(blob, owner);
68
+ this.trackChange('Created View', ret);
69
+ return ret;
68
70
  } catch (e) {
69
71
  throw XH.exception({message: `Unable to create ${owner.typeDisplayName}`, cause: e});
70
72
  }
71
73
  }
72
74
 
73
- async updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean) {
75
+ async updateViewInfoAsync(
76
+ view: ViewInfo,
77
+ name: string,
78
+ description: string,
79
+ isGlobal: boolean
80
+ ): Promise<View<T>> {
74
81
  try {
75
- await XH.jsonBlobService.updateAsync(view.token, {
82
+ const blob = await XH.jsonBlobService.updateAsync(view.token, {
76
83
  name: name.trim(),
77
84
  description: description?.trim(),
78
85
  acl: isGlobal ? '*' : null
79
86
  });
87
+ const ret = View.fromBlob(blob, this.owner);
88
+ this.trackChange('Updated View Info', ret);
89
+ return ret;
80
90
  } catch (e) {
81
91
  throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
82
92
  }
83
93
  }
84
94
 
95
+ async updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>> {
96
+ try {
97
+ const blob = await XH.jsonBlobService.updateAsync(view.token, {value});
98
+ const ret = View.fromBlob(blob, this.owner);
99
+ if (ret.isGlobal) {
100
+ this.trackChange('Updated Global View definition', ret);
101
+ }
102
+ return ret;
103
+ } catch (e) {
104
+ throw XH.exception({
105
+ message: `Unable to update value for ${view.typedName}`,
106
+ cause: e
107
+ });
108
+ }
109
+ }
110
+
85
111
  async deleteViewAsync(view: ViewInfo) {
86
112
  try {
87
113
  await XH.jsonBlobService.archiveAsync(view.token);
114
+ this.trackChange('Deleted View', view);
88
115
  } catch (e) {
89
116
  throw XH.exception({message: `Unable to delete ${view.typedName}`, cause: e});
90
117
  }
91
118
  }
119
+
120
+ private trackChange(message: string, v: View | ViewInfo) {
121
+ XH.track({
122
+ message,
123
+ category: 'Views',
124
+ data: {name: v.name, token: v.token, isGlobal: v.isGlobal, type: v.type}
125
+ });
126
+ }
92
127
  }
@@ -92,7 +92,7 @@ const menuButton = hoistCmp.factory<ViewManagerModel>({
92
92
  const {view, typeDisplayName, isLoading} = model;
93
93
  return button({
94
94
  className: 'xh-view-manager__menu-button',
95
- text: view.info?.name ?? `Default ${startCase(typeDisplayName)}`,
95
+ text: view.isDefault ? `Default ${startCase(typeDisplayName)}` : view.name,
96
96
  icon: !isLoading
97
97
  ? Icon.bookmark()
98
98
  : box({
@@ -50,7 +50,7 @@ export const viewMenu = hoistCmp.factory<ViewManagerProps>({
50
50
  ...favoriteViews.map(info => {
51
51
  return menuItem({
52
52
  key: `${info.token}-favorite`,
53
- icon: view.info?.token === info.token ? Icon.check() : Icon.placeholder(),
53
+ icon: view.token === info.token ? Icon.check() : Icon.placeholder(),
54
54
  text: textAndFaveToggle({info}),
55
55
  onClick: () => model.selectViewAsync(info),
56
56
  title: info.description
@@ -158,7 +158,7 @@ export class ManageDialogModel extends HoistModel {
158
158
  ) {
159
159
  const {viewManagerModel} = this;
160
160
 
161
- await viewManagerModel.api.updateViewAsync(view, name, description, isGlobal);
161
+ await viewManagerModel.api.updateViewInfoAsync(view, name, description, isGlobal);
162
162
  await viewManagerModel.refreshAsync();
163
163
  await this.refreshAsync();
164
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "71.0.0-SNAPSHOT.1733372979467",
3
+ "version": "71.0.0-SNAPSHOT.1733425883722",
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",