@xh/hoist 70.0.0 → 71.0.0-SNAPSHOT.1731971865033

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
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v71.0.0-SNAPSHOT - unreleased
4
+
5
+
6
+ ### ⚙️ Technical
7
+ * Misc. Improvements to ViewManager
8
+
9
+
3
10
  ## v70.0.0 - 2024-11-15
4
11
 
5
12
  ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - changes to advanced persistence APIs)
@@ -19,8 +19,10 @@ export interface View<T extends PlainObject = PlainObject> {
19
19
  isShared: boolean;
20
20
  lastUpdated: number;
21
21
  lastUpdatedBy: string;
22
- /** User-supplied descriptive name. */
22
+ /** User-supplied descriptive name, including folder designating prefix. */
23
23
  name: string;
24
+ /** User-supplied descriptive name, without folder designating prefix. */
25
+ shortName: string;
24
26
  /** Original creator of the view, and the only user with access to it if not shared. */
25
27
  owner: string;
26
28
  token: string;
@@ -75,15 +75,16 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
75
75
  pendingValue: T;
76
76
  /** Loaded saved view definitions - both private and shared. */
77
77
  views: View<T>[];
78
- /** Token identifier for the currently selected view, or null if in default mode. */
78
+ /** Currently selected view, or null if in default mode. Token only will be set during pre-loading.*/
79
79
  selectedToken: string;
80
+ selectedView: View<T>;
80
81
  /** List of tokens for the user's favorite views. */
81
82
  favorites: string[];
82
83
  /**
83
84
  * True if user has opted-in to automatically saving changes to personal views (if auto-save
84
85
  * generally available as per `enableAutoSave`).
85
86
  */
86
- autoSaveActive: boolean;
87
+ autoSave: boolean;
87
88
  /**
88
89
  * TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
89
90
  * require intensive layout/grid work, consider masking affected components with this observer.
@@ -99,17 +100,9 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
99
100
  */
100
101
  providers: ViewManagerProvider<any>[];
101
102
  get enableSharing(): boolean;
102
- get selectedView(): View<T>;
103
- get isSharedViewSelected(): boolean;
104
103
  get canSave(): boolean;
105
- /**
106
- * True if displaying the save button is appropriate from the model's point of view, even if
107
- * that button might be disabled due to no changes having been made. Works in concert with the
108
- * desktop ViewManager component's `showSaveButton` prop.
109
- */
110
- get canShowSaveButton(): boolean;
111
- get enableAutoSaveToggle(): boolean;
112
- get disabledAutoSaveReason(): string;
104
+ get canAutoSave(): boolean;
105
+ get autoSaveUnavailableReason(): string;
113
106
  get isDirty(): boolean;
114
107
  get isShared(): boolean;
115
108
  get favoriteViews(): View<T>[];
@@ -124,31 +117,29 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
124
117
  private constructor();
125
118
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
126
119
  selectViewAsync(token: string): Promise<void>;
127
- saveAsync(skipToast?: boolean): Promise<void>;
120
+ saveAsync(): Promise<void>;
128
121
  saveAsAsync(): Promise<void>;
129
122
  resetAsync(): Promise<void>;
130
123
  setPendingValue(pendingValue: T): void;
131
124
  openManageDialog(): void;
132
125
  closeManageDialog(): void;
133
- getHierarchyDisplayName(name: string): string;
134
126
  toggleFavorite(token: string): void;
135
127
  addFavorite(token: string): void;
136
128
  removeFavorite(token: string): void;
137
129
  isFavorite(token: string): boolean;
138
130
  getPersistableState(): PersistableState<ViewManagerModelPersistState>;
139
131
  setPersistableState(state: PersistableState<ViewManagerModelPersistState>): void;
132
+ private selectViewInternalAsync;
140
133
  private processRaw;
141
134
  private setValue;
142
135
  private cleanValue;
143
136
  private confirmSaveForSharedViewAsync;
144
137
  private maybeAutoSaveAsync;
145
- private buildViewTree;
146
- private getNameHierarchySubstring;
147
- private isFolderForEntry;
148
138
  private onFavoritesChange;
149
139
  }
