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