@xh/hoist 71.0.0-SNAPSHOT.1733854822950 → 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 +1 -0
- package/build/types/cmp/viewmanager/View.d.ts +5 -0
- package/build/types/cmp/viewmanager/ViewInfo.d.ts +32 -7
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +41 -31
- package/build/types/cmp/viewmanager/ViewToBlobApi.d.ts +28 -7
- package/build/types/cmp/viewmanager/index.d.ts +1 -1
- package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +0 -4
- package/build/types/desktop/cmp/viewmanager/ViewManagerLocalModel.d.ts +11 -0
- package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +2 -2
- package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +3 -1
- package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +18 -13
- package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +1 -1
- package/build/types/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.d.ts +3 -9
- package/build/types/desktop/cmp/viewmanager/dialog/Utils.d.ts +3 -0
- package/build/types/desktop/cmp/viewmanager/dialog/ViewMultiPanel.d.ts +2 -0
- package/build/types/desktop/cmp/viewmanager/dialog/ViewPanel.d.ts +5 -0
- package/build/types/desktop/cmp/viewmanager/dialog/{EditFormModel.d.ts → ViewPanelModel.d.ts} +2 -4
- package/build/types/svc/JsonBlobService.d.ts +1 -1
- package/cmp/viewmanager/View.ts +21 -1
- package/cmp/viewmanager/ViewInfo.ts +58 -11
- package/cmp/viewmanager/ViewManagerModel.ts +109 -82
- package/cmp/viewmanager/ViewToBlobApi.ts +114 -42
- package/cmp/viewmanager/index.ts +1 -1
- package/desktop/cmp/viewmanager/ViewManager.scss +25 -28
- package/desktop/cmp/viewmanager/ViewManager.ts +32 -27
- package/desktop/cmp/viewmanager/ViewManagerLocalModel.ts +33 -0
- package/desktop/cmp/viewmanager/ViewMenu.ts +162 -169
- package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +69 -42
- package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +267 -150
- package/desktop/cmp/viewmanager/dialog/SaveAsDialog.ts +30 -9
- package/{cmp/viewmanager → desktop/cmp/viewmanager/dialog}/SaveAsDialogModel.ts +35 -40
- package/desktop/cmp/viewmanager/dialog/Utils.ts +18 -0
- package/desktop/cmp/viewmanager/dialog/ViewMultiPanel.ts +70 -0
- package/desktop/cmp/viewmanager/dialog/ViewPanel.ts +166 -0
- package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +116 -0
- package/package.json +1 -1
- package/svc/JsonBlobService.ts +3 -3
- package/svc/storage/BaseStorageService.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +0 -5
- package/desktop/cmp/viewmanager/dialog/EditForm.ts +0 -126
- package/desktop/cmp/viewmanager/dialog/EditFormModel.ts +0 -125
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
ExceptionHandlerOptions,
|
|
11
11
|
HoistModel,
|
|
12
12
|
LoadSpec,
|
|
13
|
-
managed,
|
|
14
13
|
PersistableState,
|
|
15
14
|
PersistenceProvider,
|
|
16
15
|
PersistOptions,
|
|
@@ -22,15 +21,15 @@ import {
|
|
|
22
21
|
import type {ViewManagerProvider} from '@xh/hoist/core';
|
|
23
22
|
import {genDisplayName} from '@xh/hoist/data';
|
|
24
23
|
import {fmtDateTime} from '@xh/hoist/format';
|
|
25
|
-
import {action, bindable, makeObservable, observable,
|
|
24
|
+
import {action, bindable, makeObservable, observable, when} from '@xh/hoist/mobx';
|
|
26
25
|
import {olderThan, SECONDS} from '@xh/hoist/utils/datetime';
|
|
27
26
|
import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
28
|
-
import {find, isEqual, isNil, isObject, lowerCase,
|
|
27
|
+
import {find, isEmpty, isEqual, isNil, isObject, lowerCase, pickBy} from 'lodash';
|
|
28
|
+
import {runInAction} from 'mobx';
|
|
29
29
|
import {ReactNode} from 'react';
|
|
30
|
-
import {SaveAsDialogModel} from './SaveAsDialogModel';
|
|
31
30
|
import {ViewInfo} from './ViewInfo';
|
|
32
31
|
import {View} from './View';
|
|
33
|
-
import {ViewToBlobApi} from './ViewToBlobApi';
|
|
32
|
+
import {ViewToBlobApi, ViewCreateSpec} from './ViewToBlobApi';
|
|
34
33
|
|
|
35
34
|
export interface ViewManagerConfig {
|
|
36
35
|
/**
|
|
@@ -45,8 +44,16 @@ export interface ViewManagerConfig {
|
|
|
45
44
|
*/
|
|
46
45
|
enableDefault?: boolean;
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
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
|
+
|
|
53
|
+
/**
|
|
54
|
+
* True (default) to allow users to share their views with other users.
|
|
55
|
+
*/
|
|
56
|
+
enableSharing?: boolean;
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* Function to determine the initial view for a user, when no view has already been persisted.
|
|
@@ -59,11 +66,10 @@ export interface ViewManagerConfig {
|
|
|
59
66
|
initialViewSpec?: (views: ViewInfo[]) => ViewInfo;
|
|
60
67
|
|
|
61
68
|
/**
|
|
62
|
-
* Delay after state has been set on associated components before
|
|
63
|
-
*
|
|
64
|
-
* components such as dashboards or grids that
|
|
65
|
-
*
|
|
66
|
-
* Specified in milliseconds. Default is 250.
|
|
69
|
+
* Delay (in ms) to wait after state has been set on associated components before listening for
|
|
70
|
+
* further state changes. The long default wait 1000ms is intended to avoid a false positive
|
|
71
|
+
* dirty indicator when linking to complex components such as dashboards or grids that can
|
|
72
|
+
* report immediate changes to state due to internal processing or rendering.
|
|
67
73
|
*/
|
|
68
74
|
settleTime?: number;
|
|
69
75
|
|
|
@@ -97,8 +103,8 @@ export interface ViewManagerConfig {
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
export interface ViewManagerPersistOptions extends PersistOptions {
|
|
100
|
-
/** True to persist
|
|
101
|
-
|
|
106
|
+
/** True to persist pinning preferences or provide specific PersistOptions. (Default true) */
|
|
107
|
+
persistPinning?: boolean | PersistOptions;
|
|
102
108
|
|
|
103
109
|
/** True to include pending value or provide specific PersistOptions. (Default false) */
|
|
104
110
|
persistPendingValue?: boolean | PersistOptions;
|
|
@@ -112,8 +118,8 @@ export interface ViewManagerPersistOptions extends PersistOptions {
|
|
|
112
118
|
* models can be bound to a single ViewManagerModel, allowing a single view to capture the state
|
|
113
119
|
* of multiple components - e.g. grouping and filtering options along with grid state.
|
|
114
120
|
* - Views are persisted back to the server as JsonBlob objects.
|
|
115
|
-
* - Views can be private to their owner, or optionally enabled for
|
|
116
|
-
* - Views can be marked as
|
|
121
|
+
* - Views can be private to their owner, or optionally enabled for sharing to (all) other users.
|
|
122
|
+
* - Views can be marked as pinned for quick access.
|
|
117
123
|
* - See the desktop {@link ViewManager} component - the initial Hoist UI for this model.
|
|
118
124
|
*/
|
|
119
125
|
export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
@@ -146,7 +152,8 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
146
152
|
readonly globalDisplayName: string;
|
|
147
153
|
readonly enableAutoSave: boolean;
|
|
148
154
|
readonly enableDefault: boolean;
|
|
149
|
-
readonly
|
|
155
|
+
readonly enableGlobal: boolean;
|
|
156
|
+
readonly enableSharing: boolean;
|
|
150
157
|
readonly manageGlobal: boolean;
|
|
151
158
|
readonly settleTime: number;
|
|
152
159
|
readonly initialViewSpec: (views: ViewInfo[]) => ViewInfo;
|
|
@@ -155,8 +162,14 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
155
162
|
@observable.ref view: View<T> = null;
|
|
156
163
|
/** Loaded saved view library - both private and global */
|
|
157
164
|
@observable.ref views: ViewInfo[] = [];
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Map of user's preferred pinned state for views.
|
|
168
|
+
*
|
|
169
|
+
* Note that the actual pinned state for the views is determined by this value, layered
|
|
170
|
+
* over the default state of the views themselves.
|
|
171
|
+
*/
|
|
172
|
+
@observable.ref userPinned: Record<string, boolean> = {};
|
|
160
173
|
|
|
161
174
|
/**
|
|
162
175
|
* True if user has opted-in to automatically saving changes to personal views (if auto-save
|
|
@@ -170,14 +183,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
170
183
|
*/
|
|
171
184
|
selectTask: TaskObserver;
|
|
172
185
|
|
|
173
|
-
/**
|
|
174
|
-
* TaskObserver linked to {@link saveAsync}.
|
|
175
|
-
*/
|
|
186
|
+
/** TaskObserver linked to {@link saveAsync}. */
|
|
176
187
|
saveTask: TaskObserver;
|
|
177
188
|
|
|
178
|
-
@observable manageDialogOpen = false;
|
|
179
|
-
@managed saveAsDialogModel: SaveAsDialogModel;
|
|
180
|
-
|
|
181
189
|
//-----------------------
|
|
182
190
|
// Private, internal state.
|
|
183
191
|
//-------------------------
|
|
@@ -194,7 +202,7 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
194
202
|
providers: ViewManagerProvider<any>[] = [];
|
|
195
203
|
|
|
196
204
|
/**
|
|
197
|
-
* Data access for persisting views
|
|
205
|
+
* Data access for persisting views.
|
|
198
206
|
* @internal
|
|
199
207
|
*/
|
|
200
208
|
api: ViewToBlobApi<T>;
|
|
@@ -211,32 +219,41 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
211
219
|
|
|
212
220
|
get isViewSavable(): boolean {
|
|
213
221
|
const {view, manageGlobal} = this;
|
|
214
|
-
return
|
|
222
|
+
return view.isOwned || (view.isGlobal && manageGlobal);
|
|
215
223
|
}
|
|
216
224
|
|
|
217
225
|
get isViewAutoSavable(): boolean {
|
|
218
226
|
const {enableAutoSave, autoSave, view} = this;
|
|
219
|
-
return enableAutoSave && autoSave &&
|
|
227
|
+
return enableAutoSave && autoSave && view.isOwned && !XH.identityService.isImpersonating;
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
get autoSaveUnavailableReason(): string {
|
|
223
231
|
const {view, isViewAutoSavable, typeDisplayName, globalDisplayName} = this;
|
|
224
232
|
if (isViewAutoSavable) return null;
|
|
225
233
|
if (view.isGlobal) return `Cannot auto-save ${globalDisplayName} ${typeDisplayName}.`;
|
|
234
|
+
if (view.isShared) return `Cannot auto-save shared ${typeDisplayName}.`;
|
|
226
235
|
if (view.isDefault) return `Cannot auto-save default ${typeDisplayName}.`;
|
|
236
|
+
if (XH.identityService.isImpersonating) return `Auto-save disabled during impersonation.`;
|
|
227
237
|
return null;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
get
|
|
231
|
-
return this.views.filter(it => it.
|
|
240
|
+
get pinnedViews(): ViewInfo[] {
|
|
241
|
+
return this.views.filter(it => it.isPinned);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
/** Views owned by me */
|
|
245
|
+
get ownedViews(): ViewInfo[] {
|
|
246
|
+
return this.views.filter(it => it.isOwned);
|
|
236
247
|
}
|
|
237
248
|
|
|
238
|
-
|
|
239
|
-
|
|
249
|
+
/** Views shared *with* me */
|
|
250
|
+
get sharedViews(): ViewInfo[] {
|
|
251
|
+
return this.views.filter(it => it.isShared && !it.isOwned);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Global views */
|
|
255
|
+
get globalViews(): ViewInfo[] {
|
|
256
|
+
return this.views.filter(it => it.isGlobal);
|
|
240
257
|
}
|
|
241
258
|
|
|
242
259
|
/** True if any async tasks are pending. */
|
|
@@ -257,8 +274,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
257
274
|
manageGlobal = false,
|
|
258
275
|
enableAutoSave = true,
|
|
259
276
|
enableDefault = true,
|
|
260
|
-
|
|
261
|
-
|
|
277
|
+
enableGlobal = true,
|
|
278
|
+
enableSharing = true,
|
|
279
|
+
settleTime = 1000,
|
|
262
280
|
initialViewSpec = null
|
|
263
281
|
}: ViewManagerConfig) {
|
|
264
282
|
super();
|
|
@@ -275,8 +293,9 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
275
293
|
this.persistWith = persistWith;
|
|
276
294
|
this.manageGlobal = executeIfFunction(manageGlobal) ?? false;
|
|
277
295
|
this.enableDefault = enableDefault;
|
|
296
|
+
this.enableGlobal = enableGlobal;
|
|
297
|
+
this.enableSharing = enableSharing;
|
|
278
298
|
this.enableAutoSave = enableAutoSave;
|
|
279
|
-
this.enableFavorites = enableFavorites;
|
|
280
299
|
this.settleTime = settleTime;
|
|
281
300
|
this.initialViewSpec = initialViewSpec;
|
|
282
301
|
|
|
@@ -287,7 +306,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
287
306
|
message: `Saving ${this.typeDisplayName}...`
|
|
288
307
|
});
|
|
289
308
|
|
|
290
|
-
this.saveAsDialogModel = new SaveAsDialogModel(this);
|
|
291
309
|
this.api = new ViewToBlobApi(this);
|
|
292
310
|
}
|
|
293
311
|
|
|
@@ -322,6 +340,14 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
322
340
|
await this.loadViewAsync(info).catch(e => this.handleException(e));
|
|
323
341
|
}
|
|
324
342
|
|
|
343
|
+
async saveAsAsync(spec: ViewCreateSpec): Promise<void> {
|
|
344
|
+
const view = await this.api.createViewAsync({...spec, value: this.getValue()});
|
|
345
|
+
this.noteSuccess(`Created ${view.typedName}`);
|
|
346
|
+
this.userPin(view.info);
|
|
347
|
+
this.setAsView(view);
|
|
348
|
+
this.refreshAsync();
|
|
349
|
+
}
|
|
350
|
+
|
|
325
351
|
//------------------------
|
|
326
352
|
// Saving/resetting
|
|
327
353
|
//------------------------
|
|
@@ -349,15 +375,6 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
349
375
|
this.refreshAsync();
|
|
350
376
|
}
|
|
351
377
|
|
|
352
|
-
async saveAsAsync(): Promise<void> {
|
|
353
|
-
const view = (await this.saveAsDialogModel.openAsync()) as View<T>;
|
|
354
|
-
if (view) {
|
|
355
|
-
this.setAsView(view);
|
|
356
|
-
this.noteSuccess(`Saved ${view.typedName}`);
|
|
357
|
-
}
|
|
358
|
-
this.refreshAsync();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
378
|
async resetAsync(): Promise<void> {
|
|
362
379
|
await this.loadViewAsync(this.view.info).catch(e => this.handleException(e));
|
|
363
380
|
}
|
|
@@ -389,40 +406,29 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
389
406
|
}
|
|
390
407
|
|
|
391
408
|
//------------------
|
|
392
|
-
//
|
|
409
|
+
// Pinning
|
|
393
410
|
//------------------
|
|
394
|
-
|
|
395
|
-
|
|
411
|
+
togglePinned(view: ViewInfo) {
|
|
412
|
+
view.isPinned ? this.userUnpin(view) : this.userPin(view);
|
|
396
413
|
}
|
|
397
414
|
|
|
398
415
|
@action
|
|
399
|
-
|
|
400
|
-
this.
|
|
416
|
+
userPin(view: ViewInfo) {
|
|
417
|
+
this.userPinned = {...this.userPinned, [view.token]: true};
|
|
401
418
|
}
|
|
402
419
|
|
|
403
420
|
@action
|
|
404
|
-
|
|
405
|
-
this.
|
|
421
|
+
userUnpin(view: ViewInfo) {
|
|
422
|
+
this.userPinned = {...this.userPinned, [view.token]: false};
|
|
406
423
|
}
|
|
407
424
|
|
|
408
|
-
|
|
409
|
-
return this.
|
|
425
|
+
isUserPinned(view: ViewInfo): boolean | null {
|
|
426
|
+
return this.userPinned[view.token];
|
|
410
427
|
}
|
|
411
428
|
|
|
412
429
|
//-----------------
|
|
413
430
|
// Management
|
|
414
431
|
//-----------------
|
|
415
|
-
@action
|
|
416
|
-
openManageDialog() {
|
|
417
|
-
this.manageDialogOpen = true;
|
|
418
|
-
this.refreshAsync();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
@action
|
|
422
|
-
closeManageDialog() {
|
|
423
|
-
this.manageDialogOpen = false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
432
|
async validateViewNameAsync(name: string, existing: ViewInfo = null): Promise<string> {
|
|
427
433
|
const maxLength = 50;
|
|
428
434
|
name = name?.trim();
|
|
@@ -430,12 +436,30 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
430
436
|
if (name.length > maxLength) {
|
|
431
437
|
return `Name cannot be longer than ${maxLength} characters`;
|
|
432
438
|
}
|
|
433
|
-
if (this.
|
|
434
|
-
return `A ${this.typeDisplayName} with name '${name}' already exists
|
|
439
|
+
if (this.ownedViews.some(view => view.name === name && view.token != existing?.token)) {
|
|
440
|
+
return `A ${this.typeDisplayName} with name '${name}' already exists.`;
|
|
435
441
|
}
|
|
436
442
|
return null;
|
|
437
443
|
}
|
|
438
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
|
+
|
|
439
463
|
//------------------
|
|
440
464
|
// Implementation
|
|
441
465
|
//------------------
|
|
@@ -555,15 +579,15 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
555
579
|
if (isGlobal) {
|
|
556
580
|
msgs.push(
|
|
557
581
|
span(
|
|
558
|
-
`This is a ${globalDisplayName} ${typeDisplayName}
|
|
559
|
-
strong('Changes will be visible to
|
|
582
|
+
`This is a ${globalDisplayName} ${typeDisplayName}. `,
|
|
583
|
+
strong('Changes will be visible to all users.')
|
|
560
584
|
)
|
|
561
585
|
);
|
|
562
586
|
}
|
|
563
587
|
if (isStale) {
|
|
564
588
|
msgs.push(
|
|
565
589
|
span(
|
|
566
|
-
`This ${typeDisplayName} was updated by ${latestInfo.lastUpdatedBy} on ${fmtDateTime(latestInfo.lastUpdated)}
|
|
590
|
+
`This ${typeDisplayName} was updated by ${latestInfo.lastUpdatedBy} on ${fmtDateTime(latestInfo.lastUpdated)}. `,
|
|
567
591
|
strong('Your change may override those changes.')
|
|
568
592
|
)
|
|
569
593
|
);
|
|
@@ -574,11 +598,11 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
574
598
|
confirmProps: {
|
|
575
599
|
text: 'Yes, save changes',
|
|
576
600
|
intent: 'primary',
|
|
577
|
-
outlined: true
|
|
601
|
+
outlined: true,
|
|
602
|
+
autoFocus: false
|
|
578
603
|
},
|
|
579
604
|
cancelProps: {
|
|
580
|
-
text: 'Cancel'
|
|
581
|
-
autoFocus: true
|
|
605
|
+
text: 'Cancel'
|
|
582
606
|
}
|
|
583
607
|
});
|
|
584
608
|
}
|
|
@@ -588,21 +612,24 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
|
|
|
588
612
|
//------------------
|
|
589
613
|
private initPersist(options: ViewManagerPersistOptions) {
|
|
590
614
|
const {
|
|
591
|
-
|
|
615
|
+
persistPinning = true,
|
|
592
616
|
persistPendingValue = false,
|
|
593
617
|
path = 'viewManager',
|
|
594
618
|
...rootPersistWith
|
|
595
619
|
} = options;
|
|
596
620
|
|
|
597
|
-
//
|
|
598
|
-
if (
|
|
599
|
-
const opts = isObject(
|
|
621
|
+
// Pinning potentially in dedicated location
|
|
622
|
+
if (persistPinning) {
|
|
623
|
+
const opts = isObject(persistPinning) ? persistPinning : rootPersistWith;
|
|
600
624
|
PersistenceProvider.create({
|
|
601
|
-
persistOptions: {path: `${path}.
|
|
625
|
+
persistOptions: {path: `${path}.pinning`, ...opts},
|
|
602
626
|
target: {
|
|
603
|
-
getPersistableState: () => new PersistableState(this.
|
|
627
|
+
getPersistableState: () => new PersistableState(this.userPinned),
|
|
604
628
|
setPersistableState: ({value}) => {
|
|
605
|
-
|
|
629
|
+
const {views} = this;
|
|
630
|
+
this.userPinned = !isEmpty(views) // Clean state iff views loaded!
|
|
631
|
+
? pickBy(value, (_, tkn) => views.some(v => v.token === tkn))
|
|
632
|
+
: value;
|
|
606
633
|
}
|
|
607
634
|
},
|
|
608
635
|
owner: this
|
|
@@ -6,85 +6,114 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {PlainObject, XH} from '@xh/hoist/core';
|
|
9
|
-
import {pluralize} from '@xh/hoist/utils/js';
|
|
9
|
+
import {pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
10
|
+
import {isEmpty, omit, pick} from 'lodash';
|
|
10
11
|
import {ViewInfo} from './ViewInfo';
|
|
11
12
|
import {View} from './View';
|
|
12
13
|
import {ViewManagerModel} from './ViewManagerModel';
|
|
13
14
|
|
|
15
|
+
export interface ViewCreateSpec {
|
|
16
|
+
name: string;
|
|
17
|
+
group: string;
|
|
18
|
+
description: string;
|
|
19
|
+
isShared: boolean;
|
|
20
|
+
value?: PlainObject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ViewUpdateSpec {
|
|
24
|
+
name: string;
|
|
25
|
+
group: string;
|
|
26
|
+
description: string;
|
|
27
|
+
isShared?: boolean;
|
|
28
|
+
isDefaultPinned?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
/**
|
|
15
|
-
* Class for accessing and updating views using
|
|
16
|
-
*
|
|
32
|
+
* Class for accessing and updating views using {@link JsonBlobService}.
|
|
17
33
|
* @internal
|
|
18
34
|
*/
|
|
19
35
|
export class ViewToBlobApi<T> {
|
|
20
|
-
private
|
|
36
|
+
private readonly model: ViewManagerModel<T>;
|
|
21
37
|
|
|
22
|
-
constructor(
|
|
23
|
-
this.
|
|
38
|
+
constructor(model: ViewManagerModel<T>) {
|
|
39
|
+
this.model = model;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
//---------------
|
|
27
43
|
// Load/search.
|
|
28
44
|
//---------------
|
|
45
|
+
/** Fetch metadata for all views accessible by current user. */
|
|
29
46
|
async fetchViewInfosAsync(): Promise<ViewInfo[]> {
|
|
30
|
-
const {
|
|
47
|
+
const {model} = this;
|
|
31
48
|
try {
|
|
32
49
|
const blobs = await XH.jsonBlobService.listAsync({
|
|
33
|
-
type:
|
|
50
|
+
type: model.type,
|
|
34
51
|
includeValue: false
|
|
35
52
|
});
|
|
36
|
-
return blobs
|
|
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
|
+
);
|
|
37
60
|
} catch (e) {
|
|
38
61
|
throw XH.exception({
|
|
39
|
-
message: `Unable to fetch ${pluralize(
|
|
62
|
+
message: `Unable to fetch ${pluralize(model.typeDisplayName)}`,
|
|
40
63
|
cause: e
|
|
41
64
|
});
|
|
42
65
|
}
|
|
43
66
|
}
|
|
44
67
|
|
|
68
|
+
/** Fetch the latest version of a view. */
|
|
45
69
|
async fetchViewAsync(info: ViewInfo): Promise<View<T>> {
|
|
46
|
-
|
|
70
|
+
const {model} = this;
|
|
71
|
+
if (!info) return View.createDefault(model);
|
|
47
72
|
try {
|
|
48
73
|
const blob = await XH.jsonBlobService.getAsync(info.token);
|
|
49
|
-
return View.fromBlob(blob,
|
|
74
|
+
return View.fromBlob(blob, model);
|
|
50
75
|
} catch (e) {
|
|
51
76
|
throw XH.exception({message: `Unable to fetch ${info.typedName}`, cause: e});
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
79
|
|
|
55
80
|
//-----------------
|
|
56
|
-
//
|
|
81
|
+
// CRUD
|
|
57
82
|
//-----------------
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
/** Create a new view, owned by the current user.*/
|
|
84
|
+
async createViewAsync(spec: ViewCreateSpec): Promise<View<T>> {
|
|
85
|
+
const {model} = this;
|
|
60
86
|
try {
|
|
61
87
|
const blob = await XH.jsonBlobService.createAsync({
|
|
62
|
-
type:
|
|
63
|
-
name: name
|
|
64
|
-
description: description
|
|
65
|
-
|
|
88
|
+
type: model.type,
|
|
89
|
+
name: spec.name,
|
|
90
|
+
description: spec.description,
|
|
91
|
+
acl: spec.isShared ? '*' : null,
|
|
92
|
+
meta: {group: spec.group, isShared: spec.isShared},
|
|
93
|
+
value: spec.value
|
|
66
94
|
});
|
|
67
|
-
const ret = View.fromBlob(blob,
|
|
95
|
+
const ret = View.fromBlob(blob, model);
|
|
68
96
|
this.trackChange('Created View', ret);
|
|
69
97
|
return ret;
|
|
70
98
|
} catch (e) {
|
|
71
|
-
throw XH.exception({message: `Unable to create ${
|
|
99
|
+
throw XH.exception({message: `Unable to create ${model.typeDisplayName}`, cause: e});
|
|
72
100
|
}
|
|
73
101
|
}
|
|
74
102
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name: string,
|
|
78
|
-
description: string,
|
|
79
|
-
isGlobal: boolean
|
|
80
|
-
): Promise<View<T>> {
|
|
103
|
+
/** Update all aspects of a view's metadata.*/
|
|
104
|
+
async updateViewInfoAsync(view: ViewInfo, updates: ViewUpdateSpec): Promise<View<T>> {
|
|
81
105
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
this.ensureEditable(view);
|
|
107
|
+
const {isGlobal} = view,
|
|
108
|
+
{name, group, description, isShared, isDefaultPinned} = updates,
|
|
109
|
+
meta = {...view.meta, group},
|
|
110
|
+
blob = await XH.jsonBlobService.updateAsync(view.token, {
|
|
111
|
+
name: name.trim(),
|
|
112
|
+
description: description?.trim(),
|
|
113
|
+
acl: isGlobal || isShared ? '*' : null,
|
|
114
|
+
meta: isGlobal ? {...meta, isDefaultPinned} : {...meta, isShared}
|
|
115
|
+
});
|
|
116
|
+
const ret = View.fromBlob(blob, this.model);
|
|
88
117
|
this.trackChange('Updated View Info', ret);
|
|
89
118
|
return ret;
|
|
90
119
|
} catch (e) {
|
|
@@ -92,10 +121,30 @@ export class ViewToBlobApi<T> {
|
|
|
92
121
|
}
|
|
93
122
|
}
|
|
94
123
|
|
|
124
|
+
/** Promote a view to global visibility/ownership status. */
|
|
125
|
+
async makeViewGlobalAsync(view: ViewInfo): Promise<View<T>> {
|
|
126
|
+
try {
|
|
127
|
+
this.ensureEditable(view);
|
|
128
|
+
const meta = view.meta,
|
|
129
|
+
blob = await XH.jsonBlobService.updateAsync(view.token, {
|
|
130
|
+
owner: null,
|
|
131
|
+
acl: '*',
|
|
132
|
+
meta: omit(meta, ['isShared'])
|
|
133
|
+
});
|
|
134
|
+
const ret = View.fromBlob(blob, this.model);
|
|
135
|
+
this.trackChange('Made View Global', ret);
|
|
136
|
+
return ret;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
throw XH.exception({message: `Unable to update ${view.typedName}`, cause: e});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Update a view's value. */
|
|
95
143
|
async updateViewValueAsync(view: View<T>, value: Partial<T>): Promise<View<T>> {
|
|
96
144
|
try {
|
|
145
|
+
this.ensureEditable(view.info);
|
|
97
146
|
const blob = await XH.jsonBlobService.updateAsync(view.token, {value});
|
|
98
|
-
const ret = View.fromBlob(blob, this.
|
|
147
|
+
const ret = View.fromBlob(blob, this.model);
|
|
99
148
|
if (ret.isGlobal) {
|
|
100
149
|
this.trackChange('Updated Global View definition', ret);
|
|
101
150
|
}
|
|
@@ -108,20 +157,43 @@ export class ViewToBlobApi<T> {
|
|
|
108
157
|
}
|
|
109
158
|
}
|
|
110
159
|
|
|
111
|
-
async
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
});
|
|
117
178
|
}
|
|
118
179
|
}
|
|
119
180
|
|
|
120
|
-
|
|
181
|
+
//------------------
|
|
182
|
+
// Implementation
|
|
183
|
+
//------------------
|
|
184
|
+
private trackChange(message: string, v?: View | ViewInfo) {
|
|
121
185
|
XH.track({
|
|
122
186
|
message,
|
|
123
187
|
category: 'Views',
|
|
124
|
-
data:
|
|
188
|
+
data: pick(v, ['name', 'token', 'isGlobal', 'type'])
|
|
125
189
|
});
|
|
126
190
|
}
|
|
191
|
+
|
|
192
|
+
private ensureEditable(view: ViewInfo) {
|
|
193
|
+
const {model} = this;
|
|
194
|
+
throwIf(
|
|
195
|
+
!view.isEditable,
|
|
196
|
+
`Cannot save changes to ${model.globalDisplayName} ${model.typeDisplayName} - missing required permission.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
127
199
|
}
|
package/cmp/viewmanager/index.ts
CHANGED