@xh/hoist 71.0.0-SNAPSHOT.1731709792477 → 71.0.0-SNAPSHOT.1731971865033
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 +5 -0
- package/build/types/core/persist/viewmanager/Types.d.ts +3 -1
- package/build/types/core/persist/viewmanager/ViewManagerModel.d.ts +10 -19
- package/build/types/core/persist/viewmanager/impl/BuildViewTree.d.ts +8 -0
- package/build/types/desktop/cmp/viewmanager/ViewManager.d.ts +16 -2
- package/build/types/desktop/cmp/viewmanager/index.d.ts +0 -2
- package/build/types/svc/JsonBlobService.d.ts +2 -2
- package/core/persist/viewmanager/Types.ts +3 -1
- package/core/persist/viewmanager/ViewManagerModel.ts +110 -156
- package/core/persist/viewmanager/impl/BuildViewTree.ts +68 -0
- package/core/persist/viewmanager/impl/ManageDialogModel.ts +2 -0
- package/core/persist/viewmanager/impl/SaveDialogModel.ts +1 -1
- package/desktop/cmp/viewmanager/ViewManager.ts +97 -50
- package/desktop/cmp/viewmanager/{cmp → impl}/ManageDialog.ts +1 -1
- package/desktop/cmp/viewmanager/index.ts +0 -2
- package/package.json +1 -1
- package/svc/JsonBlobService.ts +3 -6
- package/tsconfig.tsbuildinfo +1 -1
- /package/build/types/desktop/cmp/viewmanager/{cmp → impl}/ManageDialog.d.ts +0 -0
- /package/build/types/desktop/cmp/viewmanager/{cmp → impl}/SaveDialog.d.ts +0 -0
- /package/desktop/cmp/viewmanager/{cmp → impl}/SaveDialog.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -19,8 +19,10 @@ export interface View<T extends PlainObject = PlainObject> {
|
|
|
19
19
|
isShared: boolean;
|
|
20
20
|
lastUpdated: number;
|
|
21
21
|
lastUpdatedBy: string;
|
|
22
|
-
/** User-supplied descriptive name. */
|
|
22
|
+
/** User-supplied descriptive name, including folder designating prefix. */
|
|
23
23
|
name: string;
|
|
24
|
+
/** User-supplied descriptive name, without folder designating prefix. */
|
|
25
|
+
shortName: string;
|
|
24
26
|
/** Original creator of the view, and the only user with access to it if not shared. */
|
|
25
27
|
owner: string;
|
|
26
28
|
token: string;
|
|
@@ -75,15 +75,16 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
|
|
|
75
75
|
pendingValue: T;
|
|
76
76
|
/** Loaded saved view definitions - both private and shared. */
|
|
77
77
|
views: View<T>[];
|
|
78
|
-
/**
|
|
78
|
+
/** Currently selected view, or null if in default mode. Token only will be set during pre-loading.*/
|
|
79
79
|
selectedToken: string;
|
|
80
|
+
selectedView: View<T>;
|
|
80
81
|
/** List of tokens for the user's favorite views. */
|
|
81
82
|
favorites: string[];
|
|
82
83
|
/**
|
|
83
84
|
* True if user has opted-in to automatically saving changes to personal views (if auto-save
|
|
84
85
|
* generally available as per `enableAutoSave`).
|
|
85
86
|
*/
|
|
86
|
-
|
|
87
|
+
autoSave: boolean;
|
|
87
88
|
/**
|
|
88
89
|
* TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
|
|
89
90
|
* require intensive layout/grid work, consider masking affected components with this observer.
|
|
@@ -99,17 +100,9 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
|
|
|
99
100
|
*/
|
|
100
101
|
providers: ViewManagerProvider<any>[];
|
|
101
102
|
get enableSharing(): boolean;
|
|
102
|
-
get selectedView(): View<T>;
|
|
103
|
-
get isSharedViewSelected(): boolean;
|
|
104
103
|
get canSave(): boolean;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* that button might be disabled due to no changes having been made. Works in concert with the
|
|
108
|
-
* desktop ViewManager component's `showSaveButton` prop.
|
|
109
|
-
*/
|
|
110
|
-
get canShowSaveButton(): boolean;
|
|
111
|
-
get enableAutoSaveToggle(): boolean;
|
|
112
|
-
get disabledAutoSaveReason(): string;
|
|
104
|
+
get canAutoSave(): boolean;
|
|
105
|
+
get autoSaveUnavailableReason(): string;
|
|
113
106
|
get isDirty(): boolean;
|
|
114
107
|
get isShared(): boolean;
|
|
115
108
|
get favoriteViews(): View<T>[];
|
|
@@ -124,31 +117,29 @@ export declare class ViewManagerModel<T extends PlainObject = PlainObject> exten
|
|
|
124
117
|
private constructor();
|
|
125
118
|
doLoadAsync(loadSpec: LoadSpec): Promise<void>;
|
|
126
119
|
selectViewAsync(token: string): Promise<void>;
|
|
127
|
-
saveAsync(
|
|
120
|
+
saveAsync(): Promise<void>;
|
|
128
121
|
saveAsAsync(): Promise<void>;
|
|
129
122
|
resetAsync(): Promise<void>;
|
|
130
123
|
setPendingValue(pendingValue: T): void;
|
|
131
124
|
openManageDialog(): void;
|
|
132
125
|
closeManageDialog(): void;
|
|
133
|
-
getHierarchyDisplayName(name: string): string;
|
|
134
126
|
toggleFavorite(token: string): void;
|
|
135
127
|
addFavorite(token: string): void;
|
|
136
128
|
removeFavorite(token: string): void;
|
|
137
129
|
isFavorite(token: string): boolean;
|
|
138
130
|
getPersistableState(): PersistableState<ViewManagerModelPersistState>;
|
|
139
131
|
setPersistableState(state: PersistableState<ViewManagerModelPersistState>): void;
|
|
132
|
+
private selectViewInternalAsync;
|
|
140
133
|
private processRaw;
|
|
141
134
|
private setValue;
|
|
142
135
|
private cleanValue;
|
|
143
136
|
private confirmSaveForSharedViewAsync;
|
|
144
137
|
private maybeAutoSaveAsync;
|
|
145
|
-
private buildViewTree;
|
|
146
|
-
private getNameHierarchySubstring;
|
|
147
|
-
private isFolderForEntry;
|
|
148
138
|
private onFavoritesChange;
|
|
149
139
|
}
|
|
150
140
|
interface ViewManagerModelPersistState {
|
|
151
|
-
selectedToken
|
|
152
|
-
favorites
|
|
141
|
+
selectedToken?: string;
|
|
142
|
+
favorites?: string[];
|
|
143
|
+
autoSave?: boolean;
|
|
153
144
|
}
|
|
154
145
|
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { View, ViewManagerModel, ViewTree } from '@xh/hoist/core/persist/viewmanager';
|
|
2
|
+
/**
|
|
3
|
+
* Create a menu-friendly, tree representation of a set of views, using the `\`
|
|
4
|
+
* in view names to create folders.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildViewTree(views: View[], model: ViewManagerModel): ViewTree[];
|
|
@@ -3,11 +3,25 @@ import { HoistProps } from '@xh/hoist/core';
|
|
|
3
3
|
import './ViewManager.scss';
|
|
4
4
|
import { ViewManagerModel } from '@xh/hoist/core/persist/viewmanager/ViewManagerModel';
|
|
5
5
|
import { ButtonProps } from '@xh/hoist/desktop/cmp/button';
|
|
6
|
+
/**
|
|
7
|
+
* Visibility options for save/revert button.
|
|
8
|
+
*
|
|
9
|
+
* 'never' to hide button.
|
|
10
|
+
* 'whenDirty' to only show when persistence state is dirty and button is therefore enabled.
|
|
11
|
+
* 'always' will always show button, unless autoSave is active.
|
|
12
|
+
*
|
|
13
|
+
* Note that we never show the button when 'autoSave' is active because it would never be enabled
|
|
14
|
+
* for more than a flash.
|
|
15
|
+
*/
|
|
16
|
+
export type ViewManagerStateButtonMode = 'whenDirty' | 'always' | 'never';
|
|
6
17
|
export interface ViewManagerProps extends HoistProps<ViewManagerModel> {
|
|
7
18
|
menuButtonProps?: Partial<ButtonProps>;
|
|
8
19
|
saveButtonProps?: Partial<ButtonProps>;
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
revertButtonProps?: Partial<ButtonProps>;
|
|
21
|
+
/** Default 'whenDirty' */
|
|
22
|
+
showSaveButton?: ViewManagerStateButtonMode;
|
|
23
|
+
/** Default 'never' */
|
|
24
|
+
showRevertButton?: ViewManagerStateButtonMode;
|
|
11
25
|
/** True to render private views in sub-menu (Default false)*/
|
|
12
26
|
showPrivateViewsInSubMenu?: boolean;
|
|
13
27
|
/** True to render shared views in sub-menu (Default false)*/
|
|
@@ -42,11 +42,11 @@ export declare class JsonBlobService extends HoistService {
|
|
|
42
42
|
/** Retrieve a single JSONBlob by its unique token. */
|
|
43
43
|
getAsync(token: string): Promise<JsonBlob>;
|
|
44
44
|
/** Retrieve all blobs of a particular type that are visible to the current user. */
|
|
45
|
-
listAsync(
|
|
45
|
+
listAsync(spec: {
|
|
46
46
|
type: string;
|
|
47
47
|
includeValue?: boolean;
|
|
48
48
|
loadSpec?: LoadSpec;
|
|
49
|
-
}): Promise<
|
|
49
|
+
}): Promise<JsonBlob[]>;
|
|
50
50
|
/** Persist a new JSONBlob back to the server. */
|
|
51
51
|
createAsync({ acl, description, type, meta, name, value }: Partial<JsonBlob>): Promise<JsonBlob>;
|
|
52
52
|
/** Modify mutable properties of an existing JSONBlob, as identified by its unique token. */
|
|
@@ -20,8 +20,10 @@ export interface View<T extends PlainObject = PlainObject> {
|
|
|
20
20
|
isShared: boolean;
|
|
21
21
|
lastUpdated: number;
|
|
22
22
|
lastUpdatedBy: string;
|
|
23
|
-
/** User-supplied descriptive name. */
|
|
23
|
+
/** User-supplied descriptive name, including folder designating prefix. */
|
|
24
24
|
name: string;
|
|
25
|
+
/** User-supplied descriptive name, without folder designating prefix. */
|
|
26
|
+
shortName: string;
|
|
25
27
|
/** Original creator of the view, and the only user with access to it if not shared. */
|
|
26
28
|
owner: string;
|
|
27
29
|
token: string;
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
} from '@xh/hoist/core';
|
|
15
15
|
import {genDisplayName} from '@xh/hoist/data';
|
|
16
16
|
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
17
|
-
import {wait} from '@xh/hoist/promise';
|
|
18
17
|
import {executeIfFunction, pluralize, throwIf} from '@xh/hoist/utils/js';
|
|
19
|
-
import {
|
|
18
|
+
import {isEqual, isNil, isUndefined, lowerCase, startCase} from 'lodash';
|
|
20
19
|
import {runInAction} from 'mobx';
|
|
21
20
|
import {SaveDialogModel} from './impl/SaveDialogModel';
|
|
21
|
+
import {buildViewTree} from './impl/BuildViewTree';
|
|
22
22
|
import {View, ViewTree} from './Types';
|
|
23
23
|
|
|
24
24
|
export interface ViewManagerConfig {
|
|
@@ -104,16 +104,19 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
104
104
|
/** Current state of the active view, can include not-yet-persisted changes. */
|
|
105
105
|
@observable.ref pendingValue: T = {} as T;
|
|
106
106
|
/** Loaded saved view definitions - both private and shared. */
|
|
107
|
-
@observable.ref views: View<T>[] =
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
|
|
110
113
|
/** List of tokens for the user's favorite views. */
|
|
111
114
|
@bindable favorites: string[] = [];
|
|
112
115
|
/**
|
|
113
116
|
* True if user has opted-in to automatically saving changes to personal views (if auto-save
|
|
114
117
|
* generally available as per `enableAutoSave`).
|
|
115
118
|
*/
|
|
116
|
-
@bindable
|
|
119
|
+
@bindable autoSave = false;
|
|
117
120
|
|
|
118
121
|
/**
|
|
119
122
|
* TaskObserver linked to {@link selectViewAsync}. If a change to the active view is likely to
|
|
@@ -137,51 +140,30 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
137
140
|
return executeIfFunction(this._enableSharing);
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
get selectedView(): View<T> {
|
|
141
|
-
return this.views.find(it => it.token === this.selectedToken);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
@computed
|
|
145
|
-
get isSharedViewSelected(): boolean {
|
|
146
|
-
return !!this.selectedView?.isShared;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
143
|
@computed
|
|
150
144
|
get canSave(): boolean {
|
|
151
|
-
const {selectedView} = this;
|
|
152
|
-
return (
|
|
153
|
-
selectedView &&
|
|
154
|
-
this.isDirty &&
|
|
155
|
-
(this.enableSharing || !selectedView.isShared) &&
|
|
156
|
-
!this.loadModel.isPending
|
|
157
|
-
);
|
|
145
|
+
const {loadModel, selectedView, enableSharing} = this;
|
|
146
|
+
return !loadModel.isPending && selectedView && (enableSharing || !selectedView.isShared);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
|
-
/**
|
|
161
|
-
* True if displaying the save button is appropriate from the model's point of view, even if
|
|
162
|
-
* that button might be disabled due to no changes having been made. Works in concert with the
|
|
163
|
-
* desktop ViewManager component's `showSaveButton` prop.
|
|
164
|
-
*/
|
|
165
149
|
@computed
|
|
166
|
-
get
|
|
167
|
-
const {selectedView} = this;
|
|
150
|
+
get canAutoSave(): boolean {
|
|
151
|
+
const {enableAutoSave, autoSave, loadModel, selectedView} = this;
|
|
168
152
|
return (
|
|
153
|
+
!loadModel.isPending &&
|
|
154
|
+
enableAutoSave &&
|
|
155
|
+
autoSave &&
|
|
169
156
|
selectedView &&
|
|
170
|
-
|
|
171
|
-
(this.enableSharing || !selectedView.isShared)
|
|
157
|
+
!selectedView.isShared
|
|
172
158
|
);
|
|
173
159
|
}
|
|
174
160
|
|
|
175
161
|
@computed
|
|
176
|
-
get
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
get disabledAutoSaveReason(): string {
|
|
182
|
-
const {displayName} = this;
|
|
183
|
-
if (!this.selectedView) return `Cannot auto-save default ${displayName}.`;
|
|
184
|
-
if (this.isSharedViewSelected) return `Cannot auto-save shared ${displayName}.`;
|
|
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}.`;
|
|
185
167
|
return null;
|
|
186
168
|
}
|
|
187
169
|
|
|
@@ -207,11 +189,11 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
207
189
|
}
|
|
208
190
|
|
|
209
191
|
get sharedViewTree(): ViewTree[] {
|
|
210
|
-
return
|
|
192
|
+
return buildViewTree(this.sharedViews, this);
|
|
211
193
|
}
|
|
212
194
|
|
|
213
195
|
get privateViewTree(): ViewTree[] {
|
|
214
|
-
return
|
|
196
|
+
return buildViewTree(this.privateViews, this);
|
|
215
197
|
}
|
|
216
198
|
|
|
217
199
|
/**
|
|
@@ -257,12 +239,9 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
257
239
|
|
|
258
240
|
this.addReaction(
|
|
259
241
|
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
{
|
|
264
|
-
track: () => this.autoSaveActive,
|
|
265
|
-
run: () => this.maybeAutoSaveAsync({skipToast: false})
|
|
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()
|
|
266
245
|
},
|
|
267
246
|
{
|
|
268
247
|
track: () => this.favorites,
|
|
@@ -274,53 +253,51 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
274
253
|
override async doLoadAsync(loadSpec: LoadSpec) {
|
|
275
254
|
const rawViews = await XH.jsonBlobService.listAsync({
|
|
276
255
|
type: this.viewType,
|
|
277
|
-
includeValue:
|
|
256
|
+
includeValue: false,
|
|
278
257
|
loadSpec
|
|
279
258
|
});
|
|
280
259
|
if (loadSpec.isStale) return;
|
|
281
260
|
|
|
282
|
-
runInAction(() =>
|
|
261
|
+
runInAction(() => {
|
|
262
|
+
this.views = rawViews.map(it => this.processRaw(it));
|
|
263
|
+
});
|
|
283
264
|
|
|
284
265
|
const token =
|
|
285
266
|
loadSpec.meta.selectToken ??
|
|
286
|
-
this.
|
|
267
|
+
this.selectedToken ??
|
|
287
268
|
(this.enableDefault ? null : this.views[0]?.token);
|
|
288
269
|
await this.selectViewAsync(token);
|
|
289
270
|
}
|
|
290
271
|
|
|
291
272
|
async selectViewAsync(token: string) {
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// selected token before views have been loaded. Once views are loaded, this method will
|
|
299
|
-
// be called again with the desired token and will proceed to set the value.
|
|
300
|
-
if (isEmpty(this.views)) return;
|
|
301
|
-
|
|
302
|
-
this.setValue(this.selectedView?.value ?? ({} as T));
|
|
303
|
-
})
|
|
304
|
-
.linkTo(this.viewSelectionObserver);
|
|
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);
|
|
305
279
|
}
|
|
306
280
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
281
|
+
//------------------------
|
|
282
|
+
// Saving/resetting
|
|
283
|
+
//------------------------
|
|
284
|
+
async saveAsync() {
|
|
285
|
+
const {canSave, selectedToken, pendingValue, selectedView, DisplayName} = this;
|
|
286
|
+
throwIf(!canSave, 'Unable to save view.');
|
|
310
287
|
|
|
311
|
-
if (
|
|
288
|
+
if (selectedView?.isShared) {
|
|
312
289
|
if (!(await this.confirmSaveForSharedViewAsync())) return;
|
|
313
290
|
}
|
|
314
291
|
|
|
315
292
|
try {
|
|
316
293
|
await XH.jsonBlobService.updateAsync(selectedToken, {value: pendingValue});
|
|
294
|
+
runInAction(() => {
|
|
295
|
+
this.value = this.pendingValue;
|
|
296
|
+
});
|
|
297
|
+
XH.successToast(`${DisplayName} successfully saved.`);
|
|
317
298
|
} catch (e) {
|
|
318
299
|
XH.handleException(e, {alertType: 'toast'});
|
|
319
|
-
skipToast = true; // don't show the success toast below, but still refresh.
|
|
320
300
|
}
|
|
321
|
-
|
|
322
|
-
await this.refreshAsync({selectToken: selectedToken});
|
|
323
|
-
if (!skipToast) XH.successToast(`${DisplayName} successfully saved.`);
|
|
324
301
|
}
|
|
325
302
|
|
|
326
303
|
async saveAsAsync() {
|
|
@@ -364,10 +341,6 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
364
341
|
this.manageDialogOpen = false;
|
|
365
342
|
}
|
|
366
343
|
|
|
367
|
-
getHierarchyDisplayName(name: string) {
|
|
368
|
-
return name?.substring(name.lastIndexOf('\\') + 1);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
344
|
//------------------
|
|
372
345
|
// Favorites
|
|
373
346
|
//------------------
|
|
@@ -391,38 +364,69 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
391
364
|
// Persistable
|
|
392
365
|
//------------------
|
|
393
366
|
getPersistableState(): PersistableState<ViewManagerModelPersistState> {
|
|
394
|
-
|
|
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);
|
|
395
375
|
}
|
|
396
376
|
|
|
397
377
|
setPersistableState(state: PersistableState<ViewManagerModelPersistState>) {
|
|
398
|
-
const {selectedToken, favorites} = state.value;
|
|
399
|
-
if (selectedToken)
|
|
400
|
-
|
|
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
|
+
}
|
|
401
388
|
}
|
|
402
389
|
|
|
403
390
|
//------------------
|
|
404
391
|
// Implementation
|
|
405
392
|
//------------------
|
|
406
|
-
private
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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));
|
|
416
410
|
});
|
|
417
411
|
}
|
|
418
412
|
|
|
419
|
-
|
|
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
|
+
|
|
420
425
|
private setValue(value: T) {
|
|
421
426
|
value = this.cleanValue(value);
|
|
422
427
|
if (isEqual(value, this.value) && isEqual(value, this.pendingValue)) return;
|
|
423
428
|
|
|
424
|
-
this.value = value;
|
|
425
|
-
this.pendingValue = value;
|
|
429
|
+
this.value = this.pendingValue = value;
|
|
426
430
|
this.providers.forEach(it => it.pushStateToTarget());
|
|
427
431
|
}
|
|
428
432
|
|
|
@@ -447,69 +451,18 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
447
451
|
});
|
|
448
452
|
}
|
|
449
453
|
|
|
450
|
-
private async maybeAutoSaveAsync(
|
|
451
|
-
if (
|
|
452
|
-
this
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
private buildViewTree(views: View<T>[], depth: number = 0): ViewTree[] {
|
|
462
|
-
const groups = {},
|
|
463
|
-
unbalancedStableGroupsAndViews = [];
|
|
464
|
-
|
|
465
|
-
views.forEach(view => {
|
|
466
|
-
// Leaf Node
|
|
467
|
-
if (this.getNameHierarchySubstring(view.name, depth + 1) == null) {
|
|
468
|
-
unbalancedStableGroupsAndViews.push(view);
|
|
469
|
-
return;
|
|
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});
|
|
470
464
|
}
|
|
471
|
-
// Belongs to an already defined group
|
|
472
|
-
const group = this.getNameHierarchySubstring(view.name, depth);
|
|
473
|
-
if (groups[group]) {
|
|
474
|
-
groups[group].children.push(view);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
// Belongs to a not defined group, create it
|
|
478
|
-
groups[group] = {name: group, children: [view], isMenuFolder: true};
|
|
479
|
-
unbalancedStableGroupsAndViews.push(groups[group]);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
return unbalancedStableGroupsAndViews.map(it => {
|
|
483
|
-
const {name, isMenuFolder, children, description, token} = it;
|
|
484
|
-
if (isMenuFolder) {
|
|
485
|
-
return {
|
|
486
|
-
type: 'folder',
|
|
487
|
-
text: name,
|
|
488
|
-
items: this.buildViewTree(children, depth + 1),
|
|
489
|
-
selected: this.isFolderForEntry(name, this.selectedView?.name, depth)
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
return {
|
|
493
|
-
type: 'view',
|
|
494
|
-
text: this.getHierarchyDisplayName(name),
|
|
495
|
-
selected: this.selectedToken === token,
|
|
496
|
-
token,
|
|
497
|
-
description
|
|
498
|
-
};
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
private getNameHierarchySubstring(name: string, depth: number) {
|
|
503
|
-
const arr = name?.split('\\') ?? [];
|
|
504
|
-
if (arr.length <= depth) {
|
|
505
|
-
return null;
|
|
506
465
|
}
|
|
507
|
-
return arr.slice(0, depth + 1).join('\\');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private isFolderForEntry(folderName: string, entryName: string, depth: number) {
|
|
511
|
-
const name = this.getNameHierarchySubstring(entryName, depth);
|
|
512
|
-
return name && name === folderName && folderName.length < entryName.length;
|
|
513
466
|
}
|
|
514
467
|
|
|
515
468
|
// Update flag on each view, replacing entire views collection for observability.
|
|
@@ -522,6 +475,7 @@ export class ViewManagerModel<T extends PlainObject = PlainObject>
|
|
|
522
475
|
}
|
|
523
476
|
|
|
524
477
|
interface ViewManagerModelPersistState {
|
|
525
|
-
selectedToken
|
|
526
|
-
favorites
|
|
478
|
+
selectedToken?: string;
|
|
479
|
+
favorites?: string[];
|
|
480
|
+
autoSave?: boolean;
|
|
527
481
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
}
|