150
140
  interface ViewManagerModelPersistState {
151
- selectedToken: string;
152
- favorites: string[];
141
+ selectedToken?: string;
142
+ favorites?: string[];
143
+ autoSave?: boolean;
153
144
  }
154
145
  export {};
@@ -0,0 +1,8 @@
1
+ import { View, ViewManagerModel, ViewTree } from '@xh/hoist/core/persist/viewmanager';
2
+ /**
3
+ * Create a menu-friendly, tree representation of a set of views, using the `\`
4
+ * in view names to create folders.
5
+ *
6
+ * @internal
7
+ */
8
+ export declare function buildViewTree(views: View[], model: ViewManagerModel): ViewTree[];
@@ -3,11 +3,25 @@ import { HoistProps } from '@xh/hoist/core';
3
3
  import './ViewManager.scss';
4
4
  import { ViewManagerModel } from '@xh/hoist/core/persist/viewmanager/ViewManagerModel';
5
5
  import { ButtonProps } from '@xh/hoist/desktop/cmp/button';
6
+ /**
7
+ * Visibility options for save/revert button.
8
+ *
9
+ * 'never' to hide button.
10
+ * 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
11
+ * 'always' will always show button, unless autoSave is active.
12
+ *
13
+ * Note that we never show the button when 'autoSave' is active because it would never be enabled
14
+ * for more than a flash.
15
+ */
16
+ export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
6
17
  export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
7
18
  menuButtonProps?: Partial<ButtonProps>;
8
19
  saveButtonProps?: Partial<ButtonProps>;
9
- /** 'whenDirty' to only show saveButton when persistence state is dirty. (Default 'whenDirty') */
10
- showSaveButton?: 'whenDirty' | 'always' | 'never';
20
+ revertButtonProps?: Partial<ButtonProps>;
21
+ /** Default 'whenDirty' */
22
+ showSaveButton?: ViewManagerStateButtonMode;
23
+ /** Default 'never' */
24
+ showRevertButton?: ViewManagerStateButtonMode;
11
25
  /** True to render private views in sub-menu (Default false)*/
12
26
  showPrivateViewsInSubMenu?: boolean;
13
27
  /** True to render shared views in sub-menu (Default false)*/
@@ -1,3 +1 @@
1
1
  export * from './ViewManager';
