@xh/hoist 86.2.0 → 86.4.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/.gitattributes +4 -0
- package/CHANGELOG.md +75 -0
- package/admin/AppModel.ts +38 -11
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
- package/admin/tabs/clients/ClientsModel.ts +4 -1
- package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
- package/admin/tabs/userData/roles/RoleModel.ts +11 -18
- package/admin/tabs/userData/roles/RolePanel.ts +2 -9
- package/appcontainer/RouterModel.ts +6 -6
- package/build/types/admin/AppModel.d.ts +7 -4
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
- package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
- package/build/types/admin/tabs/userData/roles/RoleModel.d.ts +2 -2
- package/build/types/appcontainer/RouterModel.d.ts +5 -5
- package/build/types/cmp/grid/Grid.d.ts +2 -1
- package/build/types/cmp/grid/GridModel.d.ts +2 -2
- package/build/types/cmp/grid/Types.d.ts +8 -1
- package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
- package/build/types/core/HoistAppModel.d.ts +2 -3
- package/build/types/core/types/Types.d.ts +12 -0
- package/build/types/data/Store.d.ts +3 -2
- package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
- package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
- package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
- package/build/types/desktop/cmp/input/Select.d.ts +8 -0
- package/build/types/kit/react-dropzone/index.d.ts +1 -0
- package/build/types/kit/swiper/index.d.ts +4 -3
- package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
- package/build/types/mobile/cmp/input/Select.d.ts +8 -0
- package/build/types/svc/PrefService.d.ts +12 -1
- package/build/types/utils/datetime/LocalDate.d.ts +23 -1
- package/cmp/grid/Grid.ts +10 -2
- package/cmp/grid/GridModel.ts +4 -3
- package/cmp/grid/Types.ts +9 -1
- package/cmp/grid/impl/MenuSupport.ts +1 -1
- package/cmp/input/SegmentedControlOption.ts +14 -0
- package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
- package/core/HoistAppModel.ts +2 -3
- package/core/types/Types.ts +14 -5
- package/data/Store.ts +3 -2
- package/desktop/cmp/filechooser/FileChooser.ts +6 -3
- package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
- package/desktop/cmp/filter/FilterChooser.scss +42 -0
- package/desktop/cmp/filter/FilterChooser.ts +147 -7
- package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
- package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
- package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
- package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
- package/desktop/cmp/input/SegmentedControl.ts +7 -3
- package/desktop/cmp/input/Select.ts +24 -16
- package/docs/routing.md +4 -2
- package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
- package/kit/react-dropzone/index.ts +1 -0
- package/mcp/README.md +12 -0
- package/mcp/data/ts-registry.spec.ts +156 -0
- package/mcp/data/ts-registry.ts +13 -6
- package/mcp/formatters/typescript.ts +7 -3
- package/mcp/util/paths.ts +21 -0
- package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
- package/mobile/cmp/input/SegmentedControl.ts +7 -3
- package/mobile/cmp/input/Select.ts +21 -15
- package/mobx/README.md +2 -1
- package/package.json +5 -5
- package/svc/PrefService.ts +80 -20
- package/svc/README.md +6 -0
- package/utils/datetime/LocalDate.ts +24 -1
- package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import {z} from 'zod';
|
|
13
13
|
import type {MemberInfo, MemberIndexEntry, SymbolEntry, SymbolDetail} from '../data/ts-registry.js';
|
|
14
|
-
import {
|
|
14
|
+
import {resolveRepoRootPosix, toPosixPath} from '../util/paths.js';
|
|
15
15
|
|
|
16
16
|
/** Remove blank lines from a JSDoc string to produce more compact output. */
|
|
17
17
|
function collapseJsDoc(jsDoc: string): string {
|
|
@@ -31,8 +31,12 @@ export function truncateType(typeStr: string): string {
|
|
|
31
31
|
|
|
32
32
|
/** Convert an absolute file path to a repo-relative path. */
|
|
33
33
|
export function toRelativePath(filePath: string): string {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Symbol filePaths originate from ts-morph (forward slashes on all platforms);
|
|
35
|
+
// compare in POSIX form so the repo-root prefix strips correctly on Windows,
|
|
36
|
+
// where `resolveRepoRoot()` would otherwise yield a backslash path.
|
|
37
|
+
const root = resolveRepoRootPosix();
|
|
38
|
+
const posix = toPosixPath(filePath);
|
|
39
|
+
return posix.startsWith(root) ? posix.slice(root.length + 1) : posix;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
package/mcp/util/paths.ts
CHANGED
|
@@ -39,6 +39,27 @@ export function resolveRepoRoot(): string {
|
|
|
39
39
|
return repoRoot;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Convert a filesystem path to POSIX (forward-slash) form. */
|
|
43
|
+
export function toPosixPath(p: string): string {
|
|
44
|
+
return p.replace(/\\/g, '/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Repo root in POSIX (forward-slash) form, matching ts-morph's `getFilePath()`
|
|
49
|
+
* convention, which always reports forward slashes on every platform.
|
|
50
|
+
*
|
|
51
|
+
* On Windows, {@link resolveRepoRoot} returns a backslash path (e.g.
|
|
52
|
+
* `D:\hoist-react`) while ts-morph reports `D:/hoist-react/...`; comparing or
|
|
53
|
+
* slicing the two directly fails, silently filtering out every source file and
|
|
54
|
+
* yielding an empty symbol index. Use this whenever comparing against or
|
|
55
|
+
* slicing a ts-morph source-file path (see `data/ts-registry.ts` and
|
|
56
|
+
* `formatters/typescript.ts`). Filesystem access that stays within Node's
|
|
57
|
+
* `path` APIs should keep using {@link resolveRepoRoot}.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveRepoRootPosix(): string {
|
|
60
|
+
return toPosixPath(resolveRepoRoot());
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/**
|
|
43
64
|
* Resolve the `@xh/hoist` library version from the repo root `package.json`.
|
|
44
65
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
7
|
+
import {GridModel, IColChooserModel} from '@xh/hoist/cmp/grid';
|
|
8
8
|
import {HoistModel, HSide, XH} from '@xh/hoist/core';
|
|
9
9
|
import '@xh/hoist/mobile/register';
|
|
10
10
|
import {bindable, action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
@@ -17,7 +17,7 @@ import {clone, find, sortBy} from 'lodash';
|
|
|
17
17
|
* It is not necessary to manually create instances of this class within an application.
|
|
18
18
|
* @internal
|
|
19
19
|
*/
|
|
20
|
-
export class ColChooserModel extends HoistModel {
|
|
20
|
+
export class ColChooserModel extends HoistModel implements IColChooserModel {
|
|
21
21
|
override xhImpl = true;
|
|
22
22
|
|
|
23
23
|
gridModel: GridModel;
|
|
@@ -85,6 +85,7 @@ export const [SegmentedControl, segmentedControl] = hoistCmp.withFactory<Segment
|
|
|
85
85
|
interface NormalizedOption extends SegmentedControlOption {
|
|
86
86
|
label: string;
|
|
87
87
|
intent?: Intent;
|
|
88
|
+
testId?: string;
|
|
88
89
|
_key: string;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -97,13 +98,14 @@ class SegmentedControlModel extends HoistInputModel {
|
|
|
97
98
|
return options.map((o: any, idx: number) => {
|
|
98
99
|
const key = String(idx);
|
|
99
100
|
if (isObject(o)) {
|
|
100
|
-
const {label, value, icon, disabled, intent} = o as SegmentedControlOption;
|
|
101
|
+
const {label, value, icon, disabled, intent, testId} = o as SegmentedControlOption;
|
|
101
102
|
return {
|
|
102
103
|
value: this.toInternal(value),
|
|
103
104
|
label: label ?? (icon ? '' : String(value)),
|
|
104
105
|
icon,
|
|
105
106
|
disabled,
|
|
106
107
|
intent,
|
|
108
|
+
testId,
|
|
107
109
|
_key: key
|
|
108
110
|
};
|
|
109
111
|
} else {
|
|
@@ -168,7 +170,8 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
|
|
|
168
170
|
|
|
169
171
|
const buttons = model.normalizedOptions.map(opt => {
|
|
170
172
|
const optIntent = opt.intent ?? defaultIntent,
|
|
171
|
-
selected = opt._key === selectedKey
|
|
173
|
+
selected = opt._key === selectedKey,
|
|
174
|
+
optTestId = opt.testId ?? (testId ? `${testId}-${String(opt.value)}` : null);
|
|
172
175
|
// Wrap the label so it can truncate with an ellipsis when the segment is too narrow,
|
|
173
176
|
// rather than hard-clipping mid-character. Pass null for icon-only options so the Button
|
|
174
177
|
// renders the icon alone (an empty span would suppress that).
|
|
@@ -187,7 +190,8 @@ const cmp = hoistCmp.factory<SegmentedControlModel>(({model, className, ...props
|
|
|
187
190
|
selected && 'xh-segmented-control-option--selected',
|
|
188
191
|
optIntent && `xh-segmented-control-option--${optIntent}`
|
|
189
192
|
),
|
|
190
|
-
onClick: () => model.onValueChange(opt._key)
|
|
193
|
+
onClick: () => model.onValueChange(opt._key),
|
|
194
|
+
testId: optTestId
|
|
191
195
|
});
|
|
192
196
|
});
|
|
193
197
|
|
|
@@ -21,7 +21,7 @@ import {action, bindable, makeObservable, observable, override} from '@xh/hoist/
|
|
|
21
21
|
import {debouncePromise, wait} from '@xh/hoist/promise';
|
|
22
22
|
import {throwIf, withDefault, mergeDeep} from '@xh/hoist/utils/js';
|
|
23
23
|
import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
|
|
24
|
-
import {escapeRegExp, isEqual, isNil, isPlainObject,
|
|
24
|
+
import {escapeRegExp, isEqual, isNil, isPlainObject, unionWith} from 'lodash';
|
|
25
25
|
import {Children, ReactNode, ReactPortal} from 'react';
|
|
26
26
|
import ReactDom from 'react-dom';
|
|
27
27
|
import './Select.scss';
|
|
@@ -157,6 +157,15 @@ export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps {
|
|
|
157
157
|
|
|
158
158
|
/** Field on provided options for sourcing each option's value (default `value`). */
|
|
159
159
|
valueField?: string;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Function to generate a `SelectOption` for a (non-null) selected value not present in the
|
|
163
|
+
* current options list. Return null to fall back to the default value-as-label behavior.
|
|
164
|
+
*
|
|
165
|
+
* Useful with queryFn-based selects, readonly forms, or any case where options may not be
|
|
166
|
+
* loaded when a value is set, ensuring the value renders with its proper label.
|
|
167
|
+
*/
|
|
168
|
+
generateOptionFn?: (value: any) => SelectOption;
|
|
160
169
|
}
|
|
161
170
|
|
|
162
171
|
/**
|
|
@@ -377,7 +386,8 @@ class SelectInputModel extends HoistInputModel {
|
|
|
377
386
|
|
|
378
387
|
// Convert external value into option object(s). Options created if missing - this takes the
|
|
379
388
|
// external value from the model, and we will respect that even if we don't know about it.
|
|
380
|
-
// (Exception for a null value, which
|
|
389
|
+
// (Exception for a null value, which is never synthesized - accepted only if provided via
|
|
390
|
+
// options.)
|
|
381
391
|
override toInternal(external) {
|
|
382
392
|
return this.findOption(external, !isNil(external));
|
|
383
393
|
}
|
|
@@ -393,7 +403,10 @@ class SelectInputModel extends HoistInputModel {
|
|
|
393
403
|
}
|
|
394
404
|
}
|
|
395
405
|
|
|
396
|
-
|
|
406
|
+
if (!createIfNotFound) return null;
|
|
407
|
+
|
|
408
|
+
// Value not among options - let the app generate an option for it, else synthesize one.
|
|
409
|
+
return this.componentProps.generateOptionFn?.(value) ?? this.valueToOption(value);
|
|
397
410
|
}
|
|
398
411
|
|
|
399
412
|
override toExternal(internal) {
|
|
@@ -453,15 +466,9 @@ class SelectInputModel extends HoistInputModel {
|
|
|
453
466
|
|
|
454
467
|
// Carry forward and add to any existing internalOpts to allow our value
|
|
455
468
|
// converters to continue all selected values in multiMode.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
this.internalOptions.forEach(currOpt => {
|
|
460
|
-
const matchOpt = matchesByVal[currOpt.value];
|
|
461
|
-
if (!matchOpt) newOpts.push(currOpt); // avoiding dupes
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
this.internalOptions = newOpts;
|
|
469
|
+
this.internalOptions = unionWith(matchOpts, this.internalOptions, (a, b) =>
|
|
470
|
+
isEqual(a.value, b.value)
|
|
471
|
+
);
|
|
465
472
|
|
|
466
473
|
// But only return the matching options back to the combo.
|
|
467
474
|
return matchOpts;
|
|
@@ -484,8 +491,7 @@ class SelectInputModel extends HoistInputModel {
|
|
|
484
491
|
// Option Rendering
|
|
485
492
|
//----------------------
|
|
486
493
|
formatOptionLabel = (opt, params) => {
|
|
487
|
-
//
|
|
488
|
-
// If we need to expose customization here, we could consider a dedicated prop.
|
|
494
|
+
// Display the standard label string in the value container (context == 'value').
|
|
489
495
|
if (params.context !== 'menu') {
|
|
490
496
|
return opt.label;
|
|
491
497
|
}
|
|
@@ -501,7 +507,7 @@ class SelectInputModel extends HoistInputModel {
|
|
|
501
507
|
return div(opt.label);
|
|
502
508
|
}
|
|
503
509
|
|
|
504
|
-
return this.externalValue
|
|
510
|
+
return isEqual(this.externalValue, opt.value)
|
|
505
511
|
? hbox({
|
|
506
512
|
items: [
|
|
507
513
|
div({
|
package/mobx/README.md
CHANGED
|
@@ -45,7 +45,8 @@ The following are re-exported from MobX and mobx-react-lite:
|
|
|
45
45
|
|
|
46
46
|
The package configures MobX with `enforceActions: 'observed'`, meaning any modification to
|
|
47
47
|
an observable property that is currently being observed must occur inside an `@action` method,
|
|
48
|
-
`runInAction()` block,
|
|
48
|
+
`runInAction()` block, `@bindable` setter, or a `reaction`/`when` run callback (MobX
|
|
49
|
+
auto-wraps these in an action). MobX logs a warning if this rule is violated.
|
|
49
50
|
|
|
50
51
|
This enforcement prevents accidental state mutations and makes data flow predictable.
|
|
51
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "86.
|
|
3
|
+
"version": "86.4.0",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@auth0/auth0-spa-js": "~2.
|
|
42
|
-
"@azure/msal-browser": "~5.
|
|
41
|
+
"@auth0/auth0-spa-js": "~2.23.0",
|
|
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": "^14.0.0",
|
|
94
94
|
"ts-morph": "^27.0.2",
|
|
95
95
|
"tsx": "^4.21.0",
|
|
96
96
|
"ua-parser-js": "~2.0.4",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"react-dom": "~18.2.0",
|
|
119
119
|
"stylelint": "17.x",
|
|
120
120
|
"stylelint-config-standard-scss": "17.x",
|
|
121
|
-
"type-fest": "
|
|
121
|
+
"type-fest": "5.x",
|
|
122
122
|
"typescript": "~5.9.3"
|
|
123
123
|
},
|
|
124
124
|
"resolutions": {
|
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);
|
|
@@ -1,42 +0,0 @@
|
|
|
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 © 2026 Extremely Heavy Industries Inc.
|
|
6
|
-
*/
|
|
7
|
-
.xh-popover-filter-chooser {
|
|
8
|
-
& > .bp6-popover-target {
|
|
9
|
-
display: flex;
|
|
10
|
-
flex: 1;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Extra class names required to override the default styles of the popover
|
|
14
|
-
&__popover.bp6-popover.bp6-minimal {
|
|
15
|
-
margin-top: -15px !important;
|
|
16
|
-
box-shadow: none;
|
|
17
|
-
|
|
18
|
-
.bp6-popover-content {
|
|
19
|
-
background: transparent;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.xh-select__value-container--is-multi {
|
|
23
|
-
height: unset;
|
|
24
|
-
line-height: unset;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
&__filter-chooser {
|
|
29
|
-
.xh-select__value-container--is-multi {
|
|
30
|
-
overflow-y: hidden !important;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.xh-select {
|
|
34
|
-
.xh-select__control--is-disabled {
|
|
35
|
-
background: var(--xh-input-bg);
|
|
36
|
-
}
|
|
37
|
-
.xh-select__multi-value--is-disabled .xh-select__multi-value__label {
|
|
38
|
-
color: unset;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|