@xh/hoist 71.0.0-SNAPSHOT.1734118787755 → 71.0.0-SNAPSHOT.1734551243081

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
@@ -9,6 +9,7 @@
9
9
  * Handle delete and update collisions more gracefully.
10
10
  * Support for `settleTime`,
11
11
  * Improved management UI Dialog.
12
+ * Support for "global" views.
12
13
  * New `SessionStorageService` and associated persistence provider provides support for saving
13
14
  tab local data across reloads.
14
15
 
@@ -14,6 +14,11 @@ export interface ViewManagerConfig {
14
14
  * in advance, so that there is a clear initial selection for users without any private views.
15
15
  */
16
16
  enableDefault?: boolean;
17
+ /**
18
+ * True (default) to enable "global" views - i.e. views that are not owned by a user and are
19
+ * available to all.
20
+ */
21
+ enableGlobal?: boolean;
17
22
  /**
18
23
  * True (default) to allow users to share their views with other users.
19
24
  */
@@ -101,6 +106,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
101
106
  readonly globalDisplayName: string;
102
107
  readonly enableAutoSave: boolean;
103
108
  readonly enableDefault: boolean;
109
+ readonly enableGlobal: boolean;
104
110
  readonly enableSharing: boolean;
105
111
  readonly manageGlobal: boolean;
106
112
  readonly settleTime: number;
@@ -173,6 +179,7 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
173
179
  userUnpin(view: ViewInfo): void;
174
180
  isUserPinned(view: ViewInfo): boolean | null;
175
181
  validateViewNameAsync(name: string, existing?: ViewInfo): Promise<string>;
182
+ deleteViewsAsync(toDelete: ViewInfo[]): Promise<void>;
176
183
  private initAsync;
177
184
  private loadViewAsync;
178
185
  private maybeAutoSaveAsync;
@@ -35,8 +35,7 @@ export declare class ViewToBlobApi<T> {
35
35
  makeViewGlobalAsync(view: ViewInfo): Promise<View<T>>;
36
36
  /** Update a view's value. */
37
37
  updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>>;
38
- /** Delete a view. */
39
- deleteViewAsync(view: ViewInfo): Promise<void>;
38
+ deleteViewsAsync(views: ViewInfo[]): Promise<void>;
40
39
  private trackChange;
41
40
  private ensureEditable;
42
41
  }
@@ -6,5 +6,6 @@ export declare class ViewManagerLocalModel extends HoistModel {
6
6
  readonly parent: ViewManagerModel;
7
7
  readonly manageDialogModel: ManageDialogModel;
8
8
  readonly saveAsDialogModel: SaveAsDialogModel;
9
+ isVisible: boolean;
9
10
  constructor(parent: ViewManagerModel);
10
11
  }
@@ -1,6 +1,7 @@
1
1
  import { GridModel } from '@xh/hoist/cmp/grid';
2
+ import { ManageDialogModel } from './ManageDialogModel';
2
3
  /**
3
4
  * Default management dialog for ViewManager.
4
5
  */
5
- export declare const manageDialog: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<any>>;
6
+ export declare const manageDialog: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ManageDialogModel>>;
6
7
  export declare const viewsGrid: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<GridModel>>;
@@ -21,10 +21,6 @@ export declare class ManageDialogModel extends HoistModel {
21
21
  get gridModel(): GridModel;
22
22
  get selectedView(): ViewInfo;
23
23
  get selectedViews(): ViewInfo[];
24
- get manageGlobal(): boolean;
25
- get typeDisplayName(): string;
26
- get globalDisplayName(): string;
27
- get enableSharing(): boolean;
28
24
  constructor(viewManagerModel: ViewManagerModel);
29
25
  open(): void;
30
26
  close(): void;
@@ -1 +1,2 @@
1
- export declare const viewMultiPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<any>>;
1
+ import { ManageDialogModel } from './ManageDialogModel';
2
+ export declare const viewMultiPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ManageDialogModel>>;
@@ -21,10 +21,11 @@ import {
21
21
  import type {ViewManagerProvider} from '@xh/hoist/core';
22
22
  import {genDisplayName} from '@xh/hoist/data';
23
23
  import {fmtDateTime} from '@xh/hoist/format';
24
- import {action, bindable, makeObservable, observable, runInAction, when} from '@xh/hoist/mobx';
24
+ import {action, bindable, makeObservable, observable, when} from '@xh/hoist/mobx';
25
25
  import {olderThan, SECONDS} from '@xh/hoist/utils/datetime';
26
26
  import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
27
27
  import {find, isEmpty, isEqual, isNil, isObject, lowerCase, pickBy} from 'lodash';
28
+ import {runInAction} from 'mobx';
28
29
  import {ReactNode} from 'react';
29
30
  import {ViewInfo} from './ViewInfo';
30
31
  import {View} from './View';
@@ -43,6 +44,12 @@ export interface ViewManagerConfig {
43
44
  */
44
45
  enableDefault?: boolean;
45
46
 
47
+ /**
48
+ * True (default) to enable "global" views - i.e. views that are not owned by a user and are
49
+ * available to all.
50
+ */
51
+ enableGlobal?: boolean;
52
+
46
53
  /**
47
54
  * True (default) to allow users to share their views with other users.
48
55
  */
@@ -145,6 +152,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
145
152
  readonly globalDisplayName: string;
146
153
  readonly enableAutoSave: boolean;
147
154
  readonly enableDefault: boolean;
155
+ readonly enableGlobal: boolean;
148
156
  readonly enableSharing: boolean;
149
157
  readonly manageGlobal: boolean;
150
158
  readonly settleTime: number;
@@ -216,13 +224,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
216
224
 
217
225
  get isViewAutoSavable(): boolean {
218
226
  const {enableAutoSave, autoSave, view} = this;
219
- return (
220
- enableAutoSave &&
221
- autoSave &&
222
- !view.isShared &&
223
- !view.isDefault &&
224
- !XH.identityService.isImpersonating
225
- );
227
+ return enableAutoSave && autoSave && view.isOwned && !XH.identityService.isImpersonating;
226
228
  }
227
229
 
228
230
  get autoSaveUnavailableReason(): string {
@@ -272,6 +274,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
272
274
  manageGlobal = false,
273
275
  enableAutoSave = true,
274
276
  enableDefault = true,
277
+ enableGlobal = true,
275
278
  enableSharing = true,
276
279
  settleTime = 1000,
277
280
  initialViewSpec = null
@@ -290,6 +293,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
290
293
  this.persistWith = persistWith;
291
294
  this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
292
295
  this.enableDefault = enableDefault;
296
+ this.enableGlobal = enableGlobal;
293
297
  this.enableSharing = enableSharing;
294
298
  this.enableAutoSave = enableAutoSave;
295
299
  this.settleTime = settleTime;
@@ -438,6 +442,24 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
438
442
  return null;
439
443
  }
440
444
 
445
+ async deleteViewsAsync(toDelete: ViewInfo[]): Promise<void> {
446
+ let exception;
447
+ try {
448
+ await this.api.deleteViewsAsync(toDelete);
449
+ } catch (e) {
450
+ exception = e;
451
+ }
452
+
453
+ await this.refreshAsync();
454
+ const {views} = this;
455
+
456
+ if (toDelete.some(view => view.isCurrentView) && !views.some(view => view.isCurrentView)) {
457
+ await this.loadViewAsync(this.initialViewSpec?.(views));
458
+ }
459
+
460
+ if (exception) throw exception;
461
+ }
462
+
441
463
  //------------------
442
464
  // Implementation
443
465
  //------------------
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {PlainObject, XH} from '@xh/hoist/core';
9
9
  import {pluralize, throwIf} from '@xh/hoist/utils/js';
10
- import {omit, pick} from 'lodash';
10
+ import {isEmpty, omit, pick} from 'lodash';
11
11
  import {ViewInfo} from './ViewInfo';
12
12
  import {View} from './View';
13
13
  import {ViewManagerModel} from './ViewManagerModel';
@@ -50,7 +50,13 @@ export class ViewToBlobApi<T> {
50
50
  type: model.type,
51
51
  includeValue: false
52
52
  });
53
- return blobs.map(b => new ViewInfo(b, model));
53
+ return blobs
54
+ .map(b => new ViewInfo(b, model))
55
+ .filter(
56
+ view =>
57
+ (model.enableGlobal || !view.isGlobal) &&
58
+ (model.enableSharing || !view.isShared)
59
+ );
54
60
  } catch (e) {
55
61
  throw XH.exception({
56
62
  message: `Unable to fetch ${pluralize(model.typeDisplayName)}`,
@@ -151,21 +157,31 @@ export class ViewToBlobApi<T> {
151
157
  }
152
158
  }
153
159
 
154
- /** Delete a view. */
155
- async deleteViewAsync(view: ViewInfo) {
156
- try {
157
- this.ensureEditable(view);
158
- await XH.jsonBlobService.archiveAsync(view.token);
159
- this.trackChange('Deleted View', view);
160
- } catch (e) {
161
- throw XH.exception({message: `Unable to delete ${view.typedName}`, cause: e});
160
+ async deleteViewsAsync(views: ViewInfo[]) {
161
+ views.forEach(v => this.ensureEditable(v));
162
+ const results = await Promise.allSettled(
163
+ views.map(v => XH.jsonBlobService.archiveAsync(v.token))
164
+ ),
165
+ outcome = results.map((result, idx) => ({result, view: views[idx]})),
166
+ failed = outcome.filter(({result}) => result.status === 'rejected') as Array<{
167
+ result: PromiseRejectedResult;
168
+ view: ViewInfo;
169
+ }>;
170
+
171
+ this.trackChange(`Deleted ${pluralize('View', views.length - failed.length, true)}`);
172
+
173
+ if (!isEmpty(failed)) {
174
+ throw XH.exception({
175
+ message: `Failed to delete ${pluralize(this.model.typeDisplayName, failed.length, true)}: ${failed.map(({view}) => view.name).join(', ')}`,
176
+ cause: failed.map(({result}) => result.reason)
177
+ });
162
178
  }
163
179
  }
164
180
 
165
181
  //------------------
166
182
  // Implementation
167
183
  //------------------
168
- private trackChange(message: string, v: View | ViewInfo) {
184
+ private trackChange(message: string, v?: View | ViewInfo) {
169
185
  XH.track({
170
186
  message,
171
187
  category: 'Views',
@@ -12,6 +12,7 @@ import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
12
12
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {popover} from '@xh/hoist/kit/blueprint';
15
+ import {useOnVisibleChange} from '@xh/hoist/utils/react';
15
16
  import {startCase} from 'lodash';
16
17
  import {viewMenu} from './ViewMenu';
17
18
  import {ViewManagerLocalModel} from './ViewManagerLocalModel';
@@ -67,6 +68,7 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
67
68
  save = saveButton({model: locModel, mode: showSaveButton, ...saveButtonProps}),
68
69
  revert = revertButton({model: locModel, mode: showRevertButton, ...revertButtonProps}),
69
70
  menu = popover({
71
+ disabled: !locModel.isVisible, // Prevent orphaned popover menu
70
72
  item: menuButton({model: locModel, ...menuButtonProps}),
71
73
  content: viewMenu({model: locModel}),
72
74
  placement: 'bottom-start',
@@ -75,7 +77,8 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
75
77
  return fragment(
76
78
  hbox({
77
79
  className,
78
- items: buttonSide == 'left' ? [revert, save, menu] : [menu, save, revert]
80
+ items: buttonSide == 'left' ? [revert, save, menu] : [menu, save, revert],
81
+ ref: useOnVisibleChange(isVisible => (locModel.isVisible = isVisible))
79
82
  }),
80
83
  manageDialog({model: locModel.manageDialogModel}),
81
84
  saveAsDialog({model: locModel.saveAsDialogModel})
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import {HoistModel, managed} from '@xh/hoist/core';
9
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
9
10
  import {ManageDialogModel} from './dialog/ManageDialogModel';
10
11
  import {SaveAsDialogModel} from './dialog/SaveAsDialogModel';
11
12
  import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
@@ -19,8 +20,12 @@ export class ViewManagerLocalModel extends HoistModel {
19
20
  @managed
20
21
  readonly saveAsDialogModel: SaveAsDialogModel;
21
22
 
23
+ @bindable
24
+ isVisible = true;
25
+
22
26
  constructor(parent: ViewManagerModel) {
23
27
  super();
28
+ makeObservable(this);
24
29
  this.parent = parent;
25
30
  this.manageDialogModel = new ManageDialogModel(parent);
26
31
  this.saveAsDialogModel = new SaveAsDialogModel(parent);
@@ -24,7 +24,7 @@ import {viewPanel} from './ViewPanel';
24
24
  /**
25
25
  * Default management dialog for ViewManager.
26
26
  */
27
- export const manageDialog = hoistCmp.factory({
27
+ export const manageDialog = hoistCmp.factory<ManageDialogModel>({
28
28
  displayName: 'ManageDialog',
29
29
  className: 'xh-view-manager__manage-dialog',
30
30
  model: uses(() => ManageDialogModel),
@@ -32,11 +32,11 @@ export const manageDialog = hoistCmp.factory({
32
32
  render({model, className}) {
33
33
  if (!model.isOpen) return null;
34
34
 
35
- const {typeDisplayName, updateTask, loadTask, selectedViews} = model,
35
+ const {updateTask, loadTask, selectedViews, viewManagerModel} = model,
36
36
  count = selectedViews.length;
37
37
 
38
38
  return dialog({
39
- title: `Manage ${capitalize(pluralize(typeDisplayName))}`,
39
+ title: `Manage ${capitalize(pluralize(viewManagerModel.typeDisplayName))}`,
40
40
  icon: Icon.gear(),
41
41
  className,
42
42
  isOpen: true,
@@ -103,7 +103,7 @@ export const viewsGrid = hoistCmp.factory<GridModel>({
103
103
 
104
104
  const placeholderPanel = hoistCmp.factory<ManageDialogModel>({
105
105
  render({model}) {
106
- return placeholder(Icon.gears(), `Select a ${model.typeDisplayName}`);
106
+ return placeholder(Icon.gears(), `Select a ${model.viewManagerModel.typeDisplayName}`);
107
107
  }
108
108
  });
109
109
 
@@ -17,7 +17,7 @@ import {viewsGrid} from '@xh/hoist/desktop/cmp/viewmanager/dialog/ManageDialog';
17
17
  import {Icon} from '@xh/hoist/icon';
18
18
  import {action, bindable, computed, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
19
19
  import {pluralize} from '@xh/hoist/utils/js';
20
- import {capitalize, isEmpty, some, startCase} from 'lodash';
20
+ import {capitalize, compact, isEmpty, some, startCase} from 'lodash';
21
21
  import {ReactNode} from 'react';
22
22
  import {ViewPanelModel} from './ViewPanelModel';
23
23
 
@@ -67,22 +67,6 @@ export class ManageDialogModel extends HoistModel {
67
67
  return this.gridModel.selectedRecords.map(it => it.data.view) as ViewInfo[];
68
68
  }
69
69
 
70
- get manageGlobal(): boolean {
71
- return this.viewManagerModel.manageGlobal;
72
- }
73
-
74
- get typeDisplayName(): string {
75
- return this.viewManagerModel.typeDisplayName;
76
- }
77
-
78
- get globalDisplayName(): string {
79
- return this.viewManagerModel.globalDisplayName;
80
- }
81
-
82
- get enableSharing(): boolean {
83
- return this.viewManagerModel.enableSharing;
84
- }
85
-
86
70
  constructor(viewManagerModel: ViewManagerModel) {
87
71
  super();
88
72
  makeObservable(this);
@@ -108,15 +92,22 @@ export class ManageDialogModel extends HoistModel {
108
92
 
109
93
  override async doLoadAsync(loadSpec: LoadSpec) {
110
94
  const {tabContainerModel} = this,
111
- {view, ownedViews, globalViews, sharedViews} = this.viewManagerModel;
95
+ {enableGlobal, enableSharing, view, ownedViews, globalViews, sharedViews} =
96
+ this.viewManagerModel;
112
97
 
113
98
  runInAction(() => {
114
99
  this.ownedGridModel.loadData(ownedViews);
115
- this.globalGridModel.loadData(globalViews);
116
- this.sharedGridModel.loadData(sharedViews);
117
100
  tabContainerModel.setTabTitle('owned', this.ownedTabTitle);
118
- tabContainerModel.setTabTitle('global', this.globalTabTitle);
119
- tabContainerModel.setTabTitle('shared', this.sharedTabTitle);
101
+
102
+ if (enableGlobal) {
103
+ this.globalGridModel.loadData(globalViews);
104
+ tabContainerModel.setTabTitle('global', this.globalTabTitle);
105
+ }
106
+
107
+ if (enableSharing) {
108
+ this.sharedGridModel.loadData(sharedViews);
109
+ tabContainerModel.setTabTitle('shared', this.sharedTabTitle);
110
+ }
120
111
  });
121
112
  if (!loadSpec.isRefresh && !view.isDefault) {
122
113
  await this.selectViewAsync(view.info);
@@ -145,27 +136,39 @@ export class ManageDialogModel extends HoistModel {
145
136
  // Implementation
146
137
  //------------------------
147
138
  private init() {
139
+ const {enableGlobal, enableSharing} = this.viewManagerModel;
140
+
148
141
  this.ownedGridModel = this.createGridModel('owned');
149
- this.globalGridModel = this.createGridModel('global');
150
- this.sharedGridModel = this.createGridModel('shared');
142
+ if (enableGlobal) this.globalGridModel = this.createGridModel('global');
143
+ if (enableSharing) this.sharedGridModel = this.createGridModel('shared');
144
+ const gridModels = compact([
145
+ this.ownedGridModel,
146
+ this.globalGridModel,
147
+ this.sharedGridModel
148
+ ]);
149
+
151
150
  this.tabContainerModel = this.createTabContainerModel();
152
151
  this.viewPanelModel = new ViewPanelModel(this);
153
- const gridModels = [this.ownedGridModel, this.globalGridModel, this.sharedGridModel];
152
+
154
153
  this.addReaction({
155
154
  track: () => this.filter,
156
155
  run: f => gridModels.forEach(m => m.store.setFilter(f)),
157
156
  fireImmediately: true
158
157
  });
159
- gridModels.forEach(gm => {
160
- this.addReaction({
161
- track: () => gm.hasSelection,
162
- run: hasSelection => {
163
- gridModels.forEach(it => {
164
- if (it != gm && hasSelection) it.clearSelection();
165
- });
166
- }
158
+
159
+ // Only allow one selection at a time across all grids
160
+ if (gridModels.length > 1) {
161
+ gridModels.forEach(gm => {
162
+ this.addReaction({
163
+ track: () => gm.hasSelection,
164
+ run: hasSelection => {
165
+ gridModels.forEach(it => {
166
+ if (it != gm && hasSelection) it.clearSelection();
167
+ });
168
+ }
169
+ });
167
170
  });
168
- });
171
+ }
169
172
  }
170
173
 
171
174
  private async doUpdateAsync(view: ViewInfo, update: ViewUpdateSpec) {
@@ -176,7 +179,8 @@ export class ManageDialogModel extends HoistModel {
176
179
  }
177
180
 
178
181
  private async doDeleteAsync(views: ViewInfo[]) {
179
- const {viewManagerModel, typeDisplayName} = this,
182
+ const {viewManagerModel} = this,
183
+ {typeDisplayName} = viewManagerModel,
180
184
  count = views.length;
181
185
 
182
186
  if (!count) return;
@@ -208,12 +212,7 @@ export class ManageDialogModel extends HoistModel {
208
212
  });
209
213
  if (!confirmed) return;
210
214
 
211
- for (const view of views) {
212
- await viewManagerModel.api.deleteViewAsync(view);
213
- }
214
-
215
- await viewManagerModel.refreshAsync();
216
- await this.refreshAsync();
215
+ return viewManagerModel.deleteViewsAsync(views).finally(() => this.refreshAsync());
217
216
  }
218
217
 
219
218
  private async doMakeGlobalAsync(view: ViewInfo) {
@@ -250,7 +249,7 @@ export class ManageDialogModel extends HoistModel {
250
249
  }
251
250
 
252
251
  private createGridModel(type: 'owned' | 'global' | 'shared'): GridModel {
253
- const {typeDisplayName, globalDisplayName} = this;
252
+ const {typeDisplayName, globalDisplayName} = this.viewManagerModel;
254
253
 
255
254
  const modifier =
256
255
  type == 'owned' ? `personal` : type == 'global' ? globalDisplayName : 'shared';
@@ -320,13 +319,12 @@ export class ManageDialogModel extends HoistModel {
320
319
  }
321
320
 
322
321
  private createTabContainerModel(): TabContainerModel {
323
- const view = this.typeDisplayName,
322
+ const {enableGlobal, enableSharing, globalDisplayName, typeDisplayName} =
323
+ this.viewManagerModel,
324
+ view = typeDisplayName,
324
325
  views = pluralize(view),
325
- globalViews = `${this.globalDisplayName} ${views}`,
326
- {enableSharing} = this.viewManagerModel;
327
-
328
- return new TabContainerModel({
329
- tabs: [
326
+ globalViews = `${globalDisplayName} ${views}`,
327
+ tabs = [
330
328
  {
331
329
  id: 'owned',
332
330
  title: this.ownedTabTitle,
@@ -340,50 +338,58 @@ export class ManageDialogModel extends HoistModel {
340
338
  : ''
341
339
  )
342
340
  })
343
- },
344
- {
345
- id: 'global',
346
- title: this.globalTabTitle,
347
- content: viewsGrid({
348
- model: this.globalGridModel,
349
- helpText: fragment(
350
- Icon.globe(),
351
- `This tab shows ${globalViews} available to everyone. ${capitalize(globalViews)} can be pinned by default so they appear automatically in everyone's menu, but you can choose which ${views} you would like to see by pinning/unpinning them at any time.`
352
- )
353
- })
354
- },
355
- {
356
- id: 'shared',
357
- title: this.sharedTabTitle,
358
- content: viewsGrid({
359
- model: this.sharedGridModel,
360
- helpText: fragment(
361
- Icon.users(),
362
- `This tab shows ${views} shared by other ${XH.appName} users. You can pin these ${views} to add them to your menu and access them directly. Only the owner will be able to save changes to a shared ${view}, but you can save as a copy to make it your own.`
363
- )
364
- })
365
341
  }
366
- ]
367
- });
342
+ ];
343
+
344
+ if (enableGlobal) {
345
+ tabs.push({
346
+ id: 'global',
347
+ title: this.globalTabTitle,
348
+ content: viewsGrid({
349
+ model: this.globalGridModel,
350
+ helpText: fragment(
351
+ Icon.globe(),
352
+ `This tab shows ${globalViews} available to everyone. ${capitalize(globalViews)} can be pinned by default so they appear automatically in everyone's menu, but you can choose which ${views} you would like to see by pinning/unpinning them at any time.`
353
+ )
354
+ })
355
+ });
356
+ }
357
+
358
+ if (enableSharing) {
359
+ tabs.push({
360
+ id: 'shared',
361
+ title: this.sharedTabTitle,
362
+ content: viewsGrid({
363
+ model: this.sharedGridModel,
364
+ helpText: fragment(
365
+ Icon.users(),
366
+ `This tab shows ${views} shared by other ${XH.appName} users. You can pin these ${views} to add them to your menu and access them directly. Only the owner will be able to save changes to a shared ${view}, but you can save as a copy to make it your own.`
367
+ )
368
+ })
369
+ });
370
+ }
371
+
372
+ return new TabContainerModel({tabs});
368
373
  }
369
374
 
370
375
  private get ownedTabTitle(): ReactNode {
371
376
  return hbox(
372
- `My ${startCase(pluralize(this.typeDisplayName))}`,
377
+ `My ${startCase(pluralize(this.viewManagerModel.typeDisplayName))}`,
373
378
  badge(this.ownedGridModel.store.allCount)
374
379
  );
375
380
  }
376
381
 
377
382
  private get globalTabTitle(): ReactNode {
383
+ const {globalDisplayName, typeDisplayName} = this.viewManagerModel;
378
384
  return hbox(
379
- `${startCase(this.globalDisplayName)} ${startCase(pluralize(this.typeDisplayName))}`,
385
+ `${startCase(globalDisplayName)} ${startCase(pluralize(typeDisplayName))}`,
380
386
  badge(this.globalGridModel.store.allCount)
381
387
  );
382
388
  }
383
389
 
384
390
  private get sharedTabTitle(): ReactNode {
385
391
  return hbox(
386
- `Shared ${startCase(pluralize(this.typeDisplayName))}`,
392
+ `Shared ${startCase(pluralize(this.viewManagerModel.typeDisplayName))}`,
387
393
  badge(this.sharedGridModel.store.allCount)
388
394
  );
389
395
  }
@@ -14,7 +14,7 @@ import {pluralize} from '@xh/hoist/utils/js';
14
14
  import {every, isEmpty, some} from 'lodash';
15
15
  import {ManageDialogModel} from './ManageDialogModel';
16
16
 
17
- export const viewMultiPanel = hoistCmp.factory({
17
+ export const viewMultiPanel = hoistCmp.factory<ManageDialogModel>({
18
18
  model: uses(() => ManageDialogModel),
19
19
  render({model}) {
20
20
  const views = model.selectedViews;
@@ -25,7 +25,7 @@ export const viewMultiPanel = hoistCmp.factory({
25
25
  className: 'xh-view-manager__manage-dialog__form',
26
26
  item: placeholder(
27
27
  Icon.gears(),
28
- `${views.length} selected ${pluralize(model.typeDisplayName)}`,
28
+ `${views.length} selected ${pluralize(model.viewManagerModel.typeDisplayName)}`,
29
29
  vspacer(),
30
30
  buttons()
31
31
  )