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

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.
Files changed (41) hide show
  1. package/build/types/cmp/viewmanager/View.d.ts +5 -0
  2. package/build/types/cmp/viewmanager/ViewInfo.d.ts +32 -7
  3. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +34 -31
  4. package/build/types/cmp/viewmanager/ViewToBlobApi.d.ts +28 -6
  5. package/build/types/cmp/viewmanager/index.d.ts +1 -1
  6. package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +0 -4
  7. package/build/types/desktop/cmp/viewmanager/ViewManagerLocalModel.d.ts +10 -0
  8. package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +2 -2
  9. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +4 -3
  10. package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +19 -10
  11. package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +1 -1
  12. package/build/types/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.d.ts +3 -9
  13. package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +3 -0
  14. package/build/types/desktop/cmp/viewmanager/dialog/ViewMultiPanel.d.ts +1 -0
  15. package/build/types/desktop/cmp/viewmanager/dialog/ViewPanel.d.ts +5 -0
  16. package/build/types/desktop/cmp/viewmanager/dialog/{EditFormModel.d.ts → ViewPanelModel.d.ts} +2 -4
  17. package/build/types/svc/JsonBlobService.d.ts +1 -1
  18. package/cmp/viewmanager/View.ts +21 -1
  19. package/cmp/viewmanager/ViewInfo.ts +58 -11
  20. package/cmp/viewmanager/ViewManagerModel.ts +86 -81
  21. package/cmp/viewmanager/ViewToBlobApi.ts +91 -35
  22. package/cmp/viewmanager/index.ts +1 -1
  23. package/desktop/cmp/viewmanager/ViewManager.scss +25 -28
  24. package/desktop/cmp/viewmanager/ViewManager.ts +28 -26
  25. package/desktop/cmp/viewmanager/ViewManagerLocalModel.ts +28 -0
  26. package/desktop/cmp/viewmanager/ViewMenu.ts +162 -169
  27. package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +67 -40
  28. package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +238 -127
  29. package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +30 -9
  30. package/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.ts +35 -40
  31. package/desktop/cmp/viewmanager/dialog/Utils.ts +18 -0
  32. package/desktop/cmp/viewmanager/dialog/ViewMultiPanel.ts +70 -0
  33. package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +161 -0
  34. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +116 -0
  35. package/package.json +1 -1
  36. package/svc/JsonBlobService.ts +3 -3
  37. package/svc/storage/BaseStorageService.ts +1 -1
  38. package/tsconfig.tsbuildinfo +1 -1
  39. package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +0 -5
  40. package/desktop/cmp/viewmanager/dialog/EditForm.ts +0 -126
  41. package/desktop/cmp/viewmanager/dialog/EditFormModel.ts +0 -125
@@ -5,33 +5,39 @@
5
5
  * Copyright © 2024 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {grid, GridAutosizeMode, GridModel} from '@xh/hoist/cmp/grid';
9
- import {fragment, p, strong} from '@xh/hoist/cmp/layout';
8
+ import {badge} from '@xh/hoist/cmp/badge';
9
+ import {dateTimeCol, GridAutosizeMode, GridModel} from '@xh/hoist/cmp/grid';
10
+ import {fragment, hbox, p, strong} from '@xh/hoist/cmp/layout';
10
11
  import {TabContainerModel} from '@xh/hoist/cmp/tab';
11
- import {HoistModel, LoadSpec, lookup, managed, TaskObserver, XH} from '@xh/hoist/core';
12
+ import {ViewInfo, ViewManagerModel, ViewUpdateSpec} from '@xh/hoist/cmp/viewmanager';
13
+ import {HoistModel, LoadSpec, managed, TaskObserver, XH} from '@xh/hoist/core';
12
14
  import {FilterTestFn} from '@xh/hoist/data';
13
- import {computed} from 'mobx';
14
- import {ReactNode} from 'react';
15
- import {EditFormModel} from './EditFormModel';
15
+ import {button} from '@xh/hoist/desktop/cmp/button';
16
+ import {viewsGrid} from '@xh/hoist/desktop/cmp/viewmanager/dialog/ManageDialog';
16
17
  import {Icon} from '@xh/hoist/icon';
