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