@xh/hoist 71.0.0-SNAPSHOT.1733347475493 → 71.0.0-SNAPSHOT.1733376295239

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.
@@ -3,6 +3,7 @@ import type { ViewManagerProvider } from '@xh/hoist/core';
3
3
  import { SaveAsDialogModel } from './SaveAsDialogModel';
4
4
  import { ViewInfo } from './ViewInfo';
5
5
  import { View } from './View';
6
+ import { ViewToBlobApi } from './ViewToBlobApi';
6
7
  export interface ViewManagerConfig {
7
8
  /**
8
9
  * True (default) to allow user to opt in to automatically saving changes to their current view.
@@ -94,6 +95,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
94
95
  */
95
96
  static createAsync(config: ViewManagerConfig): Promise<ViewManagerModel>;
96
97
  /** Immutable configuration for this model. */
98
+ persistWith: ViewManagerPersistOptions;
97
99
  readonly viewType: string;
98
100
  readonly typeDisplayName: string;
99
101
  readonly globalDisplayName: string;
@@ -124,18 +126,22 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
124
126
  */
125
127
  saveTask: TaskObserver;
126
128
  manageDialogOpen: boolean;
127
- readonly saveAsDialogModel: SaveAsDialogModel;
129
+ saveAsDialogModel: SaveAsDialogModel;
130
+ /** Unsaved changes on the current view.*/
128
131
  private pendingValue;
129
- private lastPushed;
130
132
  /**
131
133
  * Array of {@link ViewManagerProvider} instances bound to this model. Providers will
132
134
  * push themselves onto this array when constructed with a reference to this model. Used to
133
135
  * proactively push state to the target components when the model's selected `value` changes.
134
- *
135
136
  * @internal
136
137
  */
137
138
  providers: ViewManagerProvider<any>[];
138
- persistWith: ViewManagerPersistOptions;
139
+ /**
140
+ * Data access for persisting views
141
+ * @internal
142
+ */
143
+ api: ViewToBlobApi<T>;
144
+ private lastPushed;
139
145
  get isValueDirty(): boolean;
140
146
  get isViewSavable(): boolean;
141
147
  get isViewAutoSavable(): boolean;
@@ -150,7 +156,6 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
150
156
  * initial load before binding to persistable components.
151
157
  */
152
158
  private constructor();
153
- private initAsync;
154
159
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
155
160
  selectViewAsync(info: ViewInfo): Promise<void>;
156
161
  saveAsync(): Promise<void>;
@@ -165,12 +170,8 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
165
170
  openManageDialog(): void;
166
171
  closeManageDialog(): void;
167
172
  validateViewNameAsync(name: string, existing?: ViewInfo): Promise<string>;
168
- deleteViewAsync(view: ViewInfo): Promise<void>;
169
- updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean): Promise<void>;
170
- createViewAsync(name: string, description: string, value: PlainObject): Promise<View>;
173
+ private initAsync;
171
174
  private loadViewAsync;
172
- private fetchViewAsync;
173
- private fetchViewInfosAsync;
174
175
  private maybeAutoSaveAsync;
175
176
  private setAsView;
176
177
  private handleException;
@@ -0,0 +1,19 @@
1
+ import { PlainObject } from '@xh/hoist/core';
2
+ import { ViewInfo } from './ViewInfo';
3
+ import { View } from './View';
4
+ import { ViewManagerModel } from './ViewManagerModel';
5
+ /**
6
+ * Class for accessing and updating views using JSON Blobs Service.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare class ViewToBlobApi<T> {
11
+ private owner;
12
+ constructor(owner: ViewManagerModel<T>);
13
+ fetchViewInfosAsync(): Promise<ViewInfo[]>;
14
+ fetchViewAsync(info: ViewInfo): Promise<View<T>>;
15
+ createViewAsync(name: string, description: string, value: PlainObject): Promise<View<T>>;
16
+ updateViewInfoAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean): Promise<View<T>>;
17
+ updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>>;
18
+ deleteViewAsync(view: ViewInfo): Promise<void>;
19
+ }
@@ -81,7 +81,7 @@ export class SaveAsDialogModel extends HoistModel {
81
81
  if (!isValid) return;
82
82
 
83
83
  try {
84
- const ret = await this.parent.createViewAsync(name, description, parent.getValue());
84
+ const ret = await this.parent.api.createViewAsync(name, description, parent.getValue());
85
85
  this.close();
86
86
  this.resolveOpen(ret);
87
87
  } catch (e) {
@@ -30,6 +30,7 @@ import {ReactNode} from 'react';
30
30
  import {SaveAsDialogModel} from './SaveAsDialogModel';
31
31
  import {ViewInfo} from './ViewInfo';
32
32
  import {View} from './View';
33
+ import {ViewToBlobApi} from './ViewToBlobApi';
33
34
 
34
35
  export interface ViewManagerConfig {
35
36
  /**
@@ -139,6 +140,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
139
140
  }
140
141
 
141
142
  /** Immutable configuration for this model. */
