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

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;
@@ -165,12 +171,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
165
171
  openManageDialog(): void;
166
172
  closeManageDialog(): void;
167
173
  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>;
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,18 @@
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
+ updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean): Promise<void>;
17
+ deleteViewAsync(view: ViewInfo): Promise<void>;
18
+ }
@@ -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,11 +286,14 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
275
286
  this.saveTask = TaskObserver.trackLast({
276
287
  message: `Saving ${this.typeDisplayName}...`
277
288
  });
289
+
290
+ this.saveAsDialogModel = new SaveAsDialogModel(this);
291
+ this.api = new ViewToBlobApi(this);
278
292
  }
279
293
 
280
294
  private async initAsync() {
281
295
  try {
282
- const views = await this.fetchViewInfosAsync();
296
+ const views = await this.api.fetchViewInfosAsync();
283
297
  runInAction(() => (this.views = views));
284
298
 
285
299
  if (this.persistWith) {
@@ -308,7 +322,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
308
322
  override async doLoadAsync(loadSpec: LoadSpec) {
309
323
  try {
310
324
  // 1) Update all view info
311
- const views = await this.fetchViewInfosAsync();
325
+ const views = await this.api.fetchViewInfosAsync();
312
326
  if (loadSpec.isStale) return;
313
327
  runInAction(() => (this.views = views));
314
328
 
@@ -451,46 +465,15 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
451
465
  return null;
452
466
  }
453
467
 
454
- async deleteViewAsync(view: ViewInfo) {
455
- 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
- }
462
-
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
- }
474
-
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);
484
- } catch (e) {
485
- throw XH.exception({message: `Unable to create ${this.typeDisplayName}`, cause: e});
486
- }
487
- }
488
-
489
468
  //------------------
490
469
  // Implementation
491
470
  //------------------
492
- private loadViewAsync(info: ViewInfo, pendingValue: PendingValue<T> = null): Promise<void> {
493
- return this.fetchViewAsync(info)
471
+ private async loadViewAsync(
472
+ info: ViewInfo,
473
+ pendingValue: PendingValue<T> = null
474
+ ): Promise<void> {
475
+ return this.api
476
+ .fetchViewAsync(info)
494
477
  .thenAction(latest => {
495
478
  this.setAsView(latest, pendingValue?.token == info?.token ? pendingValue : null);
496
479
  this.providers.forEach(it => it.pushStateToTarget());
@@ -499,31 +482,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
499
482
  .linkTo(this.selectTask);
500
483
  }
501
484
 
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
485
  private async maybeAutoSaveAsync() {
528
486
  const {pendingValue, isViewAutoSavable, view} = this;
529
487
  if (isViewAutoSavable && pendingValue) {
@@ -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,92 @@
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 updateViewAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean) {
74
+ try {
75
+ await XH.jsonBlobService.updateAsync(view.token, {
76
+ name: name.trim(),
77
+ description: description?.trim(),
78
+ acl: isGlobal ? '*' : null
79
+ });
80
+ } catch (e) {
81
+ throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
82
+ }
83
+ }
84
+
85
+ async deleteViewAsync(view: ViewInfo) {
86
+ try {
87
+ await XH.jsonBlobService.archiveAsync(view.token);
88
+ } catch (e) {
89
+ throw XH.exception({message: `Unable to delete ${view.typedName}`, cause: e});
90
+ }
91
+ }
92
+ }
@@ -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.updateViewAsync(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.1733372979467",
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",