2
- export * from './cmp/ManageDialog';
3
- export * from './cmp/SaveDialog';
@@ -42,11 +42,11 @@ export declare class JsonBlobService extends HoistService {
42
42
  /** Retrieve a single JSONBlob by its unique token. */
43
43
  getAsync(token: string): Promise<JsonBlob>;
44
44
  /** Retrieve all blobs of a particular type that are visible to the current user. */
45
- listAsync({ type, includeValue, loadSpec }: {
45
+ listAsync(spec: {
46
46
  type: string;
47
47
  includeValue?: boolean;
48
48
  loadSpec?: LoadSpec;
49
- }): Promise<any>;
49
+ }): Promise<JsonBlob[]>;
50
50
  /** Persist a new JSONBlob back to the server. */
51
51
  createAsync({ acl, description, type, meta, name, value }: Partial<JsonBlob>): Promise<JsonBlob>;
52
52
  /** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
@@ -20,8 +20,10 @@ export interface View<T extends PlainObject = PlainObject> {
20
20
  isShared: boolean;
21
21
  lastUpdated: number;
22
22
  lastUpdatedBy: string;
23
- /** User-supplied descriptive name. */
23
+ /** User-supplied descriptive name, including folder designating prefix. */
24
24
  name: string;
25
+ /** User-supplied descriptive name, without folder designating prefix. */
26
+ shortName: string;
25
27
  /** Original creator of the view, and the only user with access to it if not shared. */
26
28
  owner: string;
27
29
  token: string;
@@ -14,11 +14,11 @@ import {
14
14
  } from '@xh/hoist/core';
15
15
  import {genDisplayName} from '@xh/hoist/data';
16
16
  import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
17
- import {wait} from '@xh/hoist/promise';
18
17
  import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
19
- import {isEmpty, isEqual, isNil, lowerCase, sortBy, startCase} from 'lodash';
18
+ import {isEqual, isNil, isUndefined, lowerCase, startCase} from 'lodash';
20
19
  import {runInAction} from 'mobx';
21
20
  import {SaveDialogModel} from './impl/SaveDialogModel';
21
+ import {buildViewTree} from './impl/BuildViewTree';
22
22
  import {View, ViewTree} from './Types';
23
23
 
24
24
  export interface ViewManagerConfig {
@@ -104,16 +104,19 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
104
104
  /** Current state of the active view, can include not-yet-persisted changes. */
105
105
  @observable.ref pendingValue: T = {} as T;
106
106
  /** Loaded saved view definitions - both private and shared. */
107
- @observable.ref views: View<T>[] = [];
108
- /** Token identifier for the currently selected view, or null if in default mode. */
109
- @bindable selectedToken: string = null;
107
+ @observable.ref views: View<T>[] = null;
108
+
109
+ /** Currently selected view, or null if in default mode. Token only will be set during pre-loading.*/
110
+ @observable selectedToken: string = null;
111
+ @observable.ref selectedView: View<T> = null;
112
+
110
113
  /** List of tokens for the user's favorite views. */
111
114
  @bindable favorites: string[] = [];
112
115
  /**
113
116
  * True if user has opted-in to automatically saving changes to personal views (if auto-save
114
117
  * generally available as per `enableAutoSave`).
115
118
  */
116
- @bindable autoSaveActive = false;
119
+ @bindable autoSave = false;
117
120
 
118
121
  /**
119
122
  * TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
@@ -137,51 +140,30 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
137
140
  return executeIfFunction(this._enableSharing);
138
141
  }
139
142
 
140
- get selectedView(): View<T> {
141
- return this.views.find(it => it.token === this.selectedToken);
142
- }
143
-
144
- @computed
145
- get isSharedViewSelected(): boolean {
146
- return !!this.selectedView?.isShared;
147
- }
148
-
149
143
  @computed
150
144
  get canSave(): boolean {
151
- const {selectedView} = this;
152
- return (
153
- selectedView &&
154
- this.isDirty &&
155
- (this.enableSharing || !selectedView.isShared) &&
156
- !this.loadModel.isPending
157
- );
145
+ const {loadModel, selectedView, enableSharing} = this;
146
+ return !loadModel.isPending && selectedView && (enableSharing || !selectedView.isShared);
158
147
  }
159
148
 
160
- /**
161
- * True if displaying the save button is appropriate from the model's point of view, even if
162
- * that button might be disabled due to no changes having been made. Works in concert with the
163
- * desktop ViewManager component's `showSaveButton` prop.
164
- */
165
149
  @computed
166
- get canShowSaveButton(): boolean {
167
- const {selectedView} = this;
150
+ get canAutoSave(): boolean {
151
+ const {enableAutoSave, autoSave, loadModel, selectedView} = this;
168
152
  return (
153
+ !loadModel.isPending &&
154
+ enableAutoSave &&
155
+ autoSave &&
169
156
  selectedView &&
170
- (!this.enableAutoSave || !this.autoSaveActive) &&
171
- (this.enableSharing || !selectedView.isShared)
157
+ !selectedView.isShared
172
158
  );
173
159
  }
174
160
 
175
161
  @computed
176
- get enableAutoSaveToggle(): boolean {
177
- return this.selectedView && !this.isSharedViewSelected;
178
- }
179
-
180
- @computed
181
- get disabledAutoSaveReason(): string {
182
- const {displayName} = this;
183
- if (!this.selectedView) return `Cannot auto-save default ${displayName}.`;
184
- if (this.isSharedViewSelected) return `Cannot auto-save shared ${displayName}.`;
162
+ get autoSaveUnavailableReason(): string {
163
+ const {canAutoSave, selectedView, displayName} = this;
164
+ if (canAutoSave) return null;
165
+ if (!selectedView) return `Cannot auto-save default ${displayName}.`;
166
+ if (selectedView.isShared) return `Cannot auto-save shared ${displayName}.`;
185
167
  return null;
186
168
  }
187
169
 
@@ -207,11 +189,11 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
207
189
  }
208
190
 
209
191
  get sharedViewTree(): ViewTree[] {
210
- return this.buildViewTree(sortBy(this.sharedViews, 'name'));
192
+ return buildViewTree(this.sharedViews, this);
211
193
  }
212
194
 
213
195
  get privateViewTree(): ViewTree[] {
214
- return this.buildViewTree(sortBy(this.privateViews, 'name'));
196
+ return buildViewTree(this.privateViews, this);
215
197
  }
216
198
 
217
199
  /**
@@ -257,12 +239,9 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
257
239
 
258
240
  this.addReaction(
259
241
  {
260
- track: () => this.pendingValue,
261
- run: () => this.maybeAutoSaveAsync({skipToast: true})
262
- },
263
- {
264
- track: () => this.autoSaveActive,
265
- run: () => this.maybeAutoSaveAsync({skipToast: false})
242
+ // Track pendingValue, so we retry on fail if view stays dirty -- could use backup timer
243
+ track: () => [this.pendingValue, this.autoSave],
244
+ run: () => this.maybeAutoSaveAsync()
266
245
  },
267
246
  {
268
247
  track: () => this.favorites,
@@ -274,53 +253,51 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
274
253
  override async doLoadAsync(loadSpec: LoadSpec) {
275
254
  const rawViews = await XH.jsonBlobService.listAsync({
276
255
  type: this.viewType,
277
- includeValue: true,
256
+ includeValue: false,
278
257
  loadSpec
279
258
  });
280
259
  if (loadSpec.isStale) return;
281
260
 
282
- runInAction(() => (this.views = this.processRaw(rawViews)));
261
+ runInAction(() => {
262
+ this.views = rawViews.map(it => this.processRaw(it));
263
+ });
283
264
 
284
265
  const token =
285
266
  loadSpec.meta.selectToken ??
286
- this.selectedView?.token ??
267
+ this.selectedToken ??
287
268
  (this.enableDefault ? null : this.views[0]?.token);
288
269
  await this.selectViewAsync(token);
289
270
  }
290
271
 
291
272
  async selectViewAsync(token: string) {
292
- // Introduce minimal wait and link to viewSelectionObserver to allow apps to mask.
293
- await wait(100)
294
- .then(() => {
295
- this.selectedToken = token;
296
-
297
- // Allow this model to restore its own persisted state in its ctor and note the desired
298
- // selected token before views have been loaded. Once views are loaded, this method will
299
- // be called again with the desired token and will proceed to set the value.
300
- if (isEmpty(this.views)) return;
301
-
302
- this.setValue(this.selectedView?.value ?? ({} as T));
303
- })
304
- .linkTo(this.viewSelectionObserver);
273
+ // If views have not been loaded yet (e.g. constructing), nothing to be done but pre-set state
274
+ if (!this.views) {
275
+ this.selectedToken = token;
276
+ return;
277
+ }
278
+ await this.selectViewInternalAsync(token).linkTo(this.viewSelectionObserver);
305
279
  }
306
280
 
307
- async saveAsync(skipToast: boolean = false) {
308
- const {canSave, selectedToken, pendingValue, isSharedViewSelected, DisplayName} = this;
309
- throwIf(!canSave, 'Unable to save view at this time.'); // sanity check - user should not reach
281
+ //------------------------
282
+ // Saving/resetting
283
+ //------------------------
284
+ async saveAsync() {
285
+ const {canSave, selectedToken, pendingValue, selectedView, DisplayName} = this;
286
+ throwIf(!canSave, 'Unable to save view.');
310
287
 
311
- if (isSharedViewSelected) {
288
+ if (selectedView?.isShared) {
312
289
  if (!(await this.confirmSaveForSharedViewAsync())) return;
313
290
  }
314
291
 
315
292
  try {
316
293
  await XH.jsonBlobService.updateAsync(selectedToken, {value: pendingValue});
294
+ runInAction(() => {
295
+ this.value = this.pendingValue;
296
+ });
297
+ XH.successToast(`${DisplayName} successfully saved.`);
317
298
  } catch (e) {
318
299
  XH.handleException(e, {alertType: 'toast'});
319
- skipToast = true; // don't show the success toast below, but still refresh.
320
300
  }
321
-
322
- await this.refreshAsync({selectToken: selectedToken});
323
- if (!skipToast) XH.successToast(`${DisplayName} successfully saved.`);
324
301
  }
325
302
 
326
303
  async saveAsAsync() {
@@ -364,10 +341,6 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
364
341
  this.manageDialogOpen = false;
365
342
  }
366
343
 
367
- getHierarchyDisplayName(name: string) {
368
- return name?.substring(name.lastIndexOf('\\') + 1);
369
- }
370
-
371
344
  //------------------
372
345
  // Favorites
373
346
  //------------------
@@ -391,38 +364,69 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
391
364
  // Persistable
392
365
  //------------------
393
366
  getPersistableState(): PersistableState<ViewManagerModelPersistState> {
394
- return new PersistableState({selectedToken: this.selectedToken, favorites: this.favorites});
367
+ const state: ViewManagerModelPersistState = {
368
+ selectedToken: this.selectedToken,
369
+ favorites: this.favorites
370
+ };
371
+ if (this.enableAutoSave) {
372
+ state.autoSave = this.autoSave;
373
+ }
374
+ return new PersistableState(state);
395
375
  }
396
376
 
397
377
  setPersistableState(state: PersistableState<ViewManagerModelPersistState>) {
398
- const {selectedToken, favorites} = state.value;
399
- if (selectedToken) this.selectViewAsync(selectedToken);
400
- if (favorites) this.favorites = favorites;
378
+ const {selectedToken, favorites, autoSave} = state.value;
379
+ if (!isUndefined(selectedToken)) {
380
+ this.selectViewAsync(selectedToken);
381
+ }
382
+ if (!isUndefined(favorites)) {
383
+ this.favorites = favorites;
384
+ }
385
+ if (!isUndefined(autoSave) && this.enableAutoSave) {
386
+ this.autoSave = autoSave;
387
+ }
401
388
  }
402
389
 
403
390
  //------------------
404
391
  // Implementation
405
392
  //------------------
406
- private processRaw(raw: PlainObject[]): View<T>[] {
407
- const name = pluralize(this.DisplayName);
408
- return raw.map(it => {
409
- const isShared = it.acl === '*';
410
- return {
411
- ...it,
412
- isShared,
413
- group: isShared ? `Shared ${name}` : `My ${name}`,
414
- isFavorite: this.isFavorite(it.token)
415
- } as View<T>;
393
+ private async selectViewInternalAsync(token: string) {
394
+ let view: View<T> = null;
395
+ if (token != null) {
396
+ try {
397
+ const raw = await XH.jsonBlobService.getAsync(token);
398
+ view = this.processRaw(raw);
399
+ } catch (e) {
400
+ XH.handleException(e, {showAlert: false});
401
+ view = null;
402
+ token = null;
403
+ }
404
+ }
405
+
406
+ runInAction(() => {
407
+ this.selectedToken = token;
408
+ this.selectedView = view;
409
+ this.setValue(this.selectedView?.value ?? ({} as T));
416
410
  });
417
411
  }
418
412
 
419
- @action
413
+ private processRaw(raw: PlainObject): View<T> {
414
+ const name = pluralize(this.DisplayName);
415
+ const isShared = raw.acl === '*';
416
+ return {
417
+ ...raw,
418
+ shortName: raw.name?.substring(raw.name.lastIndexOf('\\') + 1),
419
+ isShared,
420
+ group: isShared ? `Shared ${name}` : `My ${name}`,
421
+ isFavorite: this.isFavorite(raw.token)
422
+ } as View<T>;
423
+ }
424
+
420
425
  private setValue(value: T) {
421
426
  value = this.cleanValue(value);
422
427
  if (isEqual(value, this.value) && isEqual(value, this.pendingValue)) return;
423
428
 
424
- this.value = value;
425
- this.pendingValue = value;
429
+ this.value = this.pendingValue = value;
426
430
  this.providers.forEach(it => it.pushStateToTarget());
427
431
  }
428
432
 
@@ -447,69 +451,18 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
447
451
  });
448
452
  }
449
453
 
450
- private async maybeAutoSaveAsync({skipToast}: {skipToast: boolean}) {
451
- if (
452
- this.enableAutoSave &&
453
- this.autoSaveActive &&
454
- this.canSave &&
455
- !this.isSharedViewSelected
456
- ) {
457
- await this.saveAsync(skipToast);
458
- }
459
- }
460
-
461
- private buildViewTree(views: View<T>[], depth: number = 0): ViewTree[] {
462
- const groups = {},
463
- unbalancedStableGroupsAndViews = [];
464
-
465
- views.forEach(view => {
466
- // Leaf Node
467
- if (this.getNameHierarchySubstring(view.name, depth + 1) == null) {
468
- unbalancedStableGroupsAndViews.push(view);
469
- return;
454
+ private async maybeAutoSaveAsync() {
455
+ if (this.canAutoSave && this.isDirty) {
456
+ const {selectedToken, pendingValue} = this;
457
+ try {
458
+ await XH.jsonBlobService.updateAsync(selectedToken, {value: pendingValue});
459
+ runInAction(() => {
460
+ this.value = this.pendingValue;
461
+ });
462
+ } catch (e) {
463
+ XH.handleException(e, {showAlert: false});
470
464
  }
471
- // Belongs to an already defined group
472
- const group = this.getNameHierarchySubstring(view.name, depth);
473
- if (groups[group]) {
474
- groups[group].children.push(view);
475
- return;
476
- }
477
- // Belongs to a not defined group, create it
478
- groups[group] = {name: group, children: [view], isMenuFolder: true};
479
- unbalancedStableGroupsAndViews.push(groups[group]);
480
- });
481
-
482
- return unbalancedStableGroupsAndViews.map(it => {
483
- const {name, isMenuFolder, children, description, token} = it;
484
- if (isMenuFolder) {
485
- return {
486
- type: 'folder',
487
- text: name,
488
- items: this.buildViewTree(children, depth + 1),
489
- selected: this.isFolderForEntry(name, this.selectedView?.name, depth)
490
- };
491
- }
492
- return {
493
- type: 'view',
494
- text: this.getHierarchyDisplayName(name),
495
- selected: this.selectedToken === token,
496
- token,
497
- description
498
- };
499
- });
500
- }
501
-
502
- private getNameHierarchySubstring(name: string, depth: number) {
503
- const arr = name?.split('\\') ?? [];
504
- if (arr.length <= depth) {
505
- return null;
506
465
  }
507
- return arr.slice(0, depth + 1).join('\\');
508
- }
509
-
510
- private isFolderForEntry(folderName: string, entryName: string, depth: number) {
511
- const name = this.getNameHierarchySubstring(entryName, depth);
512
- return name && name === folderName && folderName.length < entryName.length;
513
466
  }
514
467
 
515
468
  // Update flag on each view, replacing entire views collection for observability.
@@ -522,6 +475,7 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
522
475
  }
523
476
 
524
477
  interface ViewManagerModelPersistState {
525
- selectedToken: string;
526
- favorites: string[];
478
+ selectedToken?: string;
479
+ favorites?: string[];
480
+ autoSave?: boolean;
527
481
  }
@@ -0,0 +1,68 @@
1
+ import {View, ViewManagerModel, ViewTree} from '@xh/hoist/core/persist/viewmanager';
2
+ import {sortBy} from 'lodash';
3
+
4
+ /**
5
+ * Create a menu-friendly, tree representation of a set of views, using the `\`
6
+ * in view names to create folders.
7
+ *
8
+ * @internal
9
+ */
10
+ export function buildViewTree(views: View[], model: ViewManagerModel): ViewTree[] {
11
+ views = sortBy(views, 'name');
12
+ return buildTreeInternal(views, model.selectedView, 0);
13
+ }
14
+
15
+ function buildTreeInternal(views: View[], selected: View, depth: number): ViewTree[] {
16
+ // 1) Get groups and leaves at this level.
17
+ const groups = {},
18
+ groupsAndLeaves = [];
19
+ views.forEach(view => {
20
+ // Leaf Node
21
+ if (getNameAtDepth(view.name, depth + 1) == null) {
22
+ groupsAndLeaves.push(view);
23
+ return;
24
+ }
25
+ // Belongs to an already defined group
26
+ const group = getNameAtDepth(view.name, depth);
27
+ if (groups[group]) {
28
+ groups[group].children.push(view);
29
+ return;
30
+ }
31
+ // Belongs to a not defined group, create it
32
+ groups[group] = {name: group, children: [view], isMenuFolder: true};
33
+ groupsAndLeaves.push(groups[group]);
34
+ });
35
+
36
+ // 2) Make ViewTree, recursing for groups
37
+ return groupsAndLeaves.map(it => {
38
+ const {name, isMenuFolder, children, description, token} = it;
39
+ return isMenuFolder
40
+ ? {
41
+ type: 'folder',
42
+ text: getFolderDisplayName(name, depth),
43
+ items: buildTreeInternal(children, selected, depth + 1),
44
+ selected: isFolderForEntry(name, selected?.name, depth)
45
+ }
46
+ : {
47
+ type: 'view',
48
+ text: it.shortName,
49
+ selected: selected?.token === token,
50
+ token,
51
+ description
52
+ };
53
+ });
54
+ }
55
+
56
+ function getNameAtDepth(name: string, depth: number) {
57
+ const arr = name?.split('\\') ?? [];
58
+ return arr.length <= depth ? null : arr.slice(0, depth + 1).join('\\');
59
+ }
60
+
61
+ function isFolderForEntry(folderName: string, entryName: string, depth: number) {
62
+ const name = getNameAtDepth(entryName, depth);
63
+ return name && name === folderName && folderName.length < entryName.length;
64
+ }
65
+
66
+ function getFolderDisplayName(name: string, depth: number) {
67
+ return name?.split('\\')[depth];
68
+ }
@@ -214,6 +214,8 @@ export class ManageDialogModel extends HoistModel {
214
214
  hideHeaders: true,
215
215
  showGroupRowCounts: false,
216
216
  selModel: 'multiple',
217
+ contextMenu: null,
218
+ sizingMode: 'standard',
217
219
  store: {
218
220
  idSpec: 'token',
219
221
  fields: [
@@ -40,7 +40,7 @@ export class SaveDialogModel extends HoistModel {
40
40
  this.invalidNames = invalidNames;
41
41
 
42
42
  this.formModel.init({
43
- name: viewStub.name ? `${viewStub.name} (COPY)` : '',
43
+ name: viewStub.name ?? '',
44
44
  description: viewStub.description
45
45
  });
46
46