@xh/hoist 72.0.0 → 72.2.0
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 +41 -2
- package/admin/jsonsearch/JsonSearch.ts +294 -0
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +175 -0
- package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +23 -4
- package/admin/tabs/general/config/ConfigPanel.ts +26 -4
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +47 -10
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +45 -15
- package/admin/tabs/userData/roles/RoleModel.ts +3 -3
- package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +2 -1
- package/admin/tabs/userData/roles/editor/form/RoleFormModel.ts +3 -3
- package/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel.ts +1 -1
- package/build/types/admin/jsonsearch/JsonSearch.d.ts +17 -0
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +32 -0
- package/build/types/cmp/tab/TabContainerModel.d.ts +5 -5
- package/build/types/core/HoistProps.d.ts +1 -0
- package/build/types/core/XH.d.ts +5 -5
- package/build/types/core/persist/PersistenceProvider.d.ts +4 -0
- package/build/types/core/types/Interfaces.d.ts +9 -0
- package/build/types/data/Store.d.ts +4 -0
- package/build/types/data/StoreRecord.d.ts +2 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
- package/build/types/kit/blueprint/Wrappers.d.ts +1 -1
- package/build/types/kit/swiper/index.d.ts +4 -4
- package/build/types/security/BaseOAuthClient.d.ts +19 -21
- package/build/types/security/Token.d.ts +0 -1
- package/build/types/security/Types.d.ts +12 -0
- package/build/types/security/authzero/AuthZeroClient.d.ts +3 -4
- package/build/types/security/msal/MsalClient.d.ts +3 -4
- package/cmp/filter/FilterChooserModel.ts +6 -2
- package/cmp/grid/impl/InitPersist.ts +9 -3
- package/cmp/grouping/GroupingChooserModel.ts +6 -2
- package/cmp/tab/TabContainerModel.ts +5 -5
- package/cmp/zoneGrid/impl/InitPersist.ts +9 -3
- package/core/HoistBase.ts +1 -2
- package/core/HoistBaseDecorators.ts +4 -1
- package/core/HoistProps.ts +1 -0
- package/core/XH.ts +13 -5
- package/core/exception/Exception.ts +19 -12
- package/core/persist/PersistenceProvider.ts +31 -0
- package/core/types/Interfaces.ts +11 -0
- package/data/Store.ts +13 -3
- package/data/StoreRecord.ts +6 -1
- package/data/cube/row/BaseRow.ts +3 -2
- package/desktop/appcontainer/ExceptionDialog.ts +1 -1
- package/desktop/cmp/dash/canvas/DashCanvas.ts +2 -1
- package/mobile/cmp/navigator/NavigatorModel.ts +7 -0
- package/package.json +1 -1
- package/security/BaseOAuthClient.ts +41 -36
- package/security/Token.ts +0 -2
- package/security/Types.ts +22 -0
- package/security/authzero/AuthZeroClient.ts +6 -8
- package/security/msal/MsalClient.ts +6 -8
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/react/LayoutPropUtils.ts +2 -1
|
@@ -24,7 +24,9 @@ export function initPersist(
|
|
|
24
24
|
}: GridModelPersistOptions
|
|
25
25
|
) {
|
|
26
26
|
if (persistColumns) {
|
|
27
|
-
const persistWith = isObject(persistColumns)
|
|
27
|
+
const persistWith = isObject(persistColumns)
|
|
28
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistColumns)
|
|
29
|
+
: rootPersistWith;
|
|
28
30
|
PersistenceProvider.create({
|
|
29
31
|
persistOptions: {
|
|
30
32
|
path: `${path}.columns`,
|
|
@@ -39,7 +41,9 @@ export function initPersist(
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
if (persistSort) {
|
|
42
|
-
const persistWith = isObject(persistSort)
|
|
44
|
+
const persistWith = isObject(persistSort)
|
|
45
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistSort)
|
|
46
|
+
: rootPersistWith;
|
|
43
47
|
PersistenceProvider.create({
|
|
44
48
|
persistOptions: {
|
|
45
49
|
path: `${path}.sortBy`,
|
|
@@ -55,7 +59,9 @@ export function initPersist(
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
if (persistGrouping) {
|
|
58
|
-
const persistWith = isObject(
|
|
62
|
+
const persistWith = isObject(persistGrouping)
|
|
63
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistGrouping)
|
|
64
|
+
: rootPersistWith;
|
|
59
65
|
PersistenceProvider.create({
|
|
60
66
|
persistOptions: {
|
|
61
67
|
path: `${path}.groupBy`,
|
|
@@ -293,7 +293,9 @@ export class GroupingChooserModel extends HoistModel {
|
|
|
293
293
|
...rootPersistWith
|
|
294
294
|
}: GroupingChooserPersistOptions) {
|
|
295
295
|
if (persistValue) {
|
|
296
|
-
const persistWith = isObject(persistValue)
|
|
296
|
+
const persistWith = isObject(persistValue)
|
|
297
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistValue)
|
|
298
|
+
: rootPersistWith;
|
|
297
299
|
PersistenceProvider.create({
|
|
298
300
|
persistOptions: {
|
|
299
301
|
path: `${path}.value`,
|
|
@@ -308,7 +310,9 @@ export class GroupingChooserModel extends HoistModel {
|
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
if (persistFavorites) {
|
|
311
|
-
const persistWith = isObject(persistFavorites)
|
|
313
|
+
const persistWith = isObject(persistFavorites)
|
|
314
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistFavorites)
|
|
315
|
+
: rootPersistWith,
|
|
312
316
|
provider = PersistenceProvider.create({
|
|
313
317
|
persistOptions: {
|
|
314
318
|
path: `${path}.favorites`,
|
|
@@ -108,7 +108,7 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
108
108
|
@managed
|
|
109
109
|
refreshContextModel: RefreshContextModel;
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
protected lastActiveTabId: string;
|
|
112
112
|
|
|
113
113
|
constructor({
|
|
114
114
|
tabs = [],
|
|
@@ -342,7 +342,7 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
342
342
|
// Implementation
|
|
343
343
|
//-------------------------
|
|
344
344
|
@action
|
|
345
|
-
|
|
345
|
+
protected setActiveTabIdInternal(id) {
|
|
346
346
|
const tab = this.findTab(id);
|
|
347
347
|
throwIf(!tab, `Unknown Tab ${id} in TabContainer.`);
|
|
348
348
|
throwIf(tab.disabled, `Cannot activate Tab ${id} because it is disabled!`);
|
|
@@ -351,7 +351,7 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
351
351
|
this.forwardRouterToTab(id);
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
protected syncWithRouter() {
|
|
355
355
|
const {tabs, route} = this,
|
|
356
356
|
{router} = XH,
|
|
357
357
|
state = router.getState();
|
|
@@ -364,14 +364,14 @@ export class TabContainerModel extends HoistModel implements Persistable<{active
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
|
|
367
|
+
protected forwardRouterToTab(id) {
|
|
368
368
|
const {route} = this;
|
|
369
369
|
if (route && id) {
|
|
370
370
|
XH.router.forward(route, route + '.' + id);
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
protected calculateActiveTabId(tabs) {
|
|
375
375
|
let ret;
|
|
376
376
|
|
|
377
377
|
// try route
|
|
@@ -24,7 +24,9 @@ export function initPersist(
|
|
|
24
24
|
}: ZoneGridModelPersistOptions
|
|
25
25
|
) {
|
|
26
26
|
if (persistMappings) {
|
|
27
|
-
const persistWith = isObject(persistMappings)
|
|
27
|
+
const persistWith = isObject(persistMappings)
|
|
28
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistMappings)
|
|
29
|
+
: rootPersistWith;
|
|
28
30
|
PersistenceProvider.create({
|
|
29
31
|
persistOptions: {
|
|
30
32
|
path: `${path}.mappings`,
|
|
@@ -39,7 +41,9 @@ export function initPersist(
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
if (persistGrouping) {
|
|
42
|
-
const persistWith = isObject(persistGrouping)
|
|
44
|
+
const persistWith = isObject(persistGrouping)
|
|
45
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistGrouping)
|
|
46
|
+
: rootPersistWith;
|
|
43
47
|
PersistenceProvider.create({
|
|
44
48
|
persistOptions: {
|
|
45
49
|
path: `${path}.groupBy`,
|
|
@@ -54,7 +58,9 @@ export function initPersist(
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
if (persistSort) {
|
|
57
|
-
const persistWith = isObject(persistSort)
|
|
61
|
+
const persistWith = isObject(persistSort)
|
|
62
|
+
? PersistenceProvider.mergePersistOptions(rootPersistWith, persistSort)
|
|
63
|
+
: rootPersistWith;
|
|
58
64
|
PersistenceProvider.create({
|
|
59
65
|
persistOptions: {
|
|
60
66
|
path: `${path}.sortBy`,
|
package/core/HoistBase.ts
CHANGED
|
@@ -261,8 +261,7 @@ export abstract class HoistBase {
|
|
|
261
261
|
PersistenceProvider.create({
|
|
262
262
|
persistOptions: {
|
|
263
263
|
path: property,
|
|
264
|
-
...this.persistWith,
|
|
265
|
-
...options
|
|
264
|
+
...PersistenceProvider.mergePersistOptions(this.persistWith, options)
|
|
266
265
|
},
|
|
267
266
|
owner: this,
|
|
268
267
|
target: {
|
|
@@ -81,7 +81,10 @@ function createPersistDescriptor(
|
|
|
81
81
|
// codeValue undefined if no initial in-code value provided, otherwise call to get initial value.
|
|
82
82
|
ret = codeValue?.call(this);
|
|
83
83
|
|
|
84
|
-
const persistOptions = {
|
|
84
|
+
const persistOptions = {
|
|
85
|
+
path: property,
|
|
86
|
+
...PersistenceProvider.mergePersistOptions(this.persistWith, options)
|
|
87
|
+
};
|
|
85
88
|
PersistenceProvider.create({
|
|
86
89
|
persistOptions,
|
|
87
90
|
owner: this,
|
package/core/HoistProps.ts
CHANGED
package/core/XH.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
MessageSpec,
|
|
55
55
|
PageState,
|
|
56
56
|
PlainObject,
|
|
57
|
+
ReloadAppOptions,
|
|
57
58
|
SizingMode,
|
|
58
59
|
TaskObserver,
|
|
59
60
|
Theme,
|
|
@@ -64,7 +65,7 @@ import {installServicesAsync} from './impl/InstallServices';
|
|
|
64
65
|
import {instanceManager} from './impl/InstanceManager';
|
|
65
66
|
import {HoistModel, ModelSelector, RefreshContextModel} from './model';
|
|
66
67
|
|
|
67
|
-
export const MIN_HOIST_CORE_VERSION = '
|
|
68
|
+
export const MIN_HOIST_CORE_VERSION = '28.0';
|
|
68
69
|
|
|
69
70
|
declare const xhAppCode: string;
|
|
70
71
|
declare const xhAppName: string;
|
|
@@ -392,18 +393,25 @@ export class XHApi {
|
|
|
392
393
|
/**
|
|
393
394
|
* Trigger a full reload of the current application.
|
|
394
395
|
*
|
|
395
|
-
* @param
|
|
396
|
-
*
|
|
396
|
+
* @param opts - options to govern reload. To support legacy usages, a provided
|
|
397
|
+
* string will be treated as `ReloadAppOptions.path`.
|
|
397
398
|
*
|
|
398
399
|
* This method will reload the entire application document in the browser - to trigger a
|
|
399
400
|
* refresh of the loadable content within the app, use {@link refreshAppAsync} instead.
|
|
400
401
|
*/
|
|
401
402
|
@action
|
|
402
|
-
reloadApp(
|
|
403
|
+
reloadApp(opts?: ReloadAppOptions | string) {
|
|
403
404
|
never().linkTo(this.appLoadModel);
|
|
405
|
+
|
|
406
|
+
opts = isString(opts) ? {path: opts} : (opts ?? {});
|
|
407
|
+
|
|
404
408
|
const {location} = window,
|
|
405
|
-
href =
|
|
409
|
+
href = opts.path
|
|
410
|
+
? `${location.origin}/${opts.path.replace(/^\/+/, '')}`
|
|
411
|
+
: location.href,
|
|
406
412
|
url = new URL(href);
|
|
413
|
+
|
|
414
|
+
if (opts.removeQueryParams) url.search = '';
|
|
407
415
|
// Add a unique query param to force a full reload without using the browser cache.
|
|
408
416
|
url.searchParams.set('xhCacheBuster', Date.now().toString());
|
|
409
417
|
document.location.assign(url);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {PlainObject, XH} from '@xh/hoist/core';
|
|
8
8
|
import {FetchOptions} from '@xh/hoist/svc';
|
|
9
9
|
import {pluralize} from '@xh/hoist/utils/js';
|
|
10
|
-
import {isPlainObject} from 'lodash';
|
|
10
|
+
import {isPlainObject, truncate} from 'lodash';
|
|
11
11
|
import {FetchException, HoistException, TimeoutException, TimeoutExceptionConfig} from './Types';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -90,17 +90,16 @@ export class Exception {
|
|
|
90
90
|
// Try to "smart" decode as server provided JSON Exception (with a name)
|
|
91
91
|
try {
|
|
92
92
|
const cType = headers.get('Content-Type');
|
|
93
|
-
if (cType
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
93
|
+
if (cType?.includes('application/json')) {
|
|
94
|
+
const obj = safeParseJson(responseText),
|
|
95
|
+
message = obj ? obj.message : truncate(responseText?.trim(), {length: 255});
|
|
96
|
+
return this.createFetchException({
|
|
97
|
+
...defaults,
|
|
98
|
+
name: obj?.name ?? defaults.name,
|
|
99
|
+
message: message ?? statusText,
|
|
100
|
+
isRoutine: obj?.isRoutine ?? false,
|
|
101
|
+
serverDetails: obj ?? responseText
|
|
102
|
+
});
|
|
104
103
|
}
|
|
105
104
|
} catch (ignored) {}
|
|
106
105
|
|
|
@@ -222,6 +221,14 @@ export class Exception {
|
|
|
222
221
|
}
|
|
223
222
|
}
|
|
224
223
|
|
|
224
|
+
function safeParseJson(txt: string): PlainObject {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(txt);
|
|
227
|
+
} catch (ignored) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
225
232
|
export function isHoistException(src: unknown): src is HoistException {
|
|
226
233
|
return src?.['isHoistException'];
|
|
227
234
|
}
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
import {logDebug, logError, throwIf} from '@xh/hoist/utils/js';
|
|
9
9
|
import {
|
|
10
10
|
cloneDeep,
|
|
11
|
+
compact,
|
|
11
12
|
debounce as lodashDebounce,
|
|
12
13
|
get,
|
|
13
14
|
isEmpty,
|
|
14
15
|
isNumber,
|
|
15
16
|
isString,
|
|
16
17
|
isUndefined,
|
|
18
|
+
omit,
|
|
17
19
|
set,
|
|
18
20
|
toPath
|
|
19
21
|
} from 'lodash';
|
|
@@ -91,6 +93,35 @@ export abstract class PersistenceProvider<S = any> {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Merge PersistOptions, respecting provider types, with later options overriding earlier ones.
|
|
98
|
+
*/
|
|
99
|
+
static mergePersistOptions(
|
|
100
|
+
defaults: PersistOptions,
|
|
101
|
+
...overrides: PersistOptions[]
|
|
102
|
+
): PersistOptions {
|
|
103
|
+
const TYPE_RELATED_KEYS = [
|
|
104
|
+
'type',
|
|
105
|
+
'prefKey',
|
|
106
|
+
'localStorageKey',
|
|
107
|
+
'sessionStorageKey',
|
|
108
|
+
'dashViewModel',
|
|
109
|
+
'viewManagerModel',
|
|
110
|
+
'getData',
|
|
111
|
+
'setData'
|
|
112
|
+
];
|
|
113
|
+
return compact(overrides).reduce(
|
|
114
|
+
(ret, override) =>
|
|
115
|
+
TYPE_RELATED_KEYS.some(key => override[key])
|
|
116
|
+
? {
|
|
117
|
+
...omit(ret, ...TYPE_RELATED_KEYS),
|
|
118
|
+
...override
|
|
119
|
+
}
|
|
120
|
+
: {...ret, ...override},
|
|
121
|
+
defaults
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
94
125
|
/** Read persisted state at this provider's path. */
|
|
95
126
|
read(): PersistableState<S> {
|
|
96
127
|
const state = get(this.readRaw(), this.path);
|
package/core/types/Interfaces.ts
CHANGED
|
@@ -28,6 +28,17 @@ export interface HoistUser {
|
|
|
28
28
|
hasGate(s: string): boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Options governing XH.reloadApp().
|
|
33
|
+
*/
|
|
34
|
+
export interface ReloadAppOptions {
|
|
35
|
+
/** Relative path to reload (e.g. 'mobile/'). Defaults to the existing location pathname. */
|
|
36
|
+
path?: string;
|
|
37
|
+
|
|
38
|
+
/** Should the query parameters be removed from the url before reload. Default false. */
|
|
39
|
+
removeQueryParams?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
43
|
* Options for showing a "toast" notification that appears and then automatically dismisses.
|
|
33
44
|
*/
|
package/data/Store.ts
CHANGED
|
@@ -435,7 +435,7 @@ export class Store extends HoistBase {
|
|
|
435
435
|
// sourced from the server / source of record and are coming in as committed.
|
|
436
436
|
this._committed = this._committed.withTransaction(rsTransaction);
|
|
437
437
|
|
|
438
|
-
if (this.
|
|
438
|
+
if (this.isDirty) {
|
|
439
439
|
// If this store had pre-existing local modifications, apply the updates over that
|
|
440
440
|
// local state. This might (or might not) effectively overwrite those local changes,
|
|
441
441
|
// so we normalize against the newly updated committed state to verify if any local
|
|
@@ -662,8 +662,13 @@ export class Store extends HoistBase {
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
/** Records modified locally since they were last loaded. */
|
|
665
|
+
get dirtyRecords(): StoreRecord[] {
|
|
666
|
+
return this.allRecords.filter(it => it.isDirty);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Alias for {@link Store.dirtyRecords} */
|
|
665
670
|
get modifiedRecords(): StoreRecord[] {
|
|
666
|
-
return this.
|
|
671
|
+
return this.dirtyRecords;
|
|
667
672
|
}
|
|
668
673
|
|
|
669
674
|
/**
|
|
@@ -696,10 +701,15 @@ export class Store extends HoistBase {
|
|
|
696
701
|
|
|
697
702
|
/** True if the store has changes which need to be committed. */
|
|
698
703
|
@computed
|
|
699
|
-
get
|
|
704
|
+
get isDirty(): boolean {
|
|
700
705
|
return this._current !== this._committed;
|
|
701
706
|
}
|
|
702
707
|
|
|
708
|
+
/** Alias for {@link Store.isDirty} */
|
|
709
|
+
get isModified(): boolean {
|
|
710
|
+
return this.isDirty;
|
|
711
|
+
}
|
|
712
|
+
|
|
703
713
|
/**
|
|
704
714
|
* Set a filter on this store.
|
|
705
715
|
*
|
package/data/StoreRecord.ts
CHANGED
|
@@ -71,10 +71,15 @@ export class StoreRecord {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** True if the StoreRecord has been modified since it was last committed. */
|
|
74
|
-
get
|
|
74
|
+
get isDirty(): boolean {
|
|
75
75
|
return this.committedData && this.committedData !== this.data;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Alias for {@link StoreRecord.isDirty} */
|
|
79
|
+
get isModified(): boolean {
|
|
80
|
+
return this.isDirty;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
/** False if the StoreRecord has been added or modified. */
|
|
79
84
|
get isCommitted(): boolean {
|
|
80
85
|
return this.committedData === this.data;
|
package/data/cube/row/BaseRow.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {PlainObject, Some} from '@xh/hoist/core';
|
|
9
9
|
import {BucketSpec} from '@xh/hoist/data/cube/BucketSpec';
|
|
10
|
-
import {isEmpty, reduce} from 'lodash';
|
|
10
|
+
import {compact, isEmpty, reduce} from 'lodash';
|
|
11
11
|
import {View} from '../View';
|
|
12
12
|
import {RowUpdate} from './RowUpdate';
|
|
13
13
|
|
|
@@ -97,7 +97,8 @@ export abstract class BaseRow {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// Recurse
|
|
100
|
-
|
|
100
|
+
const ret = compact(children.flatMap(it => it.getVisibleDatas()));
|
|
101
|
+
return !isEmpty(ret) ? ret : null;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
private isRedundantChild(parent: any, child: any) {
|
|
@@ -79,7 +79,7 @@ export const dismissButton = hoistCmp.factory<ExceptionDialogModel>(({model}) =>
|
|
|
79
79
|
icon: Icon.refresh(),
|
|
80
80
|
text: 'Reload App',
|
|
81
81
|
autoFocus: true,
|
|
82
|
-
onClick: () => XH.reloadApp()
|
|
82
|
+
onClick: () => XH.reloadApp({removeQueryParams: true})
|
|
83
83
|
})
|
|
84
84
|
: button({
|
|
85
85
|
text: 'Close',
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import {dashCanvasAddViewButton} from '@xh/hoist/desktop/cmp/button/DashCanvasAddViewButton';
|
|
19
19
|
import '@xh/hoist/desktop/register';
|
|
20
20
|
import {Classes, overlay} from '@xh/hoist/kit/blueprint';
|
|
21
|
-
import {TEST_ID} from '@xh/hoist/utils/js';
|
|
21
|
+
import {consumeEvent, TEST_ID} from '@xh/hoist/utils/js';
|
|
22
22
|
import classNames from 'classnames';
|
|
23
23
|
import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
|
|
24
24
|
import {DashCanvasModel} from './DashCanvasModel';
|
|
@@ -125,6 +125,7 @@ const onContextMenu = (e, model) => {
|
|
|
125
125
|
x = clientX + model.ref.current.scrollLeft,
|
|
126
126
|
y = clientY + model.ref.current.scrollTop;
|
|
127
127
|
|
|
128
|
+
consumeEvent(e);
|
|
128
129
|
showContextMenu(
|
|
129
130
|
dashCanvasContextMenu({
|
|
130
131
|
dashCanvasModel: model,
|
|
@@ -149,6 +149,7 @@ export class NavigatorModel extends HoistModel {
|
|
|
149
149
|
// to propagate to scrollable elements within the page.
|
|
150
150
|
swiper.on('touchStart', (s, event: PointerEvent) => {
|
|
151
151
|
swiper.allowTouchMove = false;
|
|
152
|
+
swiper.params.shortSwipes = true;
|
|
152
153
|
this._touchStartX = event.pageX;
|
|
153
154
|
});
|
|
154
155
|
|
|
@@ -175,6 +176,12 @@ export class NavigatorModel extends HoistModel {
|
|
|
175
176
|
swiper.allowTouchMove =
|
|
176
177
|
swiper.progress < 1 || !isDraggableEl(scrollableParent, 'right');
|
|
177
178
|
|
|
179
|
+
// Disable short swipes to prevent accidental navigation when reaching the
|
|
180
|
+
// end of the scrollable parent.
|
|
181
|
+
if (!swiper.allowTouchMove) {
|
|
182
|
+
swiper.params.shortSwipes = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
178
185
|
// During the swiper transition, undo the scrollable parent's internal scroll
|
|
179
186
|
// to keep it static.
|
|
180
187
|
if (swiper.progress < 1) {
|
package/package.json
CHANGED
|
@@ -9,16 +9,17 @@ import {HoistBase, managed, XH} from '@xh/hoist/core';
|
|
|
9
9
|
import {Icon} from '@xh/hoist/icon';
|
|
10
10
|
import {action, makeObservable} from '@xh/hoist/mobx';
|
|
11
11
|
import {never, wait} from '@xh/hoist/promise';
|
|
12
|
-
import {Token
|
|
12
|
+
import {Token} from '@xh/hoist/security/Token';
|
|
13
|
+
import {AccessTokenSpec, TokenMap} from './Types';
|
|
13
14
|
import {Timer} from '@xh/hoist/utils/async';
|
|
14
15
|
import {MINUTES, olderThan, ONE_MINUTE, SECONDS} from '@xh/hoist/utils/datetime';
|
|
15
16
|
import {isJSON, throwIf} from '@xh/hoist/utils/js';
|
|
16
|
-
import {find, forEach, isEmpty, isObject, keys, pickBy, union} from 'lodash';
|
|
17
|
+
import {find, forEach, isEmpty, isObject, keys, map, pickBy, union} from 'lodash';
|
|
17
18
|
import ShortUniqueId from 'short-unique-id';
|
|
18
19
|
|
|
19
20
|
export type LoginMethod = 'REDIRECT' | 'POPUP';
|
|
20
21
|
|
|
21
|
-
export interface BaseOAuthClientConfig<S> {
|
|
22
|
+
export interface BaseOAuthClientConfig<S extends AccessTokenSpec> {
|
|
22
23
|
/** Client ID (GUID) of your app registered with your Oauth provider. */
|
|
23
24
|
clientId: string;
|
|
24
25
|
|
|
@@ -45,26 +46,17 @@ export interface BaseOAuthClientConfig<S> {
|
|
|
45
46
|
* Governs an optional refresh timer that will work to keep the tokens fresh.
|
|
46
47
|
*
|
|
47
48
|
* A typical refresh will use the underlying provider cache, and should not result in
|
|
48
|
-
* network activity. However, if any token
|
|
49
|
+
* network activity. However, if any token would expire before the next autoRefresh,
|
|
49
50
|
* this client will force a call to the underlying provider to get the token.
|
|
50
51
|
*
|
|
51
52
|
* In order to allow aging tokens to be replaced in a timely manner, this value should be
|
|
52
53
|
* significantly shorter than both the minimum token lifetime that will be
|
|
53
|
-
* returned by the underlying API
|
|
54
|
+
* returned by the underlying API.
|
|
54
55
|
*
|
|
55
56
|
* Default is -1, disabling this behavior.
|
|
56
57
|
*/
|
|
57
58
|
autoRefreshSecs?: number;
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
-
* During auto-refresh, if the remaining lifetime for any token is below this threshold,
|
|
61
|
-
* force the provider to skip the local cache and go directly to the underlying provider for
|
|
62
|
-
* new tokens and refresh tokens.
|
|
63
|
-
*
|
|
64
|
-
* Default is -1, disabling this behavior.
|
|
65
|
-
*/
|
|
66
|
-
autoRefreshSkipCacheSecs?: number;
|
|
67
|
-
|
|
68
60
|
/**
|
|
69
61
|
* Scopes to request - if any - beyond the core `['openid', 'email']` scopes, which
|
|
70
62
|
* this client will always request.
|
|
@@ -89,13 +81,15 @@ export interface BaseOAuthClientConfig<S> {
|
|
|
89
81
|
* suitable concrete implementation to power a client-side OauthService. See `MsalClient` and
|
|
90
82
|
* `AuthZeroClient`
|
|
91
83
|
*
|
|
92
|
-
* Initialize such a service and this client within
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* flow as necessary.
|
|
84
|
+
* Initialize such a service and this client within an app's primary {@link HoistAuthModel} to use
|
|
85
|
+
* the tokens it acquires to authenticate with the Hoist server. (Note this requires a suitable
|
|
86
|
+
* server-side `AuthenticationService` implementation to validate the token and actually resolve
|
|
87
|
+
* the user.) On init, the client impl will initiate a pop-up or redirect flow as necessary.
|
|
97
88
|
*/
|
|
98
|
-
export abstract class BaseOAuthClient<
|
|
89
|
+
export abstract class BaseOAuthClient<
|
|
90
|
+
C extends BaseOAuthClientConfig<S>,
|
|
91
|
+
S extends AccessTokenSpec
|
|
92
|
+
> extends HoistBase {
|
|
99
93
|
/** Config loaded from UI server + init method. */
|
|
100
94
|
protected config: C;
|
|
101
95
|
|
|
@@ -121,7 +115,6 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
121
115
|
redirectUrl: 'APP_BASE_URL',
|
|
122
116
|
postLogoutRedirectUrl: 'APP_BASE_URL',
|
|
123
117
|
autoRefreshSecs: -1,
|
|
124
|
-
autoRefreshSkipCacheSecs: -1,
|
|
125
118
|
...config
|
|
126
119
|
};
|
|
127
120
|
throwIf(!config.clientId, 'Missing OAuth clientId. Please review your configuration.');
|
|
@@ -175,10 +168,10 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
/**
|
|
178
|
-
* Get all
|
|
171
|
+
* Get all configured tokens.
|
|
179
172
|
*/
|
|
180
|
-
async getAllTokensAsync(): Promise<TokenMap> {
|
|
181
|
-
return this.fetchAllTokensAsync(
|
|
173
|
+
async getAllTokensAsync(opts?: {eagerOnly?: boolean; useCache?: boolean}): Promise<TokenMap> {
|
|
174
|
+
return this.fetchAllTokensAsync(opts);
|
|
182
175
|
}
|
|
183
176
|
|
|
184
177
|
/**
|
|
@@ -314,12 +307,27 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
314
307
|
await never();
|
|
315
308
|
}
|
|
316
309
|
|
|
317
|
-
protected async fetchAllTokensAsync(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
310
|
+
protected async fetchAllTokensAsync(opts?: {
|
|
311
|
+
eagerOnly?: boolean;
|
|
312
|
+
useCache?: boolean;
|
|
313
|
+
}): Promise<TokenMap> {
|
|
314
|
+
const eagerOnly = opts?.eagerOnly ?? false,
|
|
315
|
+
useCache = opts?.useCache ?? true,
|
|
316
|
+
accessSpecs = eagerOnly
|
|
317
|
+
? pickBy(this.accessSpecs, spec => spec.fetchMode === 'eager')
|
|
318
|
+
: this.accessSpecs,
|
|
319
|
+
ret: TokenMap = {};
|
|
320
|
+
|
|
321
|
+
await Promise.allSettled(
|
|
322
|
+
map(accessSpecs, async (spec, key) => {
|
|
323
|
+
try {
|
|
324
|
+
ret[key] = await this.fetchAccessTokenAsync(spec, useCache);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
XH.handleException(e, {logOnServer: true, showAlert: false});
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
|
|
323
331
|
// Do this after getting any access tokens --which can also populate the idToken cache!
|
|
324
332
|
ret.id = await this.fetchIdTokenSafeAsync(useCache);
|
|
325
333
|
|
|
@@ -361,22 +369,19 @@ export abstract class BaseOAuthClient<C extends BaseOAuthClientConfig<S>, S> ext
|
|
|
361
369
|
private async onTimerAsync(): Promise<void> {
|
|
362
370
|
const {config, lastRefreshAttempt} = this,
|
|
363
371
|
refreshSecs = config.autoRefreshSecs * SECONDS,
|
|
364
|
-
skipCacheSecs =
|
|
372
|
+
skipCacheSecs = refreshSecs + 5 * SECONDS;
|
|
365
373
|
|
|
366
374
|
if (olderThan(lastRefreshAttempt, refreshSecs)) {
|
|
367
375
|
this.lastRefreshAttempt = Date.now();
|
|
368
376
|
try {
|
|
369
377
|
this.logDebug('Refreshing all tokens:');
|
|
370
378
|
let tokens = await this.fetchAllTokensAsync(),
|
|
371
|
-
aging = pickBy(
|
|
372
|
-
tokens,
|
|
373
|
-
v => skipCacheSecs > 0 && v.expiresWithin(skipCacheSecs)
|
|
374
|
-
);
|
|
379
|
+
aging = pickBy(tokens, v => v.expiresWithin(skipCacheSecs));
|
|
375
380
|
if (!isEmpty(aging)) {
|
|
376
381
|
this.logDebug(
|
|
377
382
|
`Tokens [${keys(aging).join(', ')}] have < ${skipCacheSecs}s remaining, reloading without cache.`
|
|
378
383
|
);
|
|
379
|
-
tokens = await this.fetchAllTokensAsync(false);
|
|
384
|
+
tokens = await this.fetchAllTokensAsync({useCache: false});
|
|
380
385
|
}
|
|
381
386
|
this.logTokensDebug(tokens);
|
|
382
387
|
} catch (e) {
|
package/security/Token.ts
CHANGED
|
@@ -11,8 +11,6 @@ import {jwtDecode} from 'jwt-decode';
|
|
|
11
11
|
import {getRelativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
|
|
12
12
|
import {isNil} from 'lodash';
|
|
13
13
|
|
|
14
|
-
export type TokenMap = Record<string, Token>;
|
|
15
|
-
|
|
16
14
|
export class Token {
|
|
17
15
|
readonly value: string;
|
|
18
16
|
readonly decoded: PlainObject;
|
|
@@ -0,0 +1,22 @@
|
|
|
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 © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {Token} from './Token';
|
|
9
|
+
|
|
10
|
+
export type TokenMap = Record<string, Token>;
|
|
11
|
+
|
|
12
|
+
export interface AccessTokenSpec {
|
|
13
|
+
/**
|
|
14
|
+
* Mode governing when the access token should be requested from provider.
|
|
15
|
+
* eager (default) - initiate lookup on initialization, but do not block on failure.
|
|
16
|
+
* lazy - lookup when requested by client.
|
|
17
|
+
*/
|
|
18
|
+
fetchMode: 'eager' | 'lazy';
|
|
19
|
+
|
|
20
|
+
/** Scopes for the desired access token.*/
|
|
21
|
+
scopes: string[];
|
|
22
|
+
}
|