17
- import {bindable, makeObservable} from '@xh/hoist/mobx';
18
+ import {action, bindable, computed, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
18
19
  import {pluralize} from '@xh/hoist/utils/js';
19
- import {ViewInfo, ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
20
- import {find, some, startCase} from 'lodash';
20
+ import {capitalize, isEmpty, some, startCase} from 'lodash';
21
+ import {ReactNode} from 'react';
22
+ import {ViewPanelModel} from './ViewPanelModel';
21
23
 
22
24
  /**
23
25
  * Backing model for ManageDialog
24
26
  */
25
27
  export class ManageDialogModel extends HoistModel {
26
- @lookup(() => ViewManagerModel)
27
28
  viewManagerModel: ViewManagerModel;
28
29
 
29
- @managed privateGridModel: GridModel;
30
+ @observable isOpen: boolean = false;
31
+
32
+ @managed ownedGridModel: GridModel;
30
33
  @managed globalGridModel: GridModel;
31
- @managed editFormModel: EditFormModel;
34
+ @managed sharedGridModel: GridModel;
35
+
36
+ @managed viewPanelModel: ViewPanelModel;
37
+
32
38
  @managed tabContainerModel: TabContainerModel;
33
39
 
34
- @bindable filter: FilterTestFn;
40
+ @bindable.ref filter: FilterTestFn;
35
41
 
36
42
  readonly updateTask = TaskObserver.trackLast();
37
43
 
@@ -40,30 +46,25 @@ export class ManageDialogModel extends HoistModel {
40
46
  }
41
47
 
42
48
  get gridModel(): GridModel {
43
- return this.tabContainerModel.activeTabId == 'global'
44
- ? this.globalGridModel
45
- : this.privateGridModel;
49
+ switch (this.tabContainerModel.activeTabId) {
50
+ case 'global':
51
+ return this.globalGridModel;
52
+ case 'shared':
53
+ return this.sharedGridModel;
54
+ case 'owned':
55
+ default:
56
+ return this.ownedGridModel;
57
+ }
46
58
  }
47
59
 
48
60
  @computed
49
61
  get selectedView(): ViewInfo {
50
- return this.gridModel.selectedRecord?.data.info;
62
+ return this.gridModel.selectedRecord?.data.view;
51
63
  }
52
64
 
53
65
  @computed
54
66
  get selectedViews(): ViewInfo[] {
55
- return this.gridModel.selectedRecords.map(it => it.data.info) as ViewInfo[];
56
- }
57
-
58
- get canDelete(): boolean {
59
- const {viewManagerModel, manageGlobal, selectedViews} = this,
60
- {views, enableDefault} = viewManagerModel;
61
-
62
- // Can't delete global views without role.
63
- if (!manageGlobal && selectedViews.some(v => v.isGlobal)) return false;
64
-
65
- // Can't delete all the views, unless default mode is enabled.
66
- return enableDefault || views.length - selectedViews.length > 0;
67
+ return this.gridModel.selectedRecords.map(it => it.data.view) as ViewInfo[];
67
68
  }
68
69
 
69
70
  get manageGlobal(): boolean {
@@ -78,60 +79,45 @@ export class ManageDialogModel extends HoistModel {
78
79
  return this.viewManagerModel.globalDisplayName;
79
80
  }
80
81
 
81
- get enableFavorites(): boolean {
82
- return this.viewManagerModel.enableFavorites;
82
+ get enableSharing(): boolean {
83
+ return this.viewManagerModel.enableSharing;
83
84
  }
84
85
 
85
- constructor() {
86
+ constructor(viewManagerModel: ViewManagerModel) {
86
87
  super();
87
88
  makeObservable(this);
89
+ this.viewManagerModel = viewManagerModel;
88
90
  }
89
91
 
90
- close() {
91
- this.viewManagerModel.closeManageDialog();
92
+ @action
93
+ open() {
94
+ if (!this.tabContainerModel) this.init();
95
+ this.loadAsync();
96
+ this.isOpen = true;
92
97
  }
93
98
 
94
- override onLinked() {
95
- super.onLinked();
96
-
97
- this.privateGridModel = this.createGridModel('personal');
98
- this.globalGridModel = this.createGridModel(this.globalDisplayName);
99
- this.tabContainerModel = this.createTabContainerModel();
100
- this.editFormModel = new EditFormModel(this);
99
+ @action
100
+ close() {
101
+ this.isOpen = false;
102
+ }
101
103
 
102
- const {privateGridModel, globalGridModel, editFormModel} = this;
103
- this.addReaction(
104
- {
105
- track: () => this.selectedView,
106
- run: r => editFormModel.setView(r)
107
- },
108
- {
109
- track: () => this.filter,
110
- run: f => {
111
- privateGridModel.store.setFilter(f);
112
- globalGridModel.store.setFilter(f);
113
- },
114
- fireImmediately: true
115
- },
116
- {
117
- track: () => privateGridModel.selectedRecords,
118
- run: recs => {
119
- if (recs.length) globalGridModel.clearSelection();
120
- }
121
- },
122
- {
123
- track: () => globalGridModel.selectedRecords,
124
- run: recs => {
125
- if (recs.length) privateGridModel.clearSelection();
126
- }
127
- }
128
- );
104
+ activateSelectedViewAndClose() {
105
+ this.viewManagerModel.selectViewAsync(this.selectedView);
106
+ this.close();
129
107
  }
130
108
 
131
109
  override async doLoadAsync(loadSpec: LoadSpec) {
132
- const {view, globalViews, privateViews} = this.viewManagerModel;
133
- this.globalGridModel.loadData(globalViews);
134
- this.privateGridModel.loadData(privateViews);
110
+ const {tabContainerModel} = this,
111
+ {view, ownedViews, globalViews, sharedViews} = this.viewManagerModel;
112
+
113
+ runInAction(() => {
114
+ this.ownedGridModel.loadData(ownedViews);
115
+ this.globalGridModel.loadData(globalViews);
116
+ this.sharedGridModel.loadData(sharedViews);
117
+ tabContainerModel.setTabTitle('owned', this.ownedTabTitle);
118
+ tabContainerModel.setTabTitle('global', this.globalTabTitle);
119
+ tabContainerModel.setTabTitle('shared', this.sharedTabTitle);
120
+ });
135
121
  if (!loadSpec.isRefresh && !view.isDefault) {
136
122
  await this.selectViewAsync(view.info);
137
123
  }
@@ -141,51 +127,74 @@ export class ManageDialogModel extends HoistModel {
141
127
  return this.doDeleteAsync(views).linkTo(this.updateTask).catchDefault();
142
128
  }
143
129
 
144
- async updateAsync(view: ViewInfo, name: string, description: string, isGlobal: boolean) {
145
- return this.doUpdateAsync(view, name, description, isGlobal)
146
- .linkTo(this.updateTask)
147
- .catchDefault();
130
+ async updateAsync(view: ViewInfo, update: ViewUpdateSpec) {
131
+ return this.doUpdateAsync(view, update).linkTo(this.updateTask).catchDefault();
132
+ }
133
+
134
+ async makeGlobalAsync(view: ViewInfo) {
135
+ return this.doMakeGlobalAsync(view).linkTo(this.updateTask).catchDefault();
136
+ }
137
+
138
+ @action
139
+ togglePinned(views: ViewInfo[]) {
140
+ views.forEach(v => this.viewManagerModel.togglePinned(v));
141
+ this.refreshAsync();
148
142
  }
149
143
 
150
144
  //------------------------
151
145
  // Implementation
152
146
  //------------------------
153
- private async doUpdateAsync(
154
- view: ViewInfo,
155
- name: string,
156
- description: string,
157
- isGlobal: boolean
158
- ) {
159
- const {viewManagerModel} = this;
147
+ private init() {
148
+ this.ownedGridModel = this.createGridModel('owned');
149
+ this.globalGridModel = this.createGridModel('global');
150
+ this.sharedGridModel = this.createGridModel('shared');
151
+ this.tabContainerModel = this.createTabContainerModel();
152
+ this.viewPanelModel = new ViewPanelModel(this);
153
+ const gridModels = [this.ownedGridModel, this.globalGridModel, this.sharedGridModel];
154
+ this.addReaction({
155
+ track: () => this.filter,
156
+ run: f => gridModels.forEach(m => m.store.setFilter(f)),
157
+ fireImmediately: true
158
+ });
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
+ }
167
+ });
168
+ });
169
+ }
160
170
 
161
- await viewManagerModel.api.updateViewInfoAsync(view, name, description, isGlobal);
171
+ private async doUpdateAsync(view: ViewInfo, update: ViewUpdateSpec) {
172
+ const {viewManagerModel} = this;
173
+ await viewManagerModel.api.updateViewInfoAsync(view, update);
162
174
  await viewManagerModel.refreshAsync();
163
175
  await this.refreshAsync();
164
-
165
- // reselect the updated copy of this view -- it may have moved.
166
- await this.selectViewAsync(find(viewManagerModel.views, {token: view.token}));
167
176
  }
168
177
 
169
178
  private async doDeleteAsync(views: ViewInfo[]) {
170
179
  const {viewManagerModel, typeDisplayName} = this,
171
- {enableDefault} = viewManagerModel,
172
180
  count = views.length;
173
181
 
174
182
  if (!count) return;
175
183
 
176
- if (viewManagerModel.views.length === count && !enableDefault) {
177
- throw XH.exception({
178
- message: `You cannot delete all ${pluralize(typeDisplayName)}.`,
179
- isRoutine: true
180
- });
181
- }
182
-
183
184
  const confirmStr = count > 1 ? pluralize(typeDisplayName, count, true) : views[0].typedName;
184
185
  const msgs: ReactNode[] = [`Are you sure you want to delete ${confirmStr}?`];
185
- if (some(views, 'isGlobal')) {
186
+ if (some(views, v => v.isGlobal || v.isShared)) {
186
187
  count > 1
187
- ? msgs.push(strong('These global views will no longer be available to ALL users.'))
188
- : msgs.push(strong('This global view will no longer be available to ALL users.'));
188
+ ? msgs.push(
189
+ strong(
190
+ `This includes at least one public ${typeDisplayName}, to be deleted for all users.`
191
+ )
192
+ )
193
+ : msgs.push(
194
+ strong(
195
+ `This is a public ${typeDisplayName} and will be deleted for all users.`
196
+ )
197
+ );
189
198
  }
190
199
 
191
200
  const confirmed = await XH.confirm({
@@ -207,73 +216,175 @@ export class ManageDialogModel extends HoistModel {
207
216
  await this.refreshAsync();
208
217
  }
209
218
 
210
- async selectViewAsync(view: ViewInfo) {
211
- this.tabContainerModel.activateTab(view.isGlobal ? 'global' : 'private');
219
+ private async doMakeGlobalAsync(view: ViewInfo) {
220
+ const {globalDisplayName, typeDisplayName} = this.viewManagerModel,
221
+ {typedName} = view,
222
+ msgs = [
223
+ `The ${typedName} will become a ${globalDisplayName} ${typeDisplayName} visible to all other ${XH.appName} users.`,
224
+ strong('Are you sure you want to proceed?')
225
+ ];
226
+
227
+ const confirmed = await XH.confirm({
228
+ message: fragment(msgs.map(m => p(m))),
229
+ confirmProps: {
230
+ text: `Yes, change visibility`,
231
+ outlined: true,
232
+ autoFocus: false,
233
+ intent: 'primary'
234
+ }
235
+ });
236
+ if (!confirmed) return;
237
+
238
+ const {viewManagerModel} = this;
239
+ const updated = await viewManagerModel.api.makeViewGlobalAsync(view);
240
+ await viewManagerModel.refreshAsync();
241
+ await this.refreshAsync();
242
+ await this.selectViewAsync(updated.info); // reselect -- will have moved tabs!
243
+ }
244
+
245
+ private async selectViewAsync(view: ViewInfo) {
246
+ this.tabContainerModel.activateTab(
247
+ view.isOwned ? 'owned' : view.isGlobal ? 'global' : 'shared'
248
+ );
212
249
  await this.gridModel.selectAsync(view.token);
213
250
  }
214
251
 
215
- private createGridModel(name: string): GridModel {
252
+ private createGridModel(type: 'owned' | 'global' | 'shared'): GridModel {
253
+ const {typeDisplayName, globalDisplayName} = this;
254
+
255
+ const modifier =
256
+ type == 'owned' ? `personal` : type == 'global' ? globalDisplayName : 'shared';
257
+
216
258
  return new GridModel({
217
- emptyText: `No ${name} ${pluralize(this.typeDisplayName)} found...`,
259
+ emptyText: `No ${modifier} ${pluralize(typeDisplayName)} found...`,
218
260
  sortBy: 'name',
219
- hideHeaders: true,
220
261
  showGroupRowCounts: false,
262
+ groupBy: ['group'],
263
+ groupSortFn: (a, b) => {
264
+ // Place ungrouped items at bottom.
265
+ if (a == '') return 1;
266
+ if (b == '') return -1;
267
+ return a.localeCompare(b);
268
+ },
221
269
  selModel: 'multiple',
222
270
  contextMenu: null,
223
271
  sizingMode: 'standard',
272
+ hideHeaders: true,
224
273
  store: {
225
274
  idSpec: 'token',
226
- processRawData: v => ({name: v.name, isFavorite: v.isFavorite, info: v}),
275
+ processRawData: v => ({
276
+ name: v.name,
277
+ group: v.isGlobal || v.isOwned ? v.group : v.owner,
278
+ owner: v.owner,
279
+ lastUpdated: v.lastUpdated,
280
+ isPinned: v.isPinned,
281
+ view: v
282
+ }),
227
283
  fields: [
228
284
  {name: 'name', type: 'string'},
229
- {name: 'isFavorite', type: 'bool'},
230
- {name: 'info', type: 'auto'}
285
+ {name: 'group', type: 'string'},
286
+ {name: 'owner', type: 'string'},
287
+ {name: 'lastUpdated', type: 'date'},
288
+ {name: 'isPinned', type: 'bool'},
289
+ {name: 'view', type: 'auto'}
231
290
  ]
232
291
  },
233
292
  autosizeOptions: {mode: GridAutosizeMode.DISABLED},
234
293
  columns: [
235
294
  {field: 'name', flex: true},
295
+ {field: 'group', hidden: true},
296
+ {field: 'owner', hidden: true},
297
+ {field: 'lastUpdated', ...dateTimeCol},
236
298
  {
237
- colId: 'isFavorite',
238
- field: 'info',
239
- omit: !this.enableFavorites,
299
+ field: 'isPinned',
240
300
  width: 40,
241
301
  align: 'center',
242
- headerName: Icon.favorite(),
243
- highlightOnChange: true,
244
- renderer: v => {
245
- const {isFavorite} = v;
246
- return Icon.favorite({
247
- prefix: isFavorite ? 'fas' : 'fal',
248
- className: isFavorite ? 'xh-yellow' : 'xh-text-color-muted'
302
+ headerName: Icon.pin(),
303
+ headerTooltip: 'Pin to menu',
304
+ renderer: (isPinned, {record}) => {
305
+ return button({
306
+ icon: Icon.pin({
307
+ prefix: isPinned ? 'fas' : 'fal',
308
+ className: isPinned ? 'xh-yellow' : 'xh-text-color-muted'
309
+ }),
310
+ tooltip: isPinned ? 'Unpin from menu' : 'Pin to menu',
311
+ onClick: () => {
312
+ this.togglePinned([record.data.view]);
313
+ }
249
314
  });
250
315
  }
251
316
  }
252
317
  ],
253
- onCellClicked: ({colDef, data: record, api}) => {
254
- if (colDef.colId === 'isFavorite') {
255
- this.viewManagerModel.toggleFavorite(record.id);
256
- api.redrawRows();
257
- }
258
- }
318
+ groupRowRenderer: ({value}) => (isEmpty(value) ? 'Ungrouped' : value)
259
319
  });
260
320
  }
261
321
 
262
322
  private createTabContainerModel(): TabContainerModel {
263
- const pluralType = startCase(pluralize(this.typeDisplayName));
323
+ const view = this.typeDisplayName,
324
+ views = pluralize(view),
325
+ globalViews = `${this.globalDisplayName} ${views}`,
326
+ {enableSharing} = this.viewManagerModel;
327
+
264
328
  return new TabContainerModel({
265
329
  tabs: [
266
330
  {
267
- id: 'private',
268
- title: `My ${pluralType}`,
269
- content: grid({model: this.privateGridModel})
331
+ id: 'owned',
332
+ title: this.ownedTabTitle,
333
+ content: viewsGrid({
334
+ model: this.ownedGridModel,
335
+ helpText: fragment(
336
+ Icon.user(),
337
+ `This tab shows ${views} you have created. Pinned ${views} are shown in your menu for quick access. Set a group on ${views} to show them together in a sub-menu. `,
338
+ enableSharing
339
+ ? `Opt-in to sharing any of your ${views} to make them discoverable by other users.`
340
+ : ''
341
+ )
342
+ })
270
343
  },
271
344
  {
272
345
  id: 'global',
273
- title: `${startCase(this.globalDisplayName)} ${pluralType}`,
274
- content: grid({model: this.globalGridModel})
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
+ })
275
365
  }
276
366
  ]
277
367
  });
278
368
  }
369
+
370
+ private get ownedTabTitle(): ReactNode {
371
+ return hbox(
372
+ `My ${startCase(pluralize(this.typeDisplayName))}`,
373
+ badge(this.ownedGridModel.store.allCount)
374
+ );
375
+ }
376
+
377
+ private get globalTabTitle(): ReactNode {
378
+ return hbox(
379
+ `${startCase(this.globalDisplayName)} ${startCase(pluralize(this.typeDisplayName))}`,
380
+ badge(this.globalGridModel.store.allCount)
381
+ );
382
+ }
383
+
384
+ private get sharedTabTitle(): ReactNode {
385
+ return hbox(
386
+ `Shared ${startCase(pluralize(this.typeDisplayName))}`,
387
+ badge(this.sharedGridModel.store.allCount)
388
+ );
389
+ }
279
390
  }
@@ -6,16 +6,17 @@
6
6
  */
7
7
 
8
8
  import {form} from '@xh/hoist/cmp/form';
9
- import {filler, vframe} from '@xh/hoist/cmp/layout';
9
+ import {filler, hbox, vframe} from '@xh/hoist/cmp/layout';
10
10
  import {hoistCmp, uses} from '@xh/hoist/core';
11
- import {SaveAsDialogModel} from '@xh/hoist/cmp/viewmanager/';
12
11
  import {button} from '@xh/hoist/desktop/cmp/button';
13
12
  import {formField} from '@xh/hoist/desktop/cmp/form';
14
- import {textArea, textInput} from '@xh/hoist/desktop/cmp/input';
13
+ import {select, switchInput, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
15
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
15
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
17
16
  import {dialog} from '@xh/hoist/kit/blueprint';
18
17
  import {startCase} from 'lodash';
18
+ import {SaveAsDialogModel} from './SaveAsDialogModel';
19
+ import {getGroupOptions} from './Utils';
19
20
 
20
21
  /**
21
22
  * Default Save As dialog used by ViewManager.
@@ -34,7 +35,7 @@ export const saveAsDialog = hoistCmp.factory<SaveAsDialogModel>({
34
35
  isOpen: true,
35
36
  style: {width: 500},
36
37
  canOutsideClickClose: false,
37
- onClose: () => model.cancel(),
38
+ onClose: () => model.close(),
38
39
  item: formPanel()
39
40
  });
40
41
  }
@@ -45,7 +46,9 @@ const formPanel = hoistCmp.factory<SaveAsDialogModel>({
45
46
  return panel({
46
47
  item: form({
47
48
  fieldDefaults: {
48
- commitOnChange: true
49
+ commitOnChange: true,
50
+ inline: true,
51
+ minimal: true
49
52
  },
50
53
  item: vframe({
51
54
  className: 'xh-view-manager__save-dialog__form',
@@ -60,13 +63,31 @@ const formPanel = hoistCmp.factory<SaveAsDialogModel>({
60
63
  }
61
64
  })
62
65
  }),
66
+ formField({
67
+ field: 'group',
68
+ item: select({
69
+ enableCreate: true,
70
+ enableClear: true,
71
+ placeholder: 'Select optional group....',
72
+ options: getGroupOptions(model.parent, 'owned')
73
+ })
74
+ }),
63
75
  formField({
64
76
  field: 'description',
65
77
  item: textArea({
66
78
  selectOnFocus: true,
67
- height: 90
79
+ height: 70
80
+ })
81
+ }),
82
+ hbox(
83
+ formField({
84
+ field: 'isShared',
85
+ label: 'Share?',
86
+ labelTextAlign: 'left',
87
+ omit: !model.parent.enableSharing,
88
+ item: switchInput()
68
89
  })
69
- })
90
+ )
70
91
  ]
71
92
  })
72
93
  }),
@@ -78,12 +99,12 @@ const formPanel = hoistCmp.factory<SaveAsDialogModel>({
78
99
 
79
100
  const bbar = hoistCmp.factory<SaveAsDialogModel>({
80
101
  render({model}) {
81
- const {typeDisplayName} = model;
102
+ const {typeDisplayName} = model.parent;
82
103
  return toolbar(
83
104
  filler(),
84
105
  button({
85
106
  text: 'Cancel',
86
- onClick: () => model.cancel()
107
+ onClick: () => model.close()
87
108
  }),
88
109
  button({
89
110
  text: `Save as new ${startCase(typeDisplayName)}`,