143
+ declare persistWith: ViewManagerPersistOptions;
142
144
  readonly viewType: string;
143
145
  readonly typeDisplayName: string;
144
146
  readonly globalDisplayName: string;
@@ -174,25 +176,35 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
174
176
  saveTask: TaskObserver;
175
177
 
176
178
  @observable manageDialogOpen = false;
177
- @managed readonly saveAsDialogModel: SaveAsDialogModel;
178
-
179
- // Unsaved changes on the current view.
180
- @observable.ref private pendingValue: PendingValue<T> = null;
179
+ @managed saveAsDialogModel: SaveAsDialogModel;
181
180
 
182
- // Last time changes were pushed to linked persistence providers
183
- private lastPushed: number = null;
181
+ //-----------------------
182
+ // Private, internal state.
183
+ //-------------------------
184
+ /** Unsaved changes on the current view.*/
185
+ @observable.ref
186
+ private pendingValue: PendingValue<T> = null;
184
187
 
185
188
  /**
186
189
  * Array of {@link ViewManagerProvider} instances bound to this model. Providers will
187
190
  * push themselves onto this array when constructed with a reference to this model. Used to
188
191
  * proactively push state to the target components when the model's selected `value` changes.
189
- *
190
192
  * @internal
191
193
  */
192
194
  providers: ViewManagerProvider<any>[] = [];
193
195
 
194
- declare persistWith: ViewManagerPersistOptions;
196
+ /**
197
+ * Data access for persisting views
198
+ * @internal
199
+ */
200
+ api: ViewToBlobApi<T>;
201
+
202
+ // Last time changes were pushed to linked persistence providers
203
+ private lastPushed: number = null;
195
204
 
205
+ //---------------
206
+ // Getters
207
+ //---------------
196
208
  get isValueDirty(): boolean {
197
209
  return !!this.pendingValue;
198
210
  }
@@ -266,7 +278,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
266
278
  this.enableAutoSave = enableAutoSave;
267
279
  this.enableFavorites = enableFavorites;
268
280
  this.settleTime = settleTime;
269
- this.saveAsDialogModel = new SaveAsDialogModel(this);
270
281
  this.initialViewSpec = initialViewSpec;
271
282
 
