@xh/hoist 76.0.0-SNAPSHOT.1757113228632 → 76.0.0-SNAPSHOT.1757274887417
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 +7 -0
- package/build/types/cmp/viewmanager/DataAccess.d.ts +0 -2
- package/build/types/cmp/viewmanager/ViewInfo.d.ts +4 -8
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +10 -11
- package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +0 -2
- package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +9 -1
- package/cmp/viewmanager/DataAccess.ts +0 -11
- package/cmp/viewmanager/ViewInfo.ts +5 -11
- package/cmp/viewmanager/ViewManagerModel.ts +15 -15
- package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +3 -40
- package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +17 -13
- package/desktop/cmp/viewmanager/dialog/SaveAsDialogModel.ts +42 -11
- package/desktop/cmp/viewmanager/dialog/Utils.ts +35 -3
- package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +20 -37
- package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +36 -13
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
toolbar for clarity.
|
|
16
16
|
* Added new ability to specify nested tab containers in a single declarative config. Apps may now
|
|
17
17
|
provide a spec for a nested tab container directly to the `TabConfig.content` property.
|
|
18
|
+
* Improvements to View Management:
|
|
19
|
+
** Allow users to create 'Global' views directly in 'Save/Save As' Dialog.
|
|
20
|
+
** Simplify presentation/edit of view visibility to new "Visibility" control
|
|
21
|
+
** Support for the 'isDefaultPinned' attribute on global views has been removed. All global
|
|
22
|
+
views will be pinned by default. This feature was deemed too confusing, and not useful in
|
|
23
|
+
practice. App maintainers should ensure that all global views are appropriate and well
|
|
24
|
+
organized enough to be shown immediately to new users in the view menu.
|
|
18
25
|
|
|
19
26
|
### 🐞 Bug Fixes
|
|
20
27
|
|
|
@@ -20,8 +20,6 @@ export declare class DataAccess<T> {
|
|
|
20
20
|
createViewAsync(spec: ViewCreateSpec): Promise<View<T>>;
|
|
21
21
|
/** Update all aspects of a view's metadata.*/
|
|
22
22
|
updateViewInfoAsync(view: ViewInfo, updates: ViewUpdateSpec): Promise<View<T>>;
|
|
23
|
-
/** Promote a view to global visibility/ownership status. */
|
|
24
|
-
makeViewGlobalAsync(view: ViewInfo): Promise<View<T>>;
|
|
25
23
|
/** Update a view's value. */
|
|
26
24
|
updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>>;
|
|
27
25
|
deleteViewsAsync(views: ViewInfo[]): Promise<void>;
|
|
@@ -24,12 +24,6 @@ export declare class ViewInfo {
|
|
|
24
24
|
readonly isGlobal: boolean;
|
|
25
25
|
/** Optional group name used for bucketing this view in display. */
|
|
26
26
|
readonly group: string;
|
|
27
|
-
/**
|
|
28
|
-
* True if this view should be pinned by default to all users' menus, where it will appear
|
|
29
|
-
* unless the user has explicitly unpinned it. Only applicable for global views, can be enabled
|
|
30
|
-
* by view managers to promote especially important global views and ensure users see them.
|
|
31
|
-
*/
|
|
32
|
-
readonly isDefaultPinned: boolean;
|
|
33
27
|
/**
|
|
34
28
|
* Original meta-data on views associated JsonBlob.
|
|
35
29
|
* Not typically used by applications.
|
|
@@ -47,8 +41,10 @@ export declare class ViewInfo {
|
|
|
47
41
|
/**
|
|
48
42
|
* True if this view should appear on the users easy access menu.
|
|
49
43
|
*
|
|
50
|
-
* This value is computed with the user persisted state
|
|
51
|
-
*
|
|
44
|
+
* This value is computed with the user persisted state for the view.
|
|
45
|
+
*
|
|
46
|
+
* If the user has not set pinning state for the view, global views and
|
|
47
|
+
* owned views are pinned by default.
|
|
52
48
|
*/
|
|
53
49
|
get isPinned(): boolean;
|
|
54
50
|
/**
|
|
@@ -8,16 +8,11 @@ export interface ViewCreateSpec {
|
|
|
8
8
|
group: string;
|
|
9
9
|
description: string;
|
|
10
10
|
isShared: boolean;
|
|
11
|
-
|
|
11
|
+
isGlobal: boolean;
|
|
12
|
+
isPinned?: boolean;
|
|
12
13
|
value: PlainObject;
|
|
13
14
|
}
|
|
14
|
-
export
|
|
15
|
-
name?: string;
|
|
16
|
-
group?: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
isShared?: boolean;
|
|
19
|
-
isDefaultPinned?: boolean;
|
|
20
|
-
}
|
|
15
|
+
export type ViewUpdateSpec = Partial<Omit<ViewCreateSpec, 'value'>>;
|
|
21
16
|
export interface ViewUserState {
|
|
22
17
|
currentView?: string;
|
|
23
18
|
userPinned: Record<string, boolean>;
|
|
@@ -209,11 +204,15 @@ export declare class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
209
204
|
userPin(view: ViewInfo): void;
|
|
210
205
|
userUnpin(view: ViewInfo): void;
|
|
211
206
|
isUserPinned(view: ViewInfo): boolean | null;
|
|
212
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Validate a name for a view.
|
|
209
|
+
* @param name - candidate name to validate
|
|
210
|
+
* @param existing - existing view that will have the name. null if the name is for a new view.
|
|
211
|
+
* @param isGlobal - true if the name is for a global view.
|
|
212
|
+
*/
|
|
213
|
+
validateViewNameAsync(name: string, existing: ViewInfo, isGlobal: boolean): Promise<string>;
|
|
213
214
|
/** Update all aspects of a view's metadata.*/
|
|
214
215
|
updateViewInfoAsync(view: ViewInfo, updates: ViewUpdateSpec): Promise<View<T>>;
|
|
215
|
-
/** Promote a view to global visibility/ownership status. */
|
|
216
|
-
makeViewGlobalAsync(view: ViewInfo): Promise<View<T>>;
|
|
217
216
|
deleteViewsAsync(toDelete: ViewInfo[]): Promise<void>;
|
|
218
217
|
/**
|
|
219
218
|
* Called by {@link ViewManagerProvider} to receive state changes from this model.
|
|
@@ -28,12 +28,10 @@ export declare class ManageDialogModel extends HoistModel {
|
|
|
28
28
|
doLoadAsync(loadSpec: LoadSpec): Promise<void>;
|
|
29
29
|
deleteAsync(views: ViewInfo[]): Promise<void>;
|
|
30
30
|
updateAsync(view: ViewInfo, update: ViewUpdateSpec): Promise<void>;
|
|
31
|
-
makeGlobalAsync(view: ViewInfo): Promise<void>;
|
|
32
31
|
togglePinned(views: ViewInfo[]): void;
|
|
33
32
|
private init;
|
|
34
33
|
private doUpdateAsync;
|
|
35
34
|
private doDeleteAsync;
|
|
36
|
-
private doMakeGlobalAsync;
|
|
37
35
|
private selectViewAsync;
|
|
38
36
|
private createGridModel;
|
|
39
37
|
private createTabContainerModel;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { ViewManagerModel } from '@xh/hoist/cmp/viewmanager';
|
|
2
2
|
import { SelectOption } from '@xh/hoist/core';
|
|
3
|
-
export declare function getGroupOptions(
|
|
3
|
+
export declare function getGroupOptions(vmm: ViewManagerModel, isGlobal: boolean): SelectOption[];
|
|
4
|
+
/**
|
|
5
|
+
* Support for "Visibility" concept used in default view editing/creation.
|
|
6
|
+
* This tri-state selection will translate into boolean `isGlobal` and `isShared`
|
|
7
|
+
* flag settings.
|
|
8
|
+
*/
|
|
9
|
+
export type Visibility = 'private' | 'shared' | 'global';
|
|
10
|
+
export declare function getVisibilityOptions(vmm: ViewManagerModel): SelectOption[];
|
|
11
|
+
export declare function getVisibilityInfo(vmm: ViewManagerModel, val: Visibility): string;
|
|
@@ -88,17 +88,6 @@ export class DataAccess<T> {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/** Promote a view to global visibility/ownership status. */
|
|
92
|
-
async makeViewGlobalAsync(view: ViewInfo): Promise<View<T>> {
|
|
93
|
-
try {
|
|
94
|
-
this.ensureEditable(view);
|
|
95
|
-
const raw = await XH.fetchJson({url: 'xhView/makeGlobal', params: {token: view.token}});
|
|
96
|
-
return View.fromBlob(raw, this.model);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
91
|
/** Update a view's value. */
|
|
103
92
|
async updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>> {
|
|
104
93
|
try {
|
|
@@ -41,13 +41,6 @@ export class ViewInfo {
|
|
|
41
41
|
/** Optional group name used for bucketing this view in display. */
|
|
42
42
|
readonly group: string;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* True if this view should be pinned by default to all users' menus, where it will appear
|
|
46
|
-
* unless the user has explicitly unpinned it. Only applicable for global views, can be enabled
|
|
47
|
-
* by view managers to promote especially important global views and ensure users see them.
|
|
48
|
-
*/
|
|
49
|
-
readonly isDefaultPinned: boolean;
|
|
50
|
-
|
|
51
44
|
/**
|
|
52
45
|
* Original meta-data on views associated JsonBlob.
|
|
53
46
|
* Not typically used by applications.
|
|
@@ -73,7 +66,6 @@ export class ViewInfo {
|
|
|
73
66
|
this.isGlobal = !this.owner;
|
|
74
67
|
|
|
75
68
|
this.group = this.meta.group ?? null;
|
|
76
|
-
this.isDefaultPinned = !!(this.isGlobal && this.meta.isDefaultPinned);
|
|
77
69
|
this.isShared = !!(!this.isGlobal && this.meta.isShared);
|
|
78
70
|
|
|
79
71
|
// Round to seconds. See: https://github.com/xh/hoist-core/issues/423
|
|
@@ -98,11 +90,13 @@ export class ViewInfo {
|
|
|
98
90
|
/**
|
|
99
91
|
* True if this view should appear on the users easy access menu.
|
|
100
92
|
*
|
|
101
|
-
* This value is computed with the user persisted state
|
|
102
|
-
*
|
|
93
|
+
* This value is computed with the user persisted state for the view.
|
|
94
|
+
*
|
|
95
|
+
* If the user has not set pinning state for the view, global views and
|
|
96
|
+
* owned views are pinned by default.
|
|
103
97
|
*/
|
|
104
98
|
get isPinned(): boolean {
|
|
105
|
-
return this.isUserPinned ?? this.
|
|
99
|
+
return this.isUserPinned ?? (this.isGlobal || this.isOwned);
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
/**
|
|
@@ -32,17 +32,12 @@ export interface ViewCreateSpec {
|
|
|
32
32
|
group: string;
|
|
33
33
|
description: string;
|
|
34
34
|
isShared: boolean;
|
|
35
|
-
|
|
35
|
+
isGlobal: boolean;
|
|
36
|
+
isPinned?: boolean;
|
|
36
37
|
value: PlainObject;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export
|
|
40
|
-
name?: string;
|
|
41
|
-
group?: string;
|
|
42
|
-
description?: string;
|
|
43
|
-
isShared?: boolean;
|
|
44
|
-
isDefaultPinned?: boolean;
|
|
45
|
-
}
|
|
40
|
+
export type ViewUpdateSpec = Partial<Omit<ViewCreateSpec, 'value'>>;
|
|
46
41
|
|
|
47
42
|
export interface ViewUserState {
|
|
48
43
|
currentView?: string;
|
|
@@ -466,14 +461,24 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
466
461
|
//-----------------
|
|
467
462
|
// Management
|
|
468
463
|
//-----------------
|
|
469
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Validate a name for a view.
|
|
466
|
+
* @param name - candidate name to validate
|
|
467
|
+
* @param existing - existing view that will have the name. null if the name is for a new view.
|
|
468
|
+
* @param isGlobal - true if the name is for a global view.
|
|
469
|
+
*/
|
|
470
|
+
async validateViewNameAsync(
|
|
471
|
+
name: string,
|
|
472
|
+
existing: ViewInfo,
|
|
473
|
+
isGlobal: boolean
|
|
474
|
+
): Promise<string> {
|
|
470
475
|
const maxLength = 50;
|
|
471
476
|
name = name?.trim();
|
|
472
477
|
if (!name) return 'Name is required';
|
|
473
478
|
if (name.length > maxLength) {
|
|
474
479
|
return `Name cannot be longer than ${maxLength} characters`;
|
|
475
480
|
}
|
|
476
|
-
const views =
|
|
481
|
+
const views = isGlobal ? this.globalViews : this.ownedViews;
|
|
477
482
|
if (views.some(view => view.name === name && view.token != existing?.token)) {
|
|
478
483
|
return `A ${this.typeDisplayName} with name '${name}' already exists.`;
|
|
479
484
|
}
|
|
@@ -485,11 +490,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
485
490
|
return this.dataAccess.updateViewInfoAsync(view, updates);
|
|
486
491
|
}
|
|
487
492
|
|
|
488
|
-
/** Promote a view to global visibility/ownership status. */
|
|
489
|
-
async makeViewGlobalAsync(view: ViewInfo): Promise<View<T>> {
|
|
490
|
-
return this.dataAccess.makeViewGlobalAsync(view);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
493
|
async deleteViewsAsync(toDelete: ViewInfo[]): Promise<void> {
|
|
494
494
|
let exception;
|
|
495
495
|
try {
|
|
@@ -122,10 +122,6 @@ export class ManageDialogModel extends HoistModel {
|
|
|
122
122
|
return this.doUpdateAsync(view, update).linkTo(this.updateTask).catchDefault();
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async makeGlobalAsync(view: ViewInfo) {
|
|
126
|
-
return this.doMakeGlobalAsync(view).linkTo(this.updateTask).catchDefault();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
125
|
@action
|
|
130
126
|
togglePinned(views: ViewInfo[]) {
|
|
131
127
|
const allPinned = every(views, 'isPinned'),
|
|
@@ -177,9 +173,10 @@ export class ManageDialogModel extends HoistModel {
|
|
|
177
173
|
|
|
178
174
|
private async doUpdateAsync(view: ViewInfo, update: ViewUpdateSpec) {
|
|
179
175
|
const {viewManagerModel} = this;
|
|
180
|
-
await viewManagerModel.updateViewInfoAsync(view, update);
|
|
176
|
+
const updated = await viewManagerModel.updateViewInfoAsync(view, update);
|
|
181
177
|
await viewManagerModel.refreshAsync();
|
|
182
178
|
await this.refreshAsync();
|
|
179
|
+
await this.selectViewAsync(updated.info); // reselect -- may have moved tabs!
|
|
183
180
|
}
|
|
184
181
|
|
|
185
182
|
private async doDeleteAsync(views: ViewInfo[]) {
|
|
@@ -219,40 +216,6 @@ export class ManageDialogModel extends HoistModel {
|
|
|
219
216
|
return viewManagerModel.deleteViewsAsync(views).finally(() => this.refreshAsync());
|
|
220
217
|
}
|
|
221
218
|
|
|
222
|
-
private async doMakeGlobalAsync(view: ViewInfo) {
|
|
223
|
-
const {globalDisplayName, typeDisplayName, globalViews} = this.viewManagerModel,
|
|
224
|
-
{typedName} = view;
|
|
225
|
-
|
|
226
|
-
if (some(globalViews, {name: view.name})) {
|
|
227
|
-
XH.alert({
|
|
228
|
-
title: 'Alert',
|
|
229
|
-
message: `There is already a ${globalDisplayName} ${typedName}. Please rename or edit it instead.`
|
|
230
|
-
});
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const msgs = [
|
|
235
|
-
`The ${typedName} will become a ${globalDisplayName} ${typeDisplayName} visible to all other ${XH.appName} users.`,
|
|
236
|
-
strong('Are you sure you want to proceed?')
|
|
237
|
-
];
|
|
238
|
-
const confirmed = await XH.confirm({
|
|
239
|
-
message: fragment(msgs.map(m => p(m))),
|
|
240
|
-
confirmProps: {
|
|
241
|
-
text: `Yes, change visibility`,
|
|
242
|
-
outlined: true,
|
|
243
|
-
autoFocus: false,
|
|
244
|
-
intent: 'primary'
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
if (!confirmed) return;
|
|
248
|
-
|
|
249
|
-
const {viewManagerModel} = this;
|
|
250
|
-
const updated = await viewManagerModel.makeViewGlobalAsync(view);
|
|
251
|
-
await viewManagerModel.refreshAsync();
|
|
252
|
-
await this.refreshAsync();
|
|
253
|
-
await this.selectViewAsync(updated.info); // reselect -- will have moved tabs!
|
|
254
|
-
}
|
|
255
|
-
|
|
256
219
|
private async selectViewAsync(view: ViewInfo) {
|
|
257
220
|
this.tabContainerModel.activateTab(
|
|
258
221
|
view.isOwned ? 'owned' : view.isGlobal ? 'global' : 'shared'
|
|
@@ -362,7 +325,7 @@ export class ManageDialogModel extends HoistModel {
|
|
|
362
325
|
Icon.globe(),
|
|
363
326
|
`This tab shows ${globalViews} available to everyone.`,
|
|
364
327
|
br(),
|
|
365
|
-
`${capitalize(globalViews)}
|
|
328
|
+
`${capitalize(globalViews)} appear by default in everyone's menu, but you can choose which ${views} you would like to see by pinning/unpinning them at any time.`
|
|
366
329
|
)
|
|
367
330
|
})
|
|
368
331
|
});
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {form} from '@xh/hoist/cmp/form';
|
|
9
|
-
import {filler,
|
|
9
|
+
import {filler, vframe} from '@xh/hoist/cmp/layout';
|
|
10
10
|
import {hoistCmp, uses} from '@xh/hoist/core';
|
|
11
11
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
12
12
|
import {formField} from '@xh/hoist/desktop/cmp/form';
|
|
13
|
-
import {select,
|
|
13
|
+
import {select, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
|
|
14
14
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
15
15
|
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
|
|
16
16
|
import {dialog} from '@xh/hoist/kit/blueprint';
|
|
17
17
|
import {startCase} from 'lodash';
|
|
18
18
|
import {SaveAsDialogModel} from './SaveAsDialogModel';
|
|
19
|
-
import {getGroupOptions} from './Utils';
|
|
19
|
+
import {getGroupOptions, getVisibilityOptions, getVisibilityInfo} from './Utils';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Default Save As dialog used by ViewManager.
|
|
@@ -43,6 +43,13 @@ export const saveAsDialog = hoistCmp.factory<SaveAsDialogModel>({
|
|
|
43
43
|
|
|
44
44
|
const formPanel = hoistCmp.factory<SaveAsDialogModel>({
|
|
45
45
|
render({model}) {
|
|
46
|
+
const {parent, formModel} = model,
|
|
47
|
+
{visibility} = formModel.values,
|
|
48
|
+
isGlobal = visibility === 'global',
|
|
49
|
+
groupOptions = getGroupOptions(parent, isGlobal),
|
|
50
|
+
visOptions = getVisibilityOptions(parent),
|
|
51
|
+
visInfo = getVisibilityInfo(parent, visibility);
|
|
52
|
+
|
|
46
53
|
return panel({
|
|
47
54
|
item: form({
|
|
48
55
|
fieldDefaults: {
|
|
@@ -69,7 +76,7 @@ const formPanel = hoistCmp.factory<SaveAsDialogModel>({
|
|
|
69
76
|
enableCreate: true,
|
|
70
77
|
enableClear: true,
|
|
71
78
|
placeholder: 'Select optional group....',
|
|
72
|
-
options:
|
|
79
|
+
options: groupOptions
|
|
73
80
|
})
|
|
74
81
|
}),
|
|
75
82
|
formField({
|
|
@@ -79,15 +86,12 @@ const formPanel = hoistCmp.factory<SaveAsDialogModel>({
|
|
|
79
86
|
height: 70
|
|
80
87
|
})
|
|
81
88
|
}),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
item: switchInput()
|
|
89
|
-
})
|
|
90
|
-
)
|
|
89
|
+
formField({
|
|
90
|
+
field: 'visibility',
|
|
91
|
+
omit: visOptions.length === 1,
|
|
92
|
+
item: select({options: visOptions, enableFilter: false}),
|
|
93
|
+
info: visInfo
|
|
94
|
+
})
|
|
91
95
|
]
|
|
92
96
|
})
|
|
93
97
|
}),
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {FormModel} from '@xh/hoist/cmp/form';
|
|
9
|
+
import {p, strong} from '@xh/hoist/cmp/layout';
|
|
9
10
|
import {HoistModel, managed, XH} from '@xh/hoist/core';
|
|
10
11
|
import {makeObservable, action, observable} from '@xh/hoist/mobx';
|
|
11
12
|
import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
|
|
13
|
+
import {some} from 'lodash';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Backing model for ViewManagerModel's SaveAs
|
|
@@ -30,15 +32,14 @@ export class SaveAsDialogModel extends HoistModel {
|
|
|
30
32
|
open() {
|
|
31
33
|
const {parent, formModel} = this,
|
|
32
34
|
src = parent.view,
|
|
33
|
-
name = parent.ownedViews.
|
|
34
|
-
? `Copy of ${src.name}`
|
|
35
|
-
: src.name;
|
|
35
|
+
name = some(parent.ownedViews, {name: src.name}) ? `Copy of ${src.name}` : src.name;
|
|
36
36
|
|
|
37
37
|
formModel.init({
|
|
38
38
|
name,
|
|
39
39
|
group: src.group,
|
|
40
40
|
description: src.description,
|
|
41
|
-
|
|
41
|
+
visibility: 'private',
|
|
42
|
+
isPinned: !!src.info?.isPinned
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
this.isOpen = true;
|
|
@@ -52,7 +53,6 @@ export class SaveAsDialogModel extends HoistModel {
|
|
|
52
53
|
async saveAsAsync() {
|
|
53
54
|
try {
|
|
54
55
|
await this.doSaveAsAsync().linkTo(this.parent.saveTask);
|
|
55
|
-
this.close();
|
|
56
56
|
} catch (e) {
|
|
57
57
|
XH.handleException(e);
|
|
58
58
|
}
|
|
@@ -66,29 +66,60 @@ export class SaveAsDialogModel extends HoistModel {
|
|
|
66
66
|
fields: [
|
|
67
67
|
{
|
|
68
68
|
name: 'name',
|
|
69
|
-
rules: [
|
|
69
|
+
rules: [
|
|
70
|
+
({value}, {visibility}) => {
|
|
71
|
+
return this.parent.validateViewNameAsync(
|
|
72
|
+
value,
|
|
73
|
+
null,
|
|
74
|
+
visibility === 'global'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
]
|
|
70
78
|
},
|
|
71
79
|
{name: 'group'},
|
|
72
80
|
{name: 'description'},
|
|
73
|
-
{name: '
|
|
81
|
+
{name: 'visibility'}
|
|
74
82
|
]
|
|
75
83
|
});
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
private async doSaveAsAsync() {
|
|
79
87
|
let {formModel, parent} = this,
|
|
80
|
-
{
|
|
81
|
-
|
|
88
|
+
{typeDisplayName, globalDisplayName} = parent,
|
|
89
|
+
{name, group, description, visibility} = formModel.getData(),
|
|
90
|
+
isValid = await formModel.validateAsync(),
|
|
91
|
+
isGlobal = visibility === 'global',
|
|
92
|
+
isShared = visibility === 'shared';
|
|
82
93
|
|
|
83
94
|
if (!isValid) return;
|
|
84
95
|
|
|
85
|
-
|
|
96
|
+
if (isGlobal) {
|
|
97
|
+
const message = [
|
|
98
|
+
p(
|
|
99
|
+
`This ${typeDisplayName} will become a ${globalDisplayName} ${typeDisplayName} visible to all other ${XH.appName} users.`
|
|
100
|
+
),
|
|
101
|
+
p(strong('Are you sure you want to proceed?'))
|
|
102
|
+
];
|
|
103
|
+
const confirmed = await XH.confirm({
|
|
104
|
+
message,
|
|
105
|
+
confirmProps: {
|
|
106
|
+
text: `Yes, save ${globalDisplayName} ${typeDisplayName}`,
|
|
107
|
+
outlined: true,
|
|
108
|
+
autoFocus: false,
|
|
109
|
+
intent: 'primary'
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
if (!confirmed) return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await parent.saveAsAsync({
|
|
86
116
|
name: name.trim(),
|
|
87
117
|
group: group?.trim(),
|
|
88
118
|
description: description?.trim(),
|
|
89
|
-
|
|
119
|
+
isGlobal,
|
|
90
120
|
isShared,
|
|
91
121
|
value: parent.getValue()
|
|
92
122
|
});
|
|
123
|
+
this.close();
|
|
93
124
|
}
|
|
94
125
|
}
|
|
@@ -7,12 +7,44 @@
|
|
|
7
7
|
|
|
8
8
|
import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
|
|
9
9
|
import {SelectOption} from '@xh/hoist/core';
|
|
10
|
-
import {
|
|
10
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
11
|
+
import {capitalize, map, startCase, uniq} from 'lodash';
|
|
11
12
|
|
|
12
|
-
export function getGroupOptions(
|
|
13
|
-
const views =
|
|
13
|
+
export function getGroupOptions(vmm: ViewManagerModel, isGlobal: boolean): SelectOption[] {
|
|
14
|
+
const views = isGlobal ? vmm.globalViews : vmm.ownedViews;
|
|
14
15
|
return uniq(map(views, 'group'))
|
|
15
16
|
.sort()
|
|
16
17
|
.filter(g => g != null)
|
|
17
18
|
.map(g => ({label: g, value: g}));
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Support for "Visibility" concept used in default view editing/creation.
|
|
23
|
+
* This tri-state selection will translate into boolean `isGlobal` and `isShared`
|
|
24
|
+
* flag settings.
|
|
25
|
+
*/
|
|
26
|
+
export type Visibility = 'private' | 'shared' | 'global';
|
|
27
|
+
|
|
28
|
+
export function getVisibilityOptions(vmm: ViewManagerModel): SelectOption[] {
|
|
29
|
+
const ret = [{value: 'private', label: 'Private'}];
|
|
30
|
+
if (vmm.enableSharing) {
|
|
31
|
+
ret.push({value: 'shared', label: 'Shared'});
|
|
32
|
+
}
|
|
33
|
+
if (vmm.enableGlobal && vmm.manageGlobal) {
|
|
34
|
+
ret.push({value: 'global', label: startCase(vmm.globalDisplayName)});
|
|
35
|
+
}
|
|
36
|
+
return ret;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getVisibilityInfo(vmm: ViewManagerModel, val: Visibility): string {
|
|
40
|
+
switch (val) {
|
|
41
|
+
case 'private':
|
|
42
|
+
return 'Visible to you only.';
|
|
43
|
+
case 'shared':
|
|
44
|
+
return `Visible to all users via the "Manage ${capitalize(pluralize(vmm.typeDisplayName))}" dialog.`;
|
|
45
|
+
case 'global':
|
|
46
|
+
return `Visible to all users and automatically pinned to their menus.`;
|
|
47
|
+
default:
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -10,13 +10,16 @@ import {div, filler, hbox, hspacer, span, vbox, vframe, vspacer} from '@xh/hoist
|
|
|
10
10
|
import {hoistCmp, uses, XH} from '@xh/hoist/core';
|
|
11
11
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
12
12
|
import {formField} from '@xh/hoist/desktop/cmp/form';
|
|
13
|
-
import {select,
|
|
13
|
+
import {select, textArea, textInput} from '@xh/hoist/desktop/cmp/input';
|
|
14
14
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
15
15
|
import {ViewPanelModel} from '@xh/hoist/desktop/cmp/viewmanager/dialog/ViewPanelModel';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getGroupOptions,
|
|
18
|
+
getVisibilityInfo,
|
|
19
|
+
getVisibilityOptions
|
|
20
|
+
} from '@xh/hoist/desktop/cmp/viewmanager/dialog/Utils';
|
|
17
21
|
import {fmtDateTime} from '@xh/hoist/format';
|
|
18
22
|
import {Icon} from '@xh/hoist/icon';
|
|
19
|
-
import {capitalize, some} from 'lodash';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Form to edit or view details on a single saved view within the ViewManager manage dialog.
|
|
@@ -24,11 +27,17 @@ import {capitalize, some} from 'lodash';
|
|
|
24
27
|
export const viewPanel = hoistCmp.factory({
|
|
25
28
|
model: uses(ViewPanelModel),
|
|
26
29
|
render({model}) {
|
|
27
|
-
const {view} = model
|
|
30
|
+
const {view, parent, formModel} = model,
|
|
31
|
+
{viewManagerModel} = parent;
|
|
32
|
+
|
|
28
33
|
if (!view) return null;
|
|
29
34
|
|
|
30
|
-
const {
|
|
31
|
-
|
|
35
|
+
const {lastUpdated, lastUpdatedBy, isEditable} = view,
|
|
36
|
+
visibility = formModel.values.visibility,
|
|
37
|
+
isGlobal = visibility === 'global',
|
|
38
|
+
visOptions = getVisibilityOptions(viewManagerModel),
|
|
39
|
+
visInfo = getVisibilityInfo(viewManagerModel, visibility),
|
|
40
|
+
groupOptions = getGroupOptions(viewManagerModel, isGlobal);
|
|
32
41
|
|
|
33
42
|
return panel({
|
|
34
43
|
item: form({
|
|
@@ -52,10 +61,7 @@ export const viewPanel = hoistCmp.factory({
|
|
|
52
61
|
item: select({
|
|
53
62
|
enableCreate: true,
|
|
54
63
|
enableClear: true,
|
|
55
|
-
options:
|
|
56
|
-
model.parent.viewManagerModel,
|
|
57
|
-
view.isOwned ? 'owned' : 'global'
|
|
58
|
-
)
|
|
64
|
+
options: groupOptions
|
|
59
65
|
}),
|
|
60
66
|
readonlyRenderer: v =>
|
|
61
67
|
v || span({item: 'None provided', className: 'xh-text-color-muted'})
|
|
@@ -70,22 +76,10 @@ export const viewPanel = hoistCmp.factory({
|
|
|
70
76
|
v || span({item: 'None provided', className: 'xh-text-color-muted'})
|
|
71
77
|
}),
|
|
72
78
|
formField({
|
|
73
|
-
field: '
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
readonlyRenderer: v => (v ? 'Yes' : 'No'),
|
|
78
|
-
omit: !enableSharing || isGlobal || !isEditable
|
|
79
|
-
}),
|
|
80
|
-
formField({
|
|
81
|
-
field: 'isDefaultPinned',
|
|
82
|
-
label: null,
|
|
83
|
-
inline: true,
|
|
84
|
-
item: switchInput({
|
|
85
|
-
label: `Pin to everyone's menu by default`,
|
|
86
|
-
labelSide: 'left'
|
|
87
|
-
}),
|
|
88
|
-
omit: !isGlobal || !isEditable
|
|
79
|
+
field: 'visibility',
|
|
80
|
+
omit: !isEditable || visOptions.length === 1,
|
|
81
|
+
item: select({options: visOptions, enableFilter: false}),
|
|
82
|
+
info: visInfo
|
|
89
83
|
}),
|
|
90
84
|
vspacer(),
|
|
91
85
|
formButtons(),
|
|
@@ -130,8 +124,6 @@ const formButtons = hoistCmp.factory<ViewPanelModel>({
|
|
|
130
124
|
});
|
|
131
125
|
}
|
|
132
126
|
|
|
133
|
-
const {enableGlobal, globalDisplayName, manageGlobal, globalViews} =
|
|
134
|
-
parent.viewManagerModel;
|
|
135
127
|
return vbox({
|
|
136
128
|
style: {gap: 10, alignItems: 'center'},
|
|
137
129
|
items: [
|
|
@@ -145,15 +137,6 @@ const formButtons = hoistCmp.factory<ViewPanelModel>({
|
|
|
145
137
|
outlined: true,
|
|
146
138
|
onClick: () => parent.togglePinned([view])
|
|
147
139
|
}),
|
|
148
|
-
button({
|
|
149
|
-
text: `Promote to ${capitalize(globalDisplayName)}`,
|
|
150
|
-
icon: Icon.globe(),
|
|
151
|
-
width: 200,
|
|
152
|
-
outlined: true,
|
|
153
|
-
disabled: some(globalViews, {name: view.name}),
|
|
154
|
-
omit: readonly || view.isGlobal || !enableGlobal || !manageGlobal,
|
|
155
|
-
onClick: () => parent.makeGlobalAsync(view)
|
|
156
|
-
}),
|
|
157
140
|
button({
|
|
158
141
|
text: 'Delete',
|
|
159
142
|
icon: Icon.delete(),
|