@xh/hoist 71.0.0-SNAPSHOT.1733198772915 → 71.0.0-SNAPSHOT.1733266596001
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 +8 -2
- package/appcontainer/AppContainerModel.ts +2 -1
- package/build/types/cmp/chart/impl/copyToClipboard.d.ts +1 -2
- package/build/types/cmp/viewmanager/SaveAsDialogModel.d.ts +23 -0
- package/build/types/cmp/viewmanager/View.d.ts +28 -0
- package/build/types/cmp/viewmanager/ViewInfo.d.ts +30 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +185 -0
- package/build/types/cmp/viewmanager/index.d.ts +4 -0
- package/build/types/core/XH.d.ts +2 -1
- package/build/types/core/persist/PersistOptions.d.ts +3 -1
- package/build/types/core/persist/index.d.ts +6 -5
- package/build/types/core/persist/{CustomProvider.d.ts → provider/CustomProvider.d.ts} +1 -1
- package/build/types/core/persist/{DashViewProvider.d.ts → provider/DashViewProvider.d.ts} +2 -2
- package/build/types/core/persist/{LocalStorageProvider.d.ts → provider/LocalStorageProvider.d.ts} +1 -1
- package/build/types/core/persist/{PrefProvider.d.ts → provider/PrefProvider.d.ts} +1 -2
- package/build/types/core/persist/provider/SessionStorageProvider.d.ts +10 -0
- package/build/types/core/persist/{viewmanager → provider}/ViewManagerProvider.d.ts +3 -3
- package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +9 -10
- package/build/types/desktop/cmp/viewmanager/ViewMenu.d.ts +5 -0
- package/build/types/desktop/cmp/viewmanager/dialog/EditForm.d.ts +5 -0
- package/build/types/desktop/cmp/viewmanager/dialog/EditFormModel.d.ts +18 -0
- package/build/types/desktop/cmp/viewmanager/dialog/ManageDialog.d.ts +5 -0
- package/build/types/desktop/cmp/viewmanager/dialog/ManageDialogModel.d.ts +38 -0
- package/build/types/desktop/cmp/viewmanager/dialog/SaveAsDialog.d.ts +5 -0
- package/build/types/svc/JsonBlobService.d.ts +1 -1
- package/build/types/svc/index.d.ts +2 -1
- package/build/types/svc/storage/BaseStorageService.d.ts +21 -0
- package/build/types/svc/storage/LocalStorageService.d.ts +12 -0
- package/build/types/svc/storage/SessionStorageService.d.ts +12 -0
- package/cmp/chart/impl/copyToClipboard.ts +1 -2
- package/cmp/viewmanager/SaveAsDialogModel.ts +97 -0
- package/cmp/viewmanager/View.ts +56 -0
- package/cmp/viewmanager/ViewInfo.ts +58 -0
- package/cmp/viewmanager/ViewManagerModel.ts +710 -0
- package/cmp/viewmanager/index.ts +4 -0
- package/core/XH.ts +2 -0
- package/core/persist/PersistOptions.ts +4 -1
- package/core/persist/PersistenceProvider.ts +5 -0
- package/core/persist/index.ts +6 -5
- package/core/persist/{CustomProvider.ts → provider/CustomProvider.ts} +1 -1
- package/core/persist/{DashViewProvider.ts → provider/DashViewProvider.ts} +1 -1
- package/core/persist/{LocalStorageProvider.ts → provider/LocalStorageProvider.ts} +1 -1
- package/core/persist/{PrefProvider.ts → provider/PrefProvider.ts} +2 -2
- package/core/persist/provider/SessionStorageProvider.ts +35 -0
- package/core/persist/{viewmanager → provider}/ViewManagerProvider.ts +5 -9
- package/desktop/cmp/viewmanager/ViewManager.ts +47 -229
- package/desktop/cmp/viewmanager/ViewMenu.ts +191 -0
- package/desktop/cmp/viewmanager/dialog/EditForm.ts +126 -0
- package/desktop/cmp/viewmanager/dialog/EditFormModel.ts +125 -0
- package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +98 -0
- package/desktop/cmp/viewmanager/dialog/ManageDialogModel.ts +279 -0
- package/desktop/cmp/viewmanager/{impl/SaveDialog.ts → dialog/SaveAsDialog.ts} +20 -12
- package/package.json +1 -1
- package/svc/JsonBlobService.ts +1 -1
- package/svc/index.ts +2 -1
- package/svc/{LocalStorageService.ts → storage/BaseStorageService.ts} +13 -23
- package/svc/storage/LocalStorageService.ts +23 -0
- package/svc/storage/SessionStorageService.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/build/types/core/persist/viewmanager/Types.d.ts +0 -48
- package/build/types/core/persist/viewmanager/ViewManagerModel.d.ts +0 -145
- package/build/types/core/persist/viewmanager/impl/BuildViewTree.d.ts +0 -8
- package/build/types/core/persist/viewmanager/impl/ManageDialogModel.d.ts +0 -30
- package/build/types/core/persist/viewmanager/impl/SaveDialogModel.d.ts +0 -23
- package/build/types/core/persist/viewmanager/index.d.ts +0 -2
- package/build/types/desktop/cmp/viewmanager/impl/ManageDialog.d.ts +0 -6
- package/build/types/desktop/cmp/viewmanager/impl/SaveDialog.d.ts +0 -2
- package/build/types/svc/LocalStorageService.d.ts +0 -24
- package/core/persist/viewmanager/Types.ts +0 -53
- package/core/persist/viewmanager/ViewManagerModel.ts +0 -481
- package/core/persist/viewmanager/impl/BuildViewTree.ts +0 -68
- package/core/persist/viewmanager/impl/ManageDialogModel.ts +0 -276
- package/core/persist/viewmanager/impl/SaveDialogModel.ts +0 -112
- package/core/persist/viewmanager/index.ts +0 -2
- package/desktop/cmp/viewmanager/impl/ManageDialog.ts +0 -197
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HoistModel,
|
|
3
|
-
LoadSpec,
|
|
4
|
-
managed,
|
|
5
|
-
Persistable,
|
|
6
|
-
PersistableState,
|
|
7
|
-
PersistenceProvider,
|
|
8
|
-
PersistOptions,
|
|
9
|
-
PlainObject,
|
|
10
|
-
TaskObserver,
|
|
11
|
-
Thunkable,
|
|
12
|
-
ViewManagerProvider,
|
|
13
|
-
XH
|
|
14
|
-
} from '@xh/hoist/core';
|
|
15
|
-
import {genDisplayName} from '@xh/hoist/data';
|
|
16
|
-
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
17
|
-
import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
18
|
-
import {isEqual, isNil, isUndefined, lowerCase, startCase} from 'lodash';
|
|
19
|
-
import {runInAction} from 'mobx';
|
|
20
|
-
import {SaveDialogModel} from './impl/SaveDialogModel';
|
|
21
|
-
import {buildViewTree} from './impl/BuildViewTree';
|
|
22
|
-
import {View, ViewTree} from './Types';
|
|
23
|
-
|
|
24
|
-
export interface ViewManagerConfig {
|
|
25
|
-
/**
|
|
26
|
-
* True (default) to allow user to opt in to automatically saving changes to their private
|
|
27
|
-
* views - requires `persistWith`.
|
|
28
|
-
*/
|
|
29
|
-
enableAutoSave?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* True (default) to allow the user to select a "Default" option that restores all persisted
|
|
32
|
-
* objects to their in-code defaults. If not enabled, at least one saved view should be created
|
|
33
|
-
* in advance, so that there is a clear initial selection for users without any private views.
|
|
34
|
-
*/
|
|
35
|
-
enableDefault?: boolean;
|
|
36
|
-
/** True (default) to allow user to mark views as favorites. Requires `persistWith`. */
|
|
37
|
-
enableFavorites?: boolean;
|
|
38
|
-
/**
|
|
39
|
-
* True to allow the user to publish or edit globally shared views. Apps are expected to
|
|
40
|
-
* commonly set this based on user roles - e.g. `XH.getUser().hasRole('MANAGE_GRID_VIEWS')`.
|
|
41
|
-
*/
|
|
42
|
-
enableSharing?: Thunkable<boolean>;
|
|
43
|
-
/** Used to persist the user's last selected + favorite views and autoSave preference. */
|
|
44
|
-
persistWith?: PersistOptions;
|
|
45
|
-
/**
|
|
46
|
-
* Required discriminator for the particular class of views to be loaded and managed by this
|
|
47
|
-
* model. Maps onto the `type` field of the persisted `JsonBlob`. Set to something descriptive
|
|
48
|
-
* and specific enough to be identifiable and allow for different viewManagers to be added
|
|
49
|
-
* to your app in the future - e.g. `portfolioGridView` or `tradeBlotterDashboard`.
|
|
50
|
-
*/
|
|
51
|
-
viewType: string;
|
|
52
|
-
/**
|
|
53
|
-
* Optional user-facing display name for the view type, displayed in the ViewManager menu
|
|
54
|
-
* and associated management dialogs and prompts. Defaulted from `viewType` if not provided.
|
|
55
|
-
*/
|
|
56
|
-
viewTypeDisplayName?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* ViewManagerModel coordinates the loading, saving, and management of user-defined bundles of
|
|
61
|
-
* {@link Persistable} component/model state.
|
|
62
|
-
*
|
|
63
|
-
* - Models to be persisted are bound to this model via their `persistWith` config. One or more
|
|
64
|
-
* models can be bound to a single ViewManagerModel, allowing a single view to capture the state
|
|
65
|
-
* of multiple components - e.g. grouping and filtering options along with grid state.
|
|
66
|
-
* - Views are persisted back to the server as JsonBlob objects.
|
|
67
|
-
* - Views can be private to their owner, or optionally enabled for sharing to (all) other users.
|
|
68
|
-
* - Views can be marked as favorites for quick access.
|
|
69
|
-
* - See the desktop {@link ViewManager} component - the initial Hoist UI for this model.
|
|
70
|
-
*/
|
|
71
|
-
export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
72
|
-
extends HoistModel
|
|
73
|
-
implements Persistable<ViewManagerModelPersistState>
|
|
74
|
-
{
|
|
75
|
-
/**
|
|
76
|
-
* Factory to create new instances of this model and await its initial load before binding to
|
|
77
|
-
* any persistable component models. This ensures that bound models will have the expected
|
|
78
|
-
* initial persisted state applied within their constructor, before their components have
|
|
79
|
-
* rendered, and avoids thrashing of component state during initial load.
|
|
80
|
-
*
|
|
81
|
-
* To minimize the impact this async requirement has on the design and lifecycle of individual
|
|
82
|
-
* components within an app, consider eagerly constructing any viewManagerModels required within
|
|
83
|
-
* your `AppModel.initAsync` method and saving a reference to them there for component models
|
|
84
|
-
* to then use when they are mounted. The VM model instances will then be "ready to go" and
|
|
85
|
-
* usable within model constructors. (Initializing and referencing from one or more app
|
|
86
|
-
* services would be another, similar option.)
|
|
87
|
-
*/
|
|
88
|
-
static async createAsync(config: ViewManagerConfig): Promise<ViewManagerModel> {
|
|
89
|
-
const ret = new ViewManagerModel(config);
|
|
90
|
-
await ret.loadAsync();
|
|
91
|
-
return ret;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Immutable configuration for this model. */
|
|
95
|
-
readonly viewType: string;
|
|
96
|
-
readonly displayName: string; // from viewTypeDisplayName, or generated off of viewType
|
|
97
|
-
readonly DisplayName: string; // capitalized viewTypeDisplayName
|
|
98
|
-
readonly enableDefault: boolean;
|
|
99
|
-
readonly enableAutoSave: boolean;
|
|
100
|
-
readonly enableFavorites: boolean;
|
|
101
|
-
|
|
102
|
-
/** Last selected, fully-persisted state of the active view. */
|
|
103
|
-
@observable.ref value: T = {} as T;
|
|
104
|
-
/** Current state of the active view, can include not-yet-persisted changes. */
|
|
105
|
-
@observable.ref pendingValue: T = {} as T;
|
|
106
|
-
/** Loaded saved view definitions - both private and shared. */
|
|
107
|
-
@observable.ref views: View<T>[] = null;
|
|
108
|
-
|
|
109
|
-
/** Currently selected view, or null if in default mode. Token only will be set during pre-loading.*/
|
|
110
|
-
@observable selectedToken: string = null;
|
|
111
|
-
@observable.ref selectedView: View<T> = null;
|
|
112
|
-
|
|
113
|
-
/** List of tokens for the user's favorite views. */
|
|
114
|
-
@bindable favorites: string[] = [];
|
|
115
|
-
/**
|
|
116
|
-
* True if user has opted-in to automatically saving changes to personal views (if auto-save
|
|
117
|
-
* generally available as per `enableAutoSave`).
|
|
118
|
-
*/
|
|
119
|
-
@bindable autoSave = false;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
|
|
123
|
-
* require intensive layout/grid work, consider masking affected components with this observer.
|
|
124
|
-
*/
|
|
125
|
-
viewSelectionObserver: TaskObserver;
|
|
126
|
-
|
|
127
|
-
@observable manageDialogOpen = false;
|
|
128
|
-
@managed readonly saveDialogModel: SaveDialogModel;
|
|
129
|
-
|
|
130
|
-
private readonly _enableSharing: Thunkable<boolean>;
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @internal array of {@link ViewManagerProvider} instances bound to this model. Providers will
|
|
134
|
-
* push themselves onto this array when constructed with a reference to this model. Used to
|
|
135
|
-
* proactively push state to the target components when the model's selected `value` changes.
|
|
136
|
-
*/
|
|
137
|
-
providers: ViewManagerProvider<any>[] = [];
|
|
138
|
-
|
|
139
|
-
get enableSharing(): boolean {
|
|
140
|
-
return executeIfFunction(this._enableSharing);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
@computed
|
|
144
|
-
get canSave(): boolean {
|
|
145
|
-
const {loadModel, selectedView, enableSharing} = this;
|
|
146
|
-
return !loadModel.isPending && selectedView && (enableSharing || !selectedView.isShared);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
@computed
|
|
150
|
-
get canAutoSave(): boolean {
|
|
151
|
-
const {enableAutoSave, autoSave, loadModel, selectedView} = this;
|
|
152
|
-
return (
|
|
153
|
-
!loadModel.isPending &&
|
|
154
|
-
enableAutoSave &&
|
|
155
|
-
autoSave &&
|
|
156
|
-
selectedView &&
|
|
157
|
-
!selectedView.isShared
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@computed
|
|
162
|
-
get autoSaveUnavailableReason(): string {
|
|
163
|
-
const {canAutoSave, selectedView, displayName} = this;
|
|
164
|
-
if (canAutoSave) return null;
|
|
165
|
-
if (!selectedView) return `Cannot auto-save default ${displayName}.`;
|
|
166
|
-
if (selectedView.isShared) return `Cannot auto-save shared ${displayName}.`;
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
@computed
|
|
171
|
-
get isDirty(): boolean {
|
|
172
|
-
return !isEqual(this.pendingValue, this.value);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
get isShared(): boolean {
|
|
176
|
-
return !!this.selectedView?.isShared;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
get favoriteViews(): View<T>[] {
|
|
180
|
-
return this.views.filter(it => it.isFavorite);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
get sharedViews(): View<T>[] {
|
|
184
|
-
return this.views.filter(it => it.isShared);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
get privateViews(): View<T>[] {
|
|
188
|
-
return this.views.filter(it => !it.isShared);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
get sharedViewTree(): ViewTree[] {
|
|
192
|
-
return buildViewTree(this.sharedViews, this);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
get privateViewTree(): ViewTree[] {
|
|
196
|
-
return buildViewTree(this.privateViews, this);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Use the static {@link createAsync} factory to create an instance of this model and await its
|
|
201
|
-
* initial load before binding to persistable components.
|
|
202
|
-
*/
|
|
203
|
-
private constructor({
|
|
204
|
-
viewType,
|
|
205
|
-
viewTypeDisplayName,
|
|
206
|
-
persistWith,
|
|
207
|
-
enableSharing = false,
|
|
208
|
-
enableDefault = true,
|
|
209
|
-
enableAutoSave = true,
|
|
210
|
-
enableFavorites = true
|
|
211
|
-
}: ViewManagerConfig) {
|
|
212
|
-
super();
|
|
213
|
-
makeObservable(this);
|
|
214
|
-
|
|
215
|
-
throwIf(!viewType, 'Missing required viewType in ViewManagerModel config.');
|
|
216
|
-
this.viewType = viewType;
|
|
217
|
-
this.displayName = lowerCase(viewTypeDisplayName ?? genDisplayName(viewType));
|
|
218
|
-
this.DisplayName = startCase(this.displayName);
|
|
219
|
-
|
|
220
|
-
this._enableSharing = enableSharing;
|
|
221
|
-
this.enableDefault = enableDefault;
|
|
222
|
-
this.enableAutoSave = enableAutoSave && !!persistWith;
|
|
223
|
-
this.enableFavorites = enableFavorites && !!persistWith;
|
|
224
|
-
this.saveDialogModel = new SaveDialogModel(this);
|
|
225
|
-
|
|
226
|
-
this.viewSelectionObserver = TaskObserver.trackLast({
|
|
227
|
-
message: `Updating ${this.displayName}...`
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (persistWith) {
|
|
231
|
-
PersistenceProvider.create({
|
|
232
|
-
persistOptions: {
|
|
233
|
-
path: 'viewManager',
|
|
234
|
-
...persistWith
|
|
235
|
-
},
|
|
236
|
-
target: this
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
this.addReaction(
|
|
241
|
-
{
|
|
242
|
-
// Track pendingValue, so we retry on fail if view stays dirty -- could use backup timer
|
|
243
|
-
track: () => [this.pendingValue, this.autoSave],
|
|
244
|
-
run: () => this.maybeAutoSaveAsync()
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
track: () => this.favorites,
|
|
248
|
-
run: () => this.onFavoritesChange()
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
override async doLoadAsync(loadSpec: LoadSpec) {
|
|
254
|
-
const rawViews = await XH.jsonBlobService.listAsync({
|
|
255
|
-
type: this.viewType,
|
|
256
|
-
includeValue: false,
|
|
257
|
-
loadSpec
|
|
258
|
-
});
|
|
259
|
-
if (loadSpec.isStale) return;
|
|
260
|
-
|
|
261
|
-
runInAction(() => {
|
|
262
|
-
this.views = rawViews.map(it => this.processRaw(it));
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
const token =
|
|
266
|
-
loadSpec.meta.selectToken ??
|
|
267
|
-
this.selectedToken ??
|
|
268
|
-
(this.enableDefault ? null : this.views[0]?.token);
|
|
269
|
-
await this.selectViewAsync(token);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async selectViewAsync(token: string) {
|
|
273
|
-
// If views have not been loaded yet (e.g. constructing), nothing to be done but pre-set state
|
|
274
|
-
if (!this.views) {
|
|
275
|
-
this.selectedToken = token;
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
await this.selectViewInternalAsync(token).linkTo(this.viewSelectionObserver);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
//------------------------
|
|
282
|
-
// Saving/resetting
|
|
283
|
-
//------------------------
|
|
284
|
-
async saveAsync() {
|
|
285
|
-
const {canSave, selectedToken, pendingValue, selectedView, DisplayName} = this;
|
|
286
|
-
throwIf(!canSave, 'Unable to save view.');
|
|
287
|
-
|
|
288
|
-
if (selectedView?.isShared) {
|
|
289
|
-
if (!(await this.confirmSaveForSharedViewAsync())) return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
await XH.jsonBlobService.updateAsync(selectedToken, {value: pendingValue});
|
|
294
|
-
runInAction(() => {
|
|
295
|
-
this.value = this.pendingValue;
|
|
296
|
-
});
|
|
297
|
-
XH.successToast(`${DisplayName} successfully saved.`);
|
|
298
|
-
} catch (e) {
|
|
299
|
-
XH.handleException(e, {alertType: 'toast'});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async saveAsAsync() {
|
|
304
|
-
const {selectedView, views, DisplayName} = this,
|
|
305
|
-
{name, description} = selectedView ?? {};
|
|
306
|
-
|
|
307
|
-
const newView = await this.saveDialogModel.openAsync(
|
|
308
|
-
{
|
|
309
|
-
name,
|
|
310
|
-
description,
|
|
311
|
-
value: this.pendingValue
|
|
312
|
-
},
|
|
313
|
-
views.map(it => it.name)
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
if (newView) {
|
|
317
|
-
await this.refreshAsync({selectToken: newView.token});
|
|
318
|
-
XH.successToast(`${DisplayName} successfully saved.`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async resetAsync() {
|
|
323
|
-
return this.selectViewAsync(this.selectedView?.token);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
@action
|
|
327
|
-
setPendingValue(pendingValue: T) {
|
|
328
|
-
pendingValue = this.cleanValue(pendingValue);
|
|
329
|
-
if (!isEqual(pendingValue, this.pendingValue)) {
|
|
330
|
-
this.pendingValue = pendingValue;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
@action
|
|
335
|
-
openManageDialog() {
|
|
336
|
-
this.manageDialogOpen = true;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
@action
|
|
340
|
-
closeManageDialog() {
|
|
341
|
-
this.manageDialogOpen = false;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
//------------------
|
|
345
|
-
// Favorites
|
|
346
|
-
//------------------
|
|
347
|
-
toggleFavorite(token: string) {
|
|
348
|
-
this.isFavorite(token) ? this.removeFavorite(token) : this.addFavorite(token);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
addFavorite(token: string) {
|
|
352
|
-
this.favorites = [...this.favorites, token];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
removeFavorite(token: string) {
|
|
356
|
-
this.favorites = this.favorites.filter(it => it !== token);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
isFavorite(token: string) {
|
|
360
|
-
return this.favorites.includes(token);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
//------------------
|
|
364
|
-
// Persistable
|
|
365
|
-
//------------------
|
|
366
|
-
getPersistableState(): PersistableState<ViewManagerModelPersistState> {
|
|
367
|
-
const state: ViewManagerModelPersistState = {
|
|
368
|
-
selectedToken: this.selectedToken,
|
|
369
|
-
favorites: this.favorites
|
|
370
|
-
};
|
|
371
|
-
if (this.enableAutoSave) {
|
|
372
|
-
state.autoSave = this.autoSave;
|
|
373
|
-
}
|
|
374
|
-
return new PersistableState(state);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
setPersistableState(state: PersistableState<ViewManagerModelPersistState>) {
|
|
378
|
-
const {selectedToken, favorites, autoSave} = state.value;
|
|
379
|
-
if (!isUndefined(selectedToken)) {
|
|
380
|
-
this.selectViewAsync(selectedToken);
|
|
381
|
-
}
|
|
382
|
-
if (!isUndefined(favorites)) {
|
|
383
|
-
this.favorites = favorites;
|
|
384
|
-
}
|
|
385
|
-
if (!isUndefined(autoSave) && this.enableAutoSave) {
|
|
386
|
-
this.autoSave = autoSave;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
//------------------
|
|
391
|
-
// Implementation
|
|
392
|
-
//------------------
|
|
393
|
-
private async selectViewInternalAsync(token: string) {
|
|
394
|
-
let view: View<T> = null;
|
|
395
|
-
if (token != null) {
|
|
396
|
-
try {
|
|
397
|
-
const raw = await XH.jsonBlobService.getAsync(token);
|
|
398
|
-
view = this.processRaw(raw);
|
|
399
|
-
} catch (e) {
|
|
400
|
-
XH.handleException(e, {showAlert: false});
|
|
401
|
-
view = null;
|
|
402
|
-
token = null;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
runInAction(() => {
|
|
407
|
-
this.selectedToken = token;
|
|
408
|
-
this.selectedView = view;
|
|
409
|
-
this.setValue(this.selectedView?.value ?? ({} as T));
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private processRaw(raw: PlainObject): View<T> {
|
|
414
|
-
const name = pluralize(this.DisplayName);
|
|
415
|
-
const isShared = raw.acl === '*';
|
|
416
|
-
return {
|
|
417
|
-
...raw,
|
|
418
|
-
shortName: raw.name?.substring(raw.name.lastIndexOf('\\') + 1),
|
|
419
|
-
isShared,
|
|
420
|
-
group: isShared ? `Shared ${name}` : `My ${name}`,
|
|
421
|
-
isFavorite: this.isFavorite(raw.token)
|
|
422
|
-
} as View<T>;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
private setValue(value: T) {
|
|
426
|
-
value = this.cleanValue(value);
|
|
427
|
-
if (isEqual(value, this.value) && isEqual(value, this.pendingValue)) return;
|
|
428
|
-
|
|
429
|
-
this.value = this.pendingValue = value;
|
|
430
|
-
this.providers.forEach(it => it.pushStateToTarget());
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Stringify and parse to ensure that any value set here is valid, serializable JSON.
|
|
434
|
-
private cleanValue(value: T): T {
|
|
435
|
-
if (isNil(value)) value = {} as T;
|
|
436
|
-
return JSON.parse(JSON.stringify(value));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private async confirmSaveForSharedViewAsync() {
|
|
440
|
-
return XH.confirm({
|
|
441
|
-
message: `You are saving a shared public ${this.displayName}. Do you wish to continue?`,
|
|
442
|
-
confirmProps: {
|
|
443
|
-
text: 'Yes, save changes',
|
|
444
|
-
intent: 'primary',
|
|
445
|
-
outlined: true
|
|
446
|
-
},
|
|
447
|
-
cancelProps: {
|
|
448
|
-
text: 'Cancel',
|
|
449
|
-
autoFocus: true
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private async maybeAutoSaveAsync() {
|
|
455
|
-
if (this.canAutoSave && this.isDirty) {
|
|
456
|
-
const {selectedToken, pendingValue} = this;
|
|
457
|
-
try {
|
|
458
|
-
await XH.jsonBlobService.updateAsync(selectedToken, {value: pendingValue});
|
|
459
|
-
runInAction(() => {
|
|
460
|
-
this.value = this.pendingValue;
|
|
461
|
-
});
|
|
462
|
-
} catch (e) {
|
|
463
|
-
XH.handleException(e, {showAlert: false});
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Update flag on each view, replacing entire views collection for observability.
|
|
469
|
-
private onFavoritesChange() {
|
|
470
|
-
this.views = this.views.map(view => ({
|
|
471
|
-
...view,
|
|
472
|
-
isFavorite: this.isFavorite(view.token)
|
|
473
|
-
}));
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
interface ViewManagerModelPersistState {
|
|
478
|
-
selectedToken?: string;
|
|
479
|
-
favorites?: string[];
|
|
480
|
-
autoSave?: boolean;
|
|
481
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {View, ViewManagerModel, ViewTree} from '@xh/hoist/core/persist/viewmanager';
|
|
2
|
-
import {sortBy} from 'lodash';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Create a menu-friendly, tree representation of a set of views, using the `\`
|
|
6
|
-
* in view names to create folders.
|
|
7
|
-
*
|
|
8
|
-
* @internal
|
|
9
|
-
*/
|
|
10
|
-
export function buildViewTree(views: View[], model: ViewManagerModel): ViewTree[] {
|
|
11
|
-
views = sortBy(views, 'name');
|
|
12
|
-
return buildTreeInternal(views, model.selectedView, 0);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function buildTreeInternal(views: View[], selected: View, depth: number): ViewTree[] {
|
|
16
|
-
// 1) Get groups and leaves at this level.
|
|
17
|
-
const groups = {},
|
|
18
|
-
groupsAndLeaves = [];
|
|
19
|
-
views.forEach(view => {
|
|
20
|
-
// Leaf Node
|
|
21
|
-
if (getNameAtDepth(view.name, depth + 1) == null) {
|
|
22
|
-
groupsAndLeaves.push(view);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// Belongs to an already defined group
|
|
26
|
-
const group = getNameAtDepth(view.name, depth);
|
|
27
|
-
if (groups[group]) {
|
|
28
|
-
groups[group].children.push(view);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
// Belongs to a not defined group, create it
|
|
32
|
-
groups[group] = {name: group, children: [view], isMenuFolder: true};
|
|
33
|
-
groupsAndLeaves.push(groups[group]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// 2) Make ViewTree, recursing for groups
|
|
37
|
-
return groupsAndLeaves.map(it => {
|
|
38
|
-
const {name, isMenuFolder, children, description, token} = it;
|
|
39
|
-
return isMenuFolder
|
|
40
|
-
? {
|
|
41
|
-
type: 'folder',
|
|
42
|
-
text: getFolderDisplayName(name, depth),
|
|
43
|
-
items: buildTreeInternal(children, selected, depth + 1),
|
|
44
|
-
selected: isFolderForEntry(name, selected?.name, depth)
|
|
45
|
-
}
|
|
46
|
-
: {
|
|
47
|
-
type: 'view',
|
|
48
|
-
text: it.shortName,
|
|
49
|
-
selected: selected?.token === token,
|
|
50
|
-
token,
|
|
51
|
-
description
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getNameAtDepth(name: string, depth: number) {
|
|
57
|
-
const arr = name?.split('\\') ?? [];
|
|
58
|
-
return arr.length <= depth ? null : arr.slice(0, depth + 1).join('\\');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isFolderForEntry(folderName: string, entryName: string, depth: number) {
|
|
62
|
-
const name = getNameAtDepth(entryName, depth);
|
|
63
|
-
return name && name === folderName && folderName.length < entryName.length;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function getFolderDisplayName(name: string, depth: number) {
|
|
67
|
-
return name?.split('\\')[depth];
|
|
68
|
-
}
|