272
283
  this.selectTask = TaskObserver.trackLast({
@@ -275,40 +286,15 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
275
286
  this.saveTask = TaskObserver.trackLast({
276
287
  message: `Saving ${this.typeDisplayName}...`
277
288
  });
278
- }
279
-
280
- private async initAsync() {
281
- try {
282
- const views = await this.fetchViewInfosAsync();
283
- runInAction(() => (this.views = views));
284
-
285
- if (this.persistWith) {
286
- this.initPersist(this.persistWith);
287
- await when(() => !this.selectTask.isPending);
288
- }
289
289
 
290
- // If the initial view not initialized from persistence, assign it.
291
- if (!this.view) {
292
- await this.loadViewAsync(this.initialViewSpec?.(views), this.pendingValue);
293
- }
294
- } catch (e) {
295
- // Always ensure at least default view is installed.
296
- if (!this.view) this.loadViewAsync(null, this.pendingValue);
297
-
298
- this.handleException(e, {showAlert: false, logOnServer: true});
299
- }
300
-
301
- this.addReaction({
302
- track: () => [this.pendingValue, this.autoSave],
303
- run: () => this.maybeAutoSaveAsync(),
304
- debounce: 5 * SECONDS
305
- });
290
+ this.saveAsDialogModel = new SaveAsDialogModel(this);
291
+ this.api = new ViewToBlobApi(this);
306
292
  }
307
293
 
308
294
  override async doLoadAsync(loadSpec: LoadSpec) {
309
295
  try {
310
296
  // 1) Update all view info
311
- const views = await this.fetchViewInfosAsync();
297
+ const views = await this.api.fetchViewInfosAsync();
312
298
  if (loadSpec.isStale) return;
313
299
  runInAction(() => (this.views = views));
314
300
 
@@ -344,21 +330,20 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
344
330
  this.logError('Unexpected conditions for call to save, skipping');
345
331
  return;
346
332
  }
347
- const {pendingValue} = this,
348
- {info} = this.view;
333
+ const {pendingValue, view, api} = this;
349
334
  try {
350
- if (!(await this.maybeConfirmSaveAsync(info, pendingValue))) {
335
+ if (!(await this.maybeConfirmSaveAsync(view.info, pendingValue))) {
351
336
  return;
352
337
  }
353
- const update = await XH.jsonBlobService
354
- .updateAsync(info.token, {value: pendingValue.value})
338
+ const updated = await api
339
+ .updateViewValueAsync(view, pendingValue.value)
355
340
  .linkTo(this.saveTask);
356
341
 
357
- this.setAsView(View.fromBlob(update, this));
358
- this.noteSuccess(`Saved ${info.typedName}`);
342
+ this.setAsView(updated);
343
+ this.noteSuccess(`Saved ${view.info.typedName}`);
359
344
  } catch (e) {
360
345
  this.handleException(e, {
361
- message: `Failed to save ${info.typedName}. If this persists consider \`Save As...\`.`
346
+ message: `Failed to save ${view.info.typedName}. If this persists consider \`Save As...\`.`
362
347
  });
363
348
  }
364
349
  this.refreshAsync();
@@ -451,46 +436,43 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
451
436
  return null;
452
437
  }
453
438
 
454
- async deleteViewAsync(view: ViewInfo) {
439
+ //------------------
440
+ // Implementation
441
+ //------------------
442
+ private async initAsync() {
455
443
  try {
456
- await XH.jsonBlobService.archiveAsync(view.token);
457
- this.removeFavorite(view.token);
458
- } catch (e) {
459
- throw XH.exception({message: `Unable to delete ${view.typedName}`, cause: e});
460
- }
461
- }
444
+ const views = await this.api.fetchViewInfosAsync();
445
+ runInAction(() => (this.views = views));
462
446
 
463
- async updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean) {
464
- try {
465
- await XH.jsonBlobService.updateAsync(view.token, {
466
- name: name.trim(),
467
- description: description?.trim(),
468
- acl: isGlobal ? '*' : null
469
- });
470
- } catch (e) {
471
- throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
472
- }
473
- }
447
+ if (this.persistWith) {
448
+ this.initPersist(this.persistWith);
449
+ await when(() => !this.selectTask.isPending);
450
+ }
474
451
 
475
- async createViewAsync(name: string, description: string, value: PlainObject): Promise<View> {
476
- try {
477
- const blob = await XH.jsonBlobService.createAsync({
478
- type: this.viewType,
479
- name: name.trim(),
480
- description: description?.trim(),
481
- value
482
- });
483
- return View.fromBlob(blob, this);
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
+ }
484
456
  } catch (e) {
485
- throw XH.exception({message: `Unable to create ${this.typeDisplayName}`, cause: 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});
486
461
  }
462
+
463
+ this.addReaction({
464
+ track: () => [this.pendingValue, this.autoSave],
465
+ run: () => this.maybeAutoSaveAsync(),
466
+ debounce: 5 * SECONDS
467
+ });
487
468
  }
488
469
 
489
- //------------------
490
- // Implementation
491
- //------------------
492
- private loadViewAsync(info: ViewInfo, pendingValue: PendingValue<T> = null): Promise<void> {
493
- return this.fetchViewAsync(info)
470
+ private async loadViewAsync(
471
+ info: ViewInfo,
472
+ pendingValue: PendingValue<T> = null
473
+ ): Promise<void> {
474
+ return this.api
475
+ .fetchViewAsync(info)
494
476
  .thenAction(latest => {
495
477
  this.setAsView(latest, pendingValue?.token == info?.token ? pendingValue : null);
496
478
  this.providers.forEach(it => it.pushStateToTarget());
@@ -499,44 +481,20 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
499
481
  .linkTo(this.selectTask);
500
482
  }
501
483
 
502
- private async fetchViewAsync(info: ViewInfo): Promise<View<T>> {
503
- if (!info) return View.createDefault();
504
- try {
505
- const blob = await XH.jsonBlobService.getAsync(info.token);
506
- return View.fromBlob(blob, this);
507
- } catch (e) {
508
- throw XH.exception({message: `Unable to fetch ${info.typedName}`, cause: e});
509
- }
510
- }
511
-
512
- private async fetchViewInfosAsync(): Promise<ViewInfo[]> {
513
- try {
514
- const blobs = await XH.jsonBlobService.listAsync({
515
- type: this.viewType,
516
- includeValue: false
517
- });
518
- return blobs.map(b => new ViewInfo(b, this));
519
- } catch (e) {
520
- throw XH.exception({
521
- message: `Unable to fetch ${pluralize(this.typeDisplayName)}`,
522
- cause: e
523
- });
524
- }
525
- }
526
-
527
484
  private async maybeAutoSaveAsync() {
528
- const {pendingValue, isViewAutoSavable, view} = this;
485
+ const {pendingValue, isViewAutoSavable, view, api} = this;
529
486
  if (isViewAutoSavable && pendingValue) {
530
487
  try {
531
- const raw = await XH.jsonBlobService
532
- .updateAsync(view.token, {value: pendingValue.value})
488
+ const updated = await api
489
+ .updateViewValueAsync(view, pendingValue.value)
533
490
  .linkTo(this.saveTask);
534
- this.setAsView(View.fromBlob(raw, this));
491
+
492
+ this.setAsView(updated);
535
493
  } catch (e) {
536
494
  // TODO: How to alert but avoid for flaky or spam when user editing a deleted view
537
495
  // Keep count and alert server and user once at count n?
538
496
  XH.handleException(e, {
539
- message: `Failing AutoSave for ${this.view.info.typedName}`,
497
+ message: `Failing AutoSave for ${view.info.typedName}`,
540
498
  showAlert: false,
541
499
  logOnServer: false
542
500
  });
@@ -586,7 +544,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
586
544
 
587
545
  private async maybeConfirmSaveAsync(info: ViewInfo, pendingValue: PendingValue<T>) {
588
546
  // Get latest from server for reference
589
- const latest = await this.fetchViewAsync(info),
547
+ const latest = await this.api.fetchViewAsync(info),
590
548
  isGlobal = latest.isGlobal,
591
549
  isStale = latest.lastUpdated > pendingValue.baseUpdated;
592
550
  if (!isStale && !isGlobal) return true;
@@ -0,0 +1,110 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2024 Extremely Heavy Industries Inc.
6
+ */
7
+
8
+ import {PlainObject, XH} from '@xh/hoist/core';
9
+ import {pluralize} from '@xh/hoist/utils/js';
10
+ import {ViewInfo} from './ViewInfo';
11
+ import {View} from './View';
12
+ import {ViewManagerModel} from './ViewManagerModel';
13
+
14
+ /**
15
+ * Class for accessing and updating views using JSON Blobs Service.
16
+ *
17
+ * @internal
18
+ */
19
+ export class ViewToBlobApi<T> {
20
+ private owner: ViewManagerModel<T>;
21
+
22
+ constructor(owner: ViewManagerModel<T>) {
23
+ this.owner = owner;
24
+ }
25
+
26
+ //---------------
27
+ // Load/search.
28
+ //---------------
29
+ async fetchViewInfosAsync(): Promise<ViewInfo[]> {
30
+ const {owner} = this;
31
+ try {
32
+ const blobs = await XH.jsonBlobService.listAsync({
33
+ type: owner.viewType,
34
+ includeValue: false
35
+ });
36
+ return blobs.map(b => new ViewInfo(b, owner));
37
+ } catch (e) {
38
+ throw XH.exception({
39
+ message: `Unable to fetch ${pluralize(owner.typeDisplayName)}`,
40
+ cause: e
41
+ });
42
+ }
43
+ }
44
+
45
+ async fetchViewAsync(info: ViewInfo): Promise<View<T>> {
46
+ if (!info) return View.createDefault();
47
+ try {
48
+ const blob = await XH.jsonBlobService.getAsync(info.token);
49
+ return View.fromBlob(blob, this.owner);
50
+ } catch (e) {
51
+ throw XH.exception({message: `Unable to fetch ${info.typedName}`, cause: e});
52
+ }
53
+ }
54
+
55
+ //-----------------
56
+ // Crud
57
+ //-----------------
58
+ async createViewAsync(name: string, description: string, value: PlainObject): Promise<View<T>> {
59
+ const {owner} = this;
60
+ try {
61
+ const blob = await XH.jsonBlobService.createAsync({
62
+ type: owner.viewType,
63
+ name: name.trim(),
64
+ description: description?.trim(),
65
+ value
66
+ });
67
+ return View.fromBlob(blob, owner);
68
+ } catch (e) {
69
+ throw XH.exception({message: `Unable to create ${owner.typeDisplayName}`, cause: e});
70
+ }
71
+ }
72
+
73
+ async updateViewInfoAsync(
74
+ view: ViewInfo,
75
+ name: string,
76
+ description: string,
77
+ isGlobal: boolean
78
+ ): Promise<View<T>> {
79
+ try {
80
+ const blob = await XH.jsonBlobService.updateAsync(view.token, {
81
+ name: name.trim(),
82
+ description: description?.trim(),
83
+ acl: isGlobal ? '*' : null
84
+ });
85
+ return View.fromBlob(blob, this.owner);
86
+ } catch (e) {
87
+ throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
88
+ }
89
+ }
90
+
91
+ async updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>> {
92
+ try {
93
+ const blob = await XH.jsonBlobService.updateAsync(view.token, {value});
94
+ return View.fromBlob(blob, this.owner);
95
+ } catch (e) {
96
+ throw XH.exception({
97
+ message: `Unable to update value for ${view.info.typedName}`,
98
+ cause: e
99
+ });
100
+ }
101
+ }
102
+
103
+ async deleteViewAsync(view: ViewInfo) {
104
+ try {
105
+ await XH.jsonBlobService.archiveAsync(view.token);
106
+ } catch (e) {
107
+ throw XH.exception({message: `Unable to delete ${view.typedName}`, cause: e});
108
+ }
109
+ }
110
+ }
@@ -15,7 +15,7 @@ import {formField} from '@xh/hoist/desktop/cmp/form';
15
15
  import {select, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
16
16
  import {panel} from '@xh/hoist/desktop/cmp/panel';
17
17
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
18
- import {fmtCompactDate} from '@xh/hoist/format';
18
+ import {fmtDateTime} from '@xh/hoist/format';
19
19
  import {Icon} from '@xh/hoist/icon';
20
20
  import {startCase} from 'lodash';
21
21
 
@@ -98,7 +98,7 @@ export const editForm = hoistCmp.factory({
98
98
  filler(),
99
99
  div({
100
100
  className: 'xh-view-manager__manage-dialog__metadata',
101
- item: `Last Updated: ${fmtCompactDate(lastUpdated)} (${lastUpdatedBy === XH.getUsername() ? 'you' : lastUpdatedBy})`
101
+ item: `Last Updated: ${fmtDateTime(lastUpdated)} by ${lastUpdatedBy === XH.getUsername() ? 'you' : lastUpdatedBy}`
102
102
  })
103
103
  ]
104
104
  })
@@ -158,7 +158,7 @@ export class ManageDialogModel extends HoistModel {
158
158
  ) {
159
159
  const {viewManagerModel} = this;
160
160
 
161
- await viewManagerModel.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
 
@@ -200,7 +200,7 @@ export class ManageDialogModel extends HoistModel {
200
200
  if (!confirmed) return;
201
201
 
202
202
  for (const view of views) {
203
- await viewManagerModel.deleteViewAsync(view);
203
+ await viewManagerModel.api.deleteViewAsync(view);
204
204
  }
205
205
 
206
206
  await viewManagerModel.refreshAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "71.0.0-SNAPSHOT.1733347475493",
3
+ "version": "71.0.0-SNAPSHOT.1733376295239",
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",