@xh/hoist 86.3.0 → 86.4.1
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 +30 -0
- package/admin/AppModel.ts +35 -7
- package/admin/tabs/userData/roles/RoleModel.ts +11 -18
- package/admin/tabs/userData/roles/RolePanel.ts +0 -7
- package/build/types/admin/AppModel.d.ts +4 -0
- package/build/types/admin/tabs/userData/roles/RoleModel.d.ts +2 -2
- package/build/types/data/Store.d.ts +3 -2
- package/build/types/kit/swiper/index.d.ts +3 -4
- package/build/types/svc/PrefService.d.ts +12 -1
- package/build/types/utils/datetime/LocalDate.d.ts +23 -1
- package/data/Store.ts +3 -2
- package/desktop/cmp/filter/FilterChooser.scss +2 -1
- package/kit/golden-layout/impl/js/LayoutManager.js +6 -6
- package/kit/golden-layout/impl/js/controls/DragProxy.js +2 -2
- package/kit/golden-layout/impl/js/items/Root.js +2 -2
- package/kit/golden-layout/impl/js/utils/utils.js +14 -0
- package/package.json +3 -3
- package/svc/PrefService.ts +80 -20
- package/svc/README.md +6 -0
- package/utils/datetime/LocalDate.ts +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 86.4.1 - 2026-08-11
|
|
4
|
+
|
|
5
|
+
### 🐞 Bug Fixes
|
|
6
|
+
|
|
7
|
+
* Fixed `DashContainer` sizing regression introduced with the golden-layout fork in v86.0.0. CSS
|
|
8
|
+
padding applied to the container element was incorrectly counted as available layout space,
|
|
9
|
+
causing dashboard content to render oversized and cut off.
|
|
10
|
+
* Revert swiper library to previous version to address swiping regressions (#4559)
|
|
11
|
+
|
|
12
|
+
### 📚 Libraries
|
|
13
|
+
* swiper `12.1 -> 14.0`
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 86.4.0 - 2026-07-15
|
|
17
|
+
|
|
18
|
+
### 🎁 New Features
|
|
19
|
+
|
|
20
|
+
* Added `PrefService.isSet()` to report whether the current user has an explicit value on file for a
|
|
21
|
+
preference vs. receiving its server-side default - a distinction that cannot be reliably inferred
|
|
22
|
+
by comparing the value to the default. Requires a hoist-core version that emits the backing
|
|
23
|
+
`isSet` flag; against older servers all prefs report as unset.
|
|
24
|
+
|
|
25
|
+
### 🐞 Bug Fixes
|
|
26
|
+
|
|
27
|
+
* `PrefService.unset()` now performs a true server-side unset, clearing the user's stored value so
|
|
28
|
+
the preference reverts to its (possibly changing) default and `isSet()` reports `false`.
|
|
29
|
+
Previously it persisted the current default as an explicit user value. Falls back to the legacy
|
|
30
|
+
behavior against hoist-core versions that predate the `xh/unsetPrefs` endpoint.
|
|
31
|
+
* Fixed `FilterChooser` popover mode to render an opaque background when expanded.
|
|
32
|
+
|
|
3
33
|
## 86.3.0 - 2026-07-10
|
|
4
34
|
|
|
5
35
|
### 🎁 New Features
|
package/admin/AppModel.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {TabConfig, TabContainerModel} from '@xh/hoist/cmp/tab';
|
|
|
9
9
|
import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
|
|
10
10
|
import {HoistAppModel, HoistRoute, InitContext, XH} from '@xh/hoist/core';
|
|
11
11
|
import {Icon} from '@xh/hoist/icon';
|
|
12
|
+
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
12
13
|
import {without} from 'lodash';
|
|
14
|
+
import {RoleModuleConfig} from './tabs/userData/roles/Types';
|
|
13
15
|
import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
|
|
14
16
|
import {clientsPanel} from './tabs/clients/ClientsPanel';
|
|
15
17
|
import {monitorTab} from './tabs/monitor/MonitorTab';
|
|
@@ -27,6 +29,9 @@ export class AppModel extends HoistAppModel {
|
|
|
27
29
|
|
|
28
30
|
viewManagerModels: Record<string, ViewManagerModel> = {};
|
|
29
31
|
|
|
32
|
+
/** Role-module config, loaded once at init and shared with the Roles tab. */
|
|
33
|
+
roleModuleConfig: RoleModuleConfig = null;
|
|
34
|
+
|
|
30
35
|
static get readonly() {
|
|
31
36
|
return !XH.getUser().isHoistAdmin;
|
|
32
37
|
}
|
|
@@ -34,16 +39,18 @@ export class AppModel extends HoistAppModel {
|
|
|
34
39
|
constructor() {
|
|
35
40
|
super();
|
|
36
41
|
|
|
37
|
-
this.tabModel = new TabContainerModel({
|
|
38
|
-
route: 'default',
|
|
39
|
-
tabs: this.createTabs()
|
|
40
|
-
});
|
|
41
|
-
|
|
42
42
|
// Enable managed autosize mode across Hoist Admin console grids.
|
|
43
43
|
GridModel.defaults.autosizeMode = 'managed';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
override async initAsync(ctx: InitContext) {
|
|
47
|
+
await this.loadRoleModuleConfigAsync(ctx);
|
|
48
|
+
|
|
49
|
+
this.tabModel = new TabContainerModel({
|
|
50
|
+
route: 'default',
|
|
51
|
+
tabs: this.createTabs()
|
|
52
|
+
});
|
|
53
|
+
|
|
47
54
|
await this.initViewManagerModelsAsync(ctx);
|
|
48
55
|
await super.initAsync(ctx);
|
|
49
56
|
}
|
|
@@ -122,7 +129,8 @@ export class AppModel extends HoistAppModel {
|
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
createTabs(): TabConfig[] {
|
|
125
|
-
const conf = XH.getConf('xhAdminAppConfig', {})
|
|
132
|
+
const conf = XH.getConf('xhAdminAppConfig', {}),
|
|
133
|
+
rolesEnabled = this.roleModuleConfig?.enabled ?? false;
|
|
126
134
|
|
|
127
135
|
return [
|
|
128
136
|
{
|
|
@@ -159,6 +167,7 @@ export class AppModel extends HoistAppModel {
|
|
|
159
167
|
},
|
|
160
168
|
{
|
|
161
169
|
id: 'userData',
|
|
170
|
+
title: rolesEnabled ? 'User Data & Roles' : 'User Data',
|
|
162
171
|
icon: Icon.users(),
|
|
163
172
|
content: {
|
|
164
173
|
refreshMode: 'onShowAlways',
|
|
@@ -172,7 +181,8 @@ export class AppModel extends HoistAppModel {
|
|
|
172
181
|
{
|
|
173
182
|
id: 'roles',
|
|
174
183
|
icon: Icon.idBadge(),
|
|
175
|
-
content: rolePanel
|
|
184
|
+
content: rolePanel,
|
|
185
|
+
omit: !rolesEnabled
|
|
176
186
|
},
|
|
177
187
|
{
|
|
178
188
|
id: 'prefs',
|
|
@@ -219,4 +229,22 @@ export class AppModel extends HoistAppModel {
|
|
|
219
229
|
ctx
|
|
220
230
|
);
|
|
221
231
|
}
|
|
232
|
+
|
|
233
|
+
//----------------
|
|
234
|
+
// Implementation
|
|
235
|
+
//----------------
|
|
236
|
+
private async loadRoleModuleConfigAsync(ctx: InitContext) {
|
|
237
|
+
// Load role config up-front to title/show the Roles tab (see createTabs).
|
|
238
|
+
// Never block startup if it can't load - the tab defaults to hidden.
|
|
239
|
+
try {
|
|
240
|
+
this.roleModuleConfig = await this.runner(ctx)
|
|
241
|
+
.span('loadRoleModuleConfig')
|
|
242
|
+
.fetchJson({url: 'roleAdmin/config', timeout: 10 * SECONDS});
|
|
243
|
+
} catch (e) {
|
|
244
|
+
XH.handleException(e, {
|
|
245
|
+
message: 'Unable to load roles configuration',
|
|
246
|
+
alertType: 'toast'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
222
250
|
}
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {getAppModel} from '@xh/hoist/admin/AdminUtils';
|
|
7
8
|
import {RecategorizeDialogModel} from '@xh/hoist/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel';
|
|
8
9
|
import {FilterChooserModel} from '@xh/hoist/cmp/filter';
|
|
9
10
|
import {GridModel, tagsRenderer, TreeStyle} from '@xh/hoist/cmp/grid';
|
|
10
11
|
import * as Col from '@xh/hoist/cmp/grid/columns';
|
|
11
12
|
import {fragment, p} from '@xh/hoist/cmp/layout';
|
|
12
|
-
import {
|
|
13
|
+
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
13
14
|
import {RecordActionSpec} from '@xh/hoist/data';
|
|
14
15
|
import {actionCol, calcActionColWidth} from '@xh/hoist/desktop/cmp/grid';
|
|
15
16
|
import {Icon} from '@xh/hoist/icon';
|
|
@@ -39,10 +40,14 @@ export class RoleModel extends HoistModel {
|
|
|
39
40
|
@managed recategorizeDialogModel = new RecategorizeDialogModel(this);
|
|
40
41
|
|
|
41
42
|
@observable.ref allRoles: HoistRole[] = [];
|
|
42
|
-
@observable.ref moduleConfig: RoleModuleConfig;
|
|
43
43
|
|
|
44
44
|
@bindable showInGroups = true;
|
|
45
45
|
|
|
46
|
+
/** Role-module config - loaded at init. */
|
|
47
|
+
get moduleConfig(): RoleModuleConfig {
|
|
48
|
+
return getAppModel().roleModuleConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
get readonly() {
|
|
47
52
|
return !XH.getUser().isHoistRoleManager;
|
|
48
53
|
}
|
|
@@ -56,6 +61,10 @@ export class RoleModel extends HoistModel {
|
|
|
56
61
|
constructor() {
|
|
57
62
|
super();
|
|
58
63
|
makeObservable(this);
|
|
64
|
+
|
|
65
|
+
this.gridModel = this.createGridModel();
|
|
66
|
+
this.filterChooserModel = this.createFilterChooserModel();
|
|
67
|
+
|
|
59
68
|
this.addReaction({
|
|
60
69
|
track: () => this.showInGroups,
|
|
61
70
|
run: showInGroups => {
|
|
@@ -74,9 +83,6 @@ export class RoleModel extends HoistModel {
|
|
|
74
83
|
return this.runner({loadSpec})
|
|
75
84
|
.span('list')
|
|
76
85
|
.run(async ctx => {
|
|
77
|
-
await this.ensureInitializedAsync(ctx);
|
|
78
|
-
if (!this.moduleConfig.enabled) return;
|
|
79
|
-
|
|
80
86
|
const {data} = await XH.fetchJson({url: 'roleAdmin/list'}, ctx);
|
|
81
87
|
if (loadSpec.isStale) return;
|
|
82
88
|
|
|
@@ -233,19 +239,6 @@ export class RoleModel extends HoistModel {
|
|
|
233
239
|
gridModel.autosizeAsync({includeCollapsedChildren: true});
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
private async ensureInitializedAsync(ctx: CallContext) {
|
|
237
|
-
if (this.moduleConfig) return;
|
|
238
|
-
|
|
239
|
-
const config = await this.runner(ctx).fetchJson({url: 'roleAdmin/config'});
|
|
240
|
-
runInAction(() => {
|
|
241
|
-
this.moduleConfig = config;
|
|
242
|
-
if (config.enabled) {
|
|
243
|
-
this.gridModel = this.createGridModel();
|
|
244
|
-
this.filterChooserModel = this.createFilterChooserModel();
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
242
|
private processRolesFromServer(roles: Partial<HoistRole>[]): HoistRole[] {
|
|
250
243
|
return roles.map(role => {
|
|
251
244
|
const membersByType = mapValues(groupBy(role.members, 'type'), members =>
|
|
@@ -9,7 +9,6 @@ import {grid} from '@xh/hoist/cmp/grid';
|
|
|
9
9
|
import {fragment, hframe, vframe} from '@xh/hoist/cmp/layout';
|
|
10
10
|
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
11
11
|
import {button, colChooserButton} from '@xh/hoist/desktop/cmp/button';
|
|
12
|
-
import {errorMessage} from '@xh/hoist/cmp/error';
|
|
13
12
|
import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
|
|
14
13
|
import {switchInput} from '@xh/hoist/desktop/cmp/input';
|
|
15
14
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
@@ -25,12 +24,6 @@ export const rolePanel = hoistCmp.factory({
|
|
|
25
24
|
model: creates(RoleModel),
|
|
26
25
|
|
|
27
26
|
render({className, model}) {
|
|
28
|
-
const {moduleConfig} = model;
|
|
29
|
-
if (!moduleConfig) return null;
|
|
30
|
-
if (!moduleConfig.enabled) {
|
|
31
|
-
return errorMessage({error: 'Default Role Module not enabled.'});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
const {gridModel, readonly} = model;
|
|
35
28
|
return fragment(
|
|
36
29
|
panel({
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { TabConfig, TabContainerModel } from '@xh/hoist/cmp/tab';
|
|
2
2
|
import { ViewManagerModel } from '@xh/hoist/cmp/viewmanager';
|
|
3
3
|
import { HoistAppModel, HoistRoute, InitContext } from '@xh/hoist/core';
|
|
4
|
+
import { RoleModuleConfig } from './tabs/userData/roles/Types';
|
|
4
5
|
export declare class AppModel extends HoistAppModel {
|
|
5
6
|
tabModel: TabContainerModel;
|
|
6
7
|
viewManagerModels: Record<string, ViewManagerModel>;
|
|
8
|
+
/** Role-module config, loaded once at init and shared with the Roles tab. */
|
|
9
|
+
roleModuleConfig: RoleModuleConfig;
|
|
7
10
|
static get readonly(): boolean;
|
|
8
11
|
constructor();
|
|
9
12
|
initAsync(ctx: InitContext): Promise<void>;
|
|
@@ -15,4 +18,5 @@ export declare class AppModel extends HoistAppModel {
|
|
|
15
18
|
openPrimaryApp(): void;
|
|
16
19
|
getPrimaryAppCode(): string;
|
|
17
20
|
initViewManagerModelsAsync(ctx: InitContext): Promise<void>;
|
|
21
|
+
private loadRoleModuleConfigAsync;
|
|
18
22
|
}
|
|
@@ -19,8 +19,9 @@ export declare class RoleModel extends HoistModel {
|
|
|
19
19
|
readonly roleEditorModel: RoleEditorModel;
|
|
20
20
|
recategorizeDialogModel: RecategorizeDialogModel;
|
|
21
21
|
allRoles: HoistRole[];
|
|
22
|
-
moduleConfig: RoleModuleConfig;
|
|
23
22
|
showInGroups: boolean;
|
|
23
|
+
/** Role-module config - loaded at init. */
|
|
24
|
+
get moduleConfig(): RoleModuleConfig;
|
|
24
25
|
get readonly(): boolean;
|
|
25
26
|
get selectedRole(): HoistRole;
|
|
26
27
|
constructor();
|
|
@@ -36,7 +37,6 @@ export declare class RoleModel extends HoistModel {
|
|
|
36
37
|
private deleteAction;
|
|
37
38
|
private groupByAction;
|
|
38
39
|
private displayRoles;
|
|
39
|
-
private ensureInitializedAsync;
|
|
40
40
|
private processRolesFromServer;
|
|
41
41
|
private processRolesForTreeGrid;
|
|
42
42
|
private createGridModel;
|
|
@@ -267,8 +267,9 @@ export declare class Store extends HoistBase implements FilterBindTarget, Filter
|
|
|
267
267
|
* Add new Records to this Store in a local, uncommitted state - i.e. with data that has yet to
|
|
268
268
|
* be persisted back to, or sourced from, the server or other data source of record.
|
|
269
269
|
*
|
|
270
|
-
* Note that data objects passed to this method must include a
|
|
271
|
-
*
|
|
270
|
+
* Note that data objects passed to this method must include a literal `id` property - this
|
|
271
|
+
* method does *not* run the Store's `idSpec` function. Callers can generate an id with
|
|
272
|
+
* `XH.genId()` if no natural ID can be produced locally on the client.
|
|
272
273
|
*
|
|
273
274
|
* For StoreRecord additions that originate from the server, call `updateData()` instead.
|
|
274
275
|
*
|
|
@@ -27,7 +27,6 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
27
27
|
onScrollbarDragStart?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
28
28
|
onScrollbarDragMove?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
29
29
|
onScrollbarDragEnd?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
30
|
-
onVirtualUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
31
30
|
onZoomChange?: (swiper: import("swiper/types").Swiper, scale: number, imageEl: HTMLElement, slideEl: HTMLElement) => void;
|
|
32
31
|
onInit?: (swiper: import("swiper/types").Swiper) => any;
|
|
33
32
|
onBeforeDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -54,8 +53,8 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
54
53
|
onReachEnd?: (swiper: import("swiper/types").Swiper) => void;
|
|
55
54
|
onToEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
56
55
|
onFromEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
57
|
-
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number
|
|
58
|
-
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number
|
|
56
|
+
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number) => void;
|
|
57
|
+
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number) => void;
|
|
59
58
|
onResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
60
59
|
onObserverUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
61
60
|
onBeforeLoopFix?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -78,7 +77,7 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
78
77
|
onBeforeInit?: (swiper: import("swiper/types").Swiper) => void;
|
|
79
78
|
onBeforeResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
80
79
|
onBeforeSlideChangeStart?: (swiper: import("swiper/types").Swiper) => void;
|
|
81
|
-
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal:
|
|
80
|
+
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal: any) => void;
|
|
82
81
|
onChangeDirection?: (swiper: import("swiper/types").Swiper) => void;
|
|
83
82
|
onDoubleClick?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
84
83
|
onDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -25,6 +25,13 @@ export declare class PrefService extends HoistService {
|
|
|
25
25
|
* Check to see if a given preference has been *defined*.
|
|
26
26
|
*/
|
|
27
27
|
hasKey(key: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Check whether the current user has an explicit value on file for the given preference, vs.
|
|
30
|
+
* receiving the preference's server-side default value.
|
|
31
|
+
*
|
|
32
|
+
* @param key - unique key used to identify the pref.
|
|
33
|
+
*/
|
|
34
|
+
isSet(key: string): boolean;
|
|
28
35
|
/**
|
|
29
36
|
* Get the value for a given key, either the user-specific value (if set) or the default.
|
|
30
37
|
* Typically accessed via the convenience alias {@link XH.getPref}.
|
|
@@ -47,7 +54,10 @@ export declare class PrefService extends HoistService {
|
|
|
47
54
|
*/
|
|
48
55
|
set(key: string, value: any): void;
|
|
49
56
|
/**
|
|
50
|
-
* Restore a preference to its default value.
|
|
57
|
+
* Restore a preference to its default value, clearing the user's explicit value on the server.
|
|
58
|
+
*
|
|
59
|
+
* Unlike `set()`, this clears the user's explicit value rather than persisting the default as
|
|
60
|
+
* one - so {@link isSet} will report `false` afterwards. Saved asynchronously (see `set()`).
|
|
51
61
|
*/
|
|
52
62
|
unset(key: string): void;
|
|
53
63
|
/**
|
|
@@ -66,6 +76,7 @@ export declare class PrefService extends HoistService {
|
|
|
66
76
|
pushPendingAsync(): Promise<void>;
|
|
67
77
|
private pushPendingBuffered;
|
|
68
78
|
private loadPrefsAsync;
|
|
79
|
+
private ensureKeyExists;
|
|
69
80
|
private validateBeforeSet;
|
|
70
81
|
private valueIsOfType;
|
|
71
82
|
}
|
|
@@ -9,6 +9,15 @@ import { Moment, MomentInput } from 'moment';
|
|
|
9
9
|
* For efficiency and to enable strict equality checks, instances of this class are memoized:
|
|
10
10
|
* only a single version of the object will be created and returned for each calendar day,
|
|
11
11
|
* as long as the caller uses one of the *public factory methods*, which they always should!
|
|
12
|
+
*
|
|
13
|
+
* Instances serialize directly to their ISO date string (e.g. '2024-01-15') via built-in
|
|
14
|
+
* `toString()`, `valueOf()`, and `toJSON()` overrides. This means a LocalDate can be passed
|
|
15
|
+
* as-is within the params or body of a `FetchService` request (or any `JSON.stringify()` call)
|
|
16
|
+
* and will serialize as expected - prefer this over calling `toString()` or `format()` yourself.
|
|
17
|
+
*
|
|
18
|
+
* Instances also support natural comparison: because they are memoized, `===` tests whether two
|
|
19
|
+
* references are the same calendar day, while `valueOf()` returning the (lexically sortable) ISO
|
|
20
|
+
* string means the relational operators `<`, `>`, `<=`, `>=` order instances chronologically.
|
|
12
21
|
*/
|
|
13
22
|
export declare class LocalDate {
|
|
14
23
|
static readonly VALID_UNITS: Set<LocalDateUnit>;
|
|
@@ -33,7 +42,11 @@ export declare class LocalDate {
|
|
|
33
42
|
* @param val - any string, timestamp, or date parsable by moment.js.
|
|
34
43
|
*/
|
|
35
44
|
static from(val: MomentInput | LocalDate): LocalDate;
|
|
36
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* LocalDate representing the current day in the browser's local time zone.
|
|
47
|
+
* See `currentAppDay()` / `currentServerDay()` to resolve "today" in the app or server zone,
|
|
48
|
+
* which can differ from the browser for users in another region.
|
|
49
|
+
*/
|
|
37
50
|
static today(): LocalDate;
|
|
38
51
|
/** LocalDate representing the current day in the App TimeZone */
|
|
39
52
|
static currentAppDay(): LocalDate;
|
|
@@ -46,9 +59,17 @@ export declare class LocalDate {
|
|
|
46
59
|
/** Is the input value a local Date? */
|
|
47
60
|
static isLocalDate(val: any): boolean;
|
|
48
61
|
get isoString(): string;
|
|
62
|
+
/** JS `Date` for this day at midnight in the browser's local time zone. Fresh instance per call. */
|
|
49
63
|
get date(): Date;
|
|
64
|
+
/** A mutable moment.js clone - safe to modify without affecting this (immutable) instance. */
|
|
50
65
|
get moment(): Moment;
|
|
66
|
+
/** Epoch millis for this day at midnight in the browser's local time zone. */
|
|
51
67
|
get timestamp(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Format this date using moment.js format tokens, primarily for display.
|
|
70
|
+
* Note: to send a LocalDate to the server, pass the instance directly rather than a formatted
|
|
71
|
+
* string - it serializes to an ISO date on its own (see class-level docs).
|
|
72
|
+
*/
|
|
52
73
|
format(...args: any[]): string;
|
|
53
74
|
dayOfWeek(): string;
|
|
54
75
|
get isToday(): boolean;
|
|
@@ -83,6 +104,7 @@ export declare class LocalDate {
|
|
|
83
104
|
currentOrNextWeekday(): LocalDate;
|
|
84
105
|
/** The same date if already a weekday, or the previous weekday. */
|
|
85
106
|
currentOrPreviousWeekday(): LocalDate;
|
|
107
|
+
/** Difference between this date and `other` in the given unit; positive when this is later. */
|
|
86
108
|
diff(other: LocalDate, unit?: LocalDateUnit): number;
|
|
87
109
|
/** @internal - use one of the static factory methods instead. */
|
|
88
110
|
private constructor();
|
package/data/Store.ts
CHANGED
|
@@ -559,8 +559,9 @@ export class Store
|
|
|
559
559
|
* Add new Records to this Store in a local, uncommitted state - i.e. with data that has yet to
|
|
560
560
|
* be persisted back to, or sourced from, the server or other data source of record.
|
|
561
561
|
*
|
|
562
|
-
* Note that data objects passed to this method must include a
|
|
563
|
-
*
|
|
562
|
+
* Note that data objects passed to this method must include a literal `id` property - this
|
|
563
|
+
* method does *not* run the Store's `idSpec` function. Callers can generate an id with
|
|
564
|
+
* `XH.genId()` if no natural ID can be produced locally on the client.
|
|
564
565
|
*
|
|
565
566
|
* For StoreRecord additions that originate from the server, call `updateData()` instead.
|
|
566
567
|
*
|
|
@@ -210,8 +210,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
210
210
|
this.width = width;
|
|
211
211
|
this.height = height;
|
|
212
212
|
} else {
|
|
213
|
-
this.width = containerNode
|
|
214
|
-
this.height = containerNode
|
|
213
|
+
this.width = lm.utils.contentWidth( containerNode );
|
|
214
|
+
this.height = lm.utils.contentHeight( containerNode );
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
if( this.isInitialised === true ) {
|
|
@@ -219,8 +219,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
219
219
|
|
|
220
220
|
if( this._maximisedItem ) {
|
|
221
221
|
var maxNode = this._maximisedItem.element;
|
|
222
|
-
maxNode.style.width =
|
|
223
|
-
maxNode.style.height =
|
|
222
|
+
maxNode.style.width = lm.utils.contentWidth( containerNode ) + 'px';
|
|
223
|
+
maxNode.style.height = lm.utils.contentHeight( containerNode ) + 'px';
|
|
224
224
|
this._maximisedItem.callDownwards( 'setSize' );
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -379,8 +379,8 @@ lm.utils.copy( lm.LayoutManager.prototype, {
|
|
|
379
379
|
itemNode.classList.add( 'lm_maximised' );
|
|
380
380
|
itemNode.after( placeholderNode );
|
|
381
381
|
rootNode.prepend( itemNode );
|
|
382
|
-
itemNode.style.width =
|
|
383
|
-
itemNode.style.height =
|
|
382
|
+
itemNode.style.width = lm.utils.contentWidth( containerNode ) + 'px';
|
|
383
|
+
itemNode.style.height = lm.utils.contentHeight( containerNode ) + 'px';
|
|
384
384
|
contentItem.callDownwards( 'setSize' );
|
|
385
385
|
this._maximisedItem.emit( 'maximised' );
|
|
386
386
|
this.emit( 'stateChanged' );
|
|
@@ -62,8 +62,8 @@ lm.controls.DragProxy = function( x, y, dragListener, layoutManager, contentItem
|
|
|
62
62
|
|
|
63
63
|
this._minX = containerRect.left + window.scrollX;
|
|
64
64
|
this._minY = containerRect.top + window.scrollY;
|
|
65
|
-
this._maxX = containerNode
|
|
66
|
-
this._maxY = containerNode
|
|
65
|
+
this._maxX = lm.utils.contentWidth(containerNode) + this._minX;
|
|
66
|
+
this._maxY = lm.utils.contentHeight(containerNode) + this._minY;
|
|
67
67
|
this._width = this.element.clientWidth;
|
|
68
68
|
this._height = this.element.clientHeight;
|
|
69
69
|
|
|
@@ -29,8 +29,8 @@ lm.utils.copy( lm.items.Root.prototype, {
|
|
|
29
29
|
|
|
30
30
|
setSize: function( width, height ) {
|
|
31
31
|
var containerNode = this._containerElement;
|
|
32
|
-
width = ( typeof width === 'undefined' ) ?
|
|
33
|
-
height = ( typeof height === 'undefined' ) ?
|
|
32
|
+
width = ( typeof width === 'undefined' ) ? lm.utils.contentWidth( containerNode ) : width;
|
|
33
|
+
height = ( typeof height === 'undefined' ) ? lm.utils.contentHeight( containerNode ) : height;
|
|
34
34
|
|
|
35
35
|
this.element.style.width = width + 'px';
|
|
36
36
|
this.element.style.height = height + 'px';
|
|
@@ -159,6 +159,20 @@ lm.utils.getUniqueId = function() {
|
|
|
159
159
|
.replace( '.', '' );
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Content-box dimensions - equivalent to jQuery's .width()/.height(),
|
|
164
|
+
* which the pre-fork code used. clientWidth/Height include CSS padding.
|
|
165
|
+
*/
|
|
166
|
+
lm.utils.contentWidth = function( node ) {
|
|
167
|
+
var style = getComputedStyle( node );
|
|
168
|
+
return node.clientWidth - parseFloat( style.paddingLeft ) - parseFloat( style.paddingRight );
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
lm.utils.contentHeight = function( node ) {
|
|
172
|
+
var style = getComputedStyle( node );
|
|
173
|
+
return node.clientHeight - parseFloat( style.paddingTop ) - parseFloat( style.paddingBottom );
|
|
174
|
+
};
|
|
175
|
+
|
|
162
176
|
/**
|
|
163
177
|
* A basic XSS filter. It is ultimately up to the
|
|
164
178
|
* implementing developer to make sure their particular
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "86.
|
|
3
|
+
"version": "86.4.1",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@auth0/auth0-spa-js": "~2.23.0",
|
|
42
|
-
"@azure/msal-browser": "~5.
|
|
42
|
+
"@azure/msal-browser": "~5.17.0",
|
|
43
43
|
"@blueprintjs/core": "^6.3.2",
|
|
44
44
|
"@blueprintjs/datetime": "^6.0.6",
|
|
45
45
|
"@codemirror/commands": "^6.10.3",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"semver": "~7.8.0",
|
|
91
91
|
"short-unique-id": "~5.3.2",
|
|
92
92
|
"store2": "~2.14.3",
|
|
93
|
-
"swiper": "^
|
|
93
|
+
"swiper": "^12.1.2",
|
|
94
94
|
"ts-morph": "^27.0.2",
|
|
95
95
|
"tsx": "^4.21.0",
|
|
96
96
|
"ua-parser-js": "~2.0.4",
|
package/svc/PrefService.ts
CHANGED
|
@@ -30,9 +30,8 @@ export class PrefService extends HoistService {
|
|
|
30
30
|
override telemetryPrefix = 'xh.client.prefs';
|
|
31
31
|
|
|
32
32
|
static instance: PrefService;
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
private _updates = {};
|
|
33
|
+
private _data: Record<string, PrefEntry> = {};
|
|
34
|
+
private _updates: Record<string, any> = {}; // undefined indicates unset
|
|
36
35
|
|
|
37
36
|
override async initAsync(ctx: InitContext) {
|
|
38
37
|
// Flush on page teardown while the page is still alive.
|
|
@@ -52,6 +51,17 @@ export class PrefService extends HoistService {
|
|
|
52
51
|
return this._data.hasOwnProperty(key);
|
|
53
52
|
}
|
|
54
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Check whether the current user has an explicit value on file for the given preference, vs.
|
|
56
|
+
* receiving the preference's server-side default value.
|
|
57
|
+
*
|
|
58
|
+
* @param key - unique key used to identify the pref.
|
|
59
|
+
*/
|
|
60
|
+
isSet(key: string): boolean {
|
|
61
|
+
this.ensureKeyExists(key);
|
|
62
|
+
return !!this._data[key].isSet;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
/**
|
|
56
66
|
* Get the value for a given key, either the user-specific value (if set) or the default.
|
|
57
67
|
* Typically accessed via the convenience alias {@link XH.getPref}.
|
|
@@ -91,7 +101,9 @@ export class PrefService extends HoistService {
|
|
|
91
101
|
|
|
92
102
|
// Change local value to sanitized copy and fire.
|
|
93
103
|
value = deepFreeze(cloneDeep(value));
|
|
94
|
-
this._data[key]
|
|
104
|
+
const pref = this._data[key];
|
|
105
|
+
pref.value = value;
|
|
106
|
+
pref.isSet = true;
|
|
95
107
|
|
|
96
108
|
// Schedule serialization to storage
|
|
97
109
|
this._updates[key] = value;
|
|
@@ -99,11 +111,22 @@ export class PrefService extends HoistService {
|
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
/**
|
|
102
|
-
* Restore a preference to its default value.
|
|
114
|
+
* Restore a preference to its default value, clearing the user's explicit value on the server.
|
|
115
|
+
*
|
|
116
|
+
* Unlike `set()`, this clears the user's explicit value rather than persisting the default as
|
|
117
|
+
* one - so {@link isSet} will report `false` afterwards. Saved asynchronously (see `set()`).
|
|
103
118
|
*/
|
|
104
119
|
unset(key: string) {
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
this.ensureKeyExists(key);
|
|
121
|
+
const pref = this._data[key];
|
|
122
|
+
if (!pref.isSet && isEqual(pref.value, pref.defaultValue)) return;
|
|
123
|
+
|
|
124
|
+
pref.value = pref.defaultValue;
|
|
125
|
+
pref.isSet = false;
|
|
126
|
+
|
|
127
|
+
// Schedule serialization to storage
|
|
128
|
+
this._updates[key] = undefined;
|
|
129
|
+
this.pushPendingBuffered();
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/**
|
|
@@ -131,18 +154,44 @@ export class PrefService extends HoistService {
|
|
|
131
154
|
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
132
155
|
this._updates = {};
|
|
133
156
|
|
|
157
|
+
// Partition into value updates and unsets.
|
|
158
|
+
// On a core that predates unset support, fall back to persisting default
|
|
159
|
+
const setPrefs = {},
|
|
160
|
+
unsetKeys = [];
|
|
161
|
+
forEach(updates, (value, key) => {
|
|
162
|
+
const pref = this._data[key];
|
|
163
|
+
if (value !== undefined) {
|
|
164
|
+
setPrefs[key] = value;
|
|
165
|
+
} else if (pref.hasOwnProperty('isSet')) {
|
|
166
|
+
unsetKeys.push(key);
|
|
167
|
+
} else {
|
|
168
|
+
setPrefs[key] = pref.defaultValue;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
134
172
|
await this.runner()
|
|
135
|
-
.span('
|
|
136
|
-
.run(ctx =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
.span('update')
|
|
174
|
+
.run(async ctx => {
|
|
175
|
+
const clientUsername = XH.getUsername(),
|
|
176
|
+
tasks = [];
|
|
177
|
+
if (!isEmpty(setPrefs)) {
|
|
178
|
+
tasks.push(
|
|
179
|
+
terminationSafePostJson(
|
|
180
|
+
{url: 'xh/setPrefs', body: setPrefs, params: {clientUsername}},
|
|
181
|
+
ctx
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (!isEmpty(unsetKeys)) {
|
|
186
|
+
tasks.push(
|
|
187
|
+
terminationSafePostJson(
|
|
188
|
+
{url: 'xh/unsetPrefs', body: unsetKeys, params: {clientUsername}},
|
|
189
|
+
ctx
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
await Promise.all(tasks);
|
|
194
|
+
});
|
|
146
195
|
}
|
|
147
196
|
|
|
148
197
|
//-------------------
|
|
@@ -172,9 +221,13 @@ export class PrefService extends HoistService {
|
|
|
172
221
|
});
|
|
173
222
|
}
|
|
174
223
|
|
|
175
|
-
private
|
|
224
|
+
private ensureKeyExists(key: string) {
|
|
225
|
+
throwIf(!this.hasKey(key), `Preference key not found: '${key}'`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private validateBeforeSet(key: string, value: any) {
|
|
229
|
+
this.ensureKeyExists(key);
|
|
176
230
|
const pref = this._data[key];
|
|
177
|
-
throwIf(!pref, `Cannot set preference ${key}: not found`);
|
|
178
231
|
throwIf(value === undefined, `Cannot set preference ${key}: value not defined`);
|
|
179
232
|
throwIf(
|
|
180
233
|
!this.valueIsOfType(value, pref.type),
|
|
@@ -201,3 +254,10 @@ export class PrefService extends HoistService {
|
|
|
201
254
|
}
|
|
202
255
|
}
|
|
203
256
|
}
|
|
257
|
+
|
|
258
|
+
interface PrefEntry {
|
|
259
|
+
type: string;
|
|
260
|
+
value: any;
|
|
261
|
+
defaultValue: any;
|
|
262
|
+
isSet: boolean;
|
|
263
|
+
}
|
package/svc/README.md
CHANGED
|
@@ -186,6 +186,12 @@ XH.setPref('gridPageSize', 100);
|
|
|
186
186
|
|
|
187
187
|
// Immediate save - no alias, access service directly
|
|
188
188
|
await XH.prefService.pushAsync('criticalPref', value);
|
|
189
|
+
|
|
190
|
+
// Distinguish an explicit user value from the server-side default
|
|
191
|
+
if (XH.prefService.isSet('gridPageSize')) { /* user has customized this */ }
|
|
192
|
+
|
|
193
|
+
// Clear the user's value, reverting to the default (real server-side unset)
|
|
194
|
+
XH.prefService.unset('gridPageSize');
|
|
189
195
|
```
|
|
190
196
|
|
|
191
197
|
Preferences are type-validated against server-defined types: `string`, `int`, `long`, `double`,
|
|
@@ -18,6 +18,15 @@ import moment, {Moment, MomentInput} from 'moment';
|
|
|
18
18
|
* For efficiency and to enable strict equality checks, instances of this class are memoized:
|
|
19
19
|
* only a single version of the object will be created and returned for each calendar day,
|
|
20
20
|
* as long as the caller uses one of the *public factory methods*, which they always should!
|
|
21
|
+
*
|
|
22
|
+
* Instances serialize directly to their ISO date string (e.g. '2024-01-15') via built-in
|
|
23
|
+
* `toString()`, `valueOf()`, and `toJSON()` overrides. This means a LocalDate can be passed
|
|
24
|
+
* as-is within the params or body of a `FetchService` request (or any `JSON.stringify()` call)
|
|
25
|
+
* and will serialize as expected - prefer this over calling `toString()` or `format()` yourself.
|
|
26
|
+
*
|
|
27
|
+
* Instances also support natural comparison: because they are memoized, `===` tests whether two
|
|
28
|
+
* references are the same calendar day, while `valueOf()` returning the (lexically sortable) ISO
|
|
29
|
+
* string means the relational operators `<`, `>`, `<=`, `>=` order instances chronologically.
|
|
21
30
|
*/
|
|
22
31
|
export class LocalDate {
|
|
23
32
|
static readonly VALID_UNITS: Set<LocalDateUnit> = new Set([
|
|
@@ -77,7 +86,11 @@ export class LocalDate {
|
|
|
77
86
|
return this.get(m.format('YYYY-MM-DD'));
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* LocalDate representing the current day in the browser's local time zone.
|
|
91
|
+
* See `currentAppDay()` / `currentServerDay()` to resolve "today" in the app or server zone,
|
|
92
|
+
* which can differ from the browser for users in another region.
|
|
93
|
+
*/
|
|
81
94
|
static today(): LocalDate {
|
|
82
95
|
return this.from(moment());
|
|
83
96
|
}
|
|
@@ -120,18 +133,26 @@ export class LocalDate {
|
|
|
120
133
|
return this._isoString;
|
|
121
134
|
}
|
|
122
135
|
|
|
136
|
+
/** JS `Date` for this day at midnight in the browser's local time zone. Fresh instance per call. */
|
|
123
137
|
get date(): Date {
|
|
124
138
|
return new Date(this.timestamp);
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
/** A mutable moment.js clone - safe to modify without affecting this (immutable) instance. */
|
|
127
142
|
get moment(): Moment {
|
|
128
143
|
return this._moment.clone();
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
/** Epoch millis for this day at midnight in the browser's local time zone. */
|
|
131
147
|
get timestamp(): number {
|
|
132
148
|
return this._date.getTime();
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Format this date using moment.js format tokens, primarily for display.
|
|
153
|
+
* Note: to send a LocalDate to the server, pass the instance directly rather than a formatted
|
|
154
|
+
* string - it serializes to an ISO date on its own (see class-level docs).
|
|
155
|
+
*/
|
|
135
156
|
format(...args): string {
|
|
136
157
|
return this._moment.format(...args);
|
|
137
158
|
}
|
|
@@ -186,6 +207,7 @@ export class LocalDate {
|
|
|
186
207
|
return this._isoString;
|
|
187
208
|
}
|
|
188
209
|
|
|
210
|
+
// Returns the ISO string (not a number) so the relational operators sort instances by date.
|
|
189
211
|
valueOf(): string {
|
|
190
212
|
return this._isoString;
|
|
191
213
|
}
|
|
@@ -319,6 +341,7 @@ export class LocalDate {
|
|
|
319
341
|
return this.isWeekday ? this : this.previousWeekday();
|
|
320
342
|
}
|
|
321
343
|
|
|
344
|
+
/** Difference between this date and `other` in the given unit; positive when this is later. */
|
|
322
345
|
diff(other: LocalDate, unit: LocalDateUnit = 'days'): number {
|
|
323
346
|
this.ensureUnitValid(unit);
|
|
324
347
|
return this._moment.diff(other._moment, unit);
|