@xh/hoist 86.0.0 → 86.1.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 +73 -0
- package/admin/tabs/userData/roles/RoleModel.ts +1 -19
- package/appcontainer/RouterModel.ts +2 -0
- package/appcontainer/ThemeModel.ts +41 -0
- package/appcontainer/login/LoginPanelModel.ts +6 -2
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +9 -0
- package/build/types/cmp/input/SegmentedControlOption.d.ts +44 -0
- package/build/types/cmp/input/index.d.ts +1 -0
- package/build/types/core/AppSpec.d.ts +8 -2
- package/build/types/core/HoistAuthModel.d.ts +6 -3
- package/build/types/data/filter/BaseFilterFieldSpec.d.ts +7 -1
- package/build/types/data/filter/FieldFilter.d.ts +2 -0
- package/build/types/desktop/cmp/filechooser/FileChooserModel.d.ts +1 -1
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +1 -41
- package/build/types/icon/Icon.d.ts +1 -0
- package/build/types/mobile/cmp/appOption/SizingModeAppOption.d.ts +3 -3
- package/build/types/mobile/cmp/appOption/ThemeAppOption.d.ts +3 -3
- package/build/types/mobile/cmp/input/SegmentedControl.d.ts +47 -0
- package/build/types/mobile/cmp/input/SwitchInput.d.ts +1 -1
- package/build/types/mobile/cmp/input/TextInput.d.ts +3 -0
- package/build/types/mobile/cmp/input/index.d.ts +1 -0
- package/build/types/svc/JsonBlobService.d.ts +7 -7
- package/build/types/svc/PrefService.d.ts +4 -3
- package/build/types/svc/TraceService.d.ts +8 -6
- package/build/types/svc/TrackService.d.ts +5 -3
- package/build/types/svc/impl/Fetch.d.ts +13 -0
- package/build/types/utils/js/TestUtils.d.ts +1 -1
- package/build/types/utils/react/LayoutPropUtils.d.ts +1 -1
- package/build/types/utils/react/ReactUtils.d.ts +1 -1
- package/cmp/error/ErrorMessage.scss +2 -0
- package/cmp/grid/filter/GridFilterFieldSpec.ts +5 -2
- package/cmp/grid/impl/GridHScrollbar.ts +10 -3
- package/cmp/input/SegmentedControlOption.ts +61 -0
- package/cmp/input/index.ts +1 -0
- package/core/AppSpec.ts +9 -1
- package/core/HoistAuthModel.ts +13 -6
- package/data/Field.ts +3 -1
- package/data/filter/BaseFilterFieldSpec.ts +16 -2
- package/data/filter/FieldFilter.ts +90 -21
- package/desktop/appcontainer/LoginPanel.ts +5 -3
- package/desktop/cmp/filechooser/FileChooserModel.ts +1 -1
- package/desktop/cmp/filter/FilterChooser.scss +0 -5
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +9 -4
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +2 -2
- package/desktop/cmp/input/SegmentedControl.ts +8 -51
- package/desktop/cmp/tab/Tabs.scss +6 -2
- package/desktop/cmp/tab/dynamic/DynamicTabSwitcher.ts +7 -3
- package/docs/error-handling.md +9 -0
- package/icon/Icon.ts +3 -0
- package/icon/index.ts +8 -0
- package/mobile/appcontainer/LoginPanel.scss +53 -11
- package/mobile/appcontainer/LoginPanel.ts +69 -44
- package/mobile/cmp/appOption/SizingModeAppOption.ts +5 -16
- package/mobile/cmp/appOption/ThemeAppOption.ts +8 -25
- package/mobile/cmp/error/impl/ErrorMessage.ts +4 -8
- package/mobile/cmp/input/SegmentedControl.scss +126 -0
- package/mobile/cmp/input/SegmentedControl.ts +210 -0
- package/mobile/cmp/input/SwitchInput.ts +1 -1
- package/mobile/cmp/input/TextInput.scss +7 -0
- package/mobile/cmp/input/TextInput.ts +11 -1
- package/mobile/cmp/input/index.ts +1 -0
- package/mobile/cmp/tab/impl/Tabs.scss +8 -0
- package/mobile/cmp/toolbar/Toolbar.scss +8 -0
- package/package.json +4 -4
- package/styles/vars.scss +6 -1
- package/svc/JsonBlobService.ts +21 -20
- package/svc/PrefService.ts +20 -10
- package/svc/TraceService.ts +53 -41
- package/svc/TrackService.ts +21 -10
- package/svc/impl/Fetch.ts +33 -0
- package/utils/js/ClipboardUtils.ts +2 -2
- package/utils/js/TestUtils.ts +1 -1
- package/utils/react/LayoutPropUtils.ts +1 -1
- package/utils/react/ReactUtils.ts +1 -1
package/svc/TraceService.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService,
|
|
7
|
+
import {HoistService, PlainObject, XH, Span, FullSpanConfig} from '@xh/hoist/core';
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {debounced, parseNameSource} from '@xh/hoist/utils/js';
|
|
10
|
-
import {every, forEach, groupBy, isEmpty, isString, omitBy} from 'lodash';
|
|
10
|
+
import {every, forEach, groupBy, isEmpty, isString, omitBy, takeRight} from 'lodash';
|
|
11
|
+
import {terminationSafePostJson} from './impl/Fetch';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Client-side distributed tracing service for Hoist applications.
|
|
@@ -48,8 +49,14 @@ export class TraceService extends HoistService {
|
|
|
48
49
|
//------------------
|
|
49
50
|
// Initialization
|
|
50
51
|
//------------------
|
|
51
|
-
override async initAsync(
|
|
52
|
-
|
|
52
|
+
override async initAsync() {
|
|
53
|
+
// Flush on page teardown while the page is still alive.
|
|
54
|
+
this.addReaction({
|
|
55
|
+
track: () => XH.pageState,
|
|
56
|
+
run: () => {
|
|
57
|
+
if (!XH.pageIsVisible) this.pushPendingInternalAsync();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
//------------------
|
|
@@ -159,44 +166,11 @@ export class TraceService extends HoistService {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
169
|
+
* Flush the queue of pending spans to the server.
|
|
170
|
+
* @internal - apps should generally allow this service to manage w/its internal debounce.
|
|
164
171
|
*/
|
|
165
172
|
async pushPendingAsync() {
|
|
166
|
-
|
|
167
|
-
if (isEmpty(spans)) return;
|
|
168
|
-
|
|
169
|
-
this._pending = [];
|
|
170
|
-
try {
|
|
171
|
-
await XH.postJson({
|
|
172
|
-
url: 'xh/submitSpans',
|
|
173
|
-
body: spans.map(s => s.toJSON()),
|
|
174
|
-
params: {
|
|
175
|
-
clientUsername: XH.getUsername()
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
} catch (e) {
|
|
179
|
-
if (isRetryableError(e)) {
|
|
180
|
-
// Transient failure - re-queue the batch (ahead of newer spans) to retry on the
|
|
181
|
-
// next flush, then bound the buffer in case the outage is prolonged.
|
|
182
|
-
this._pending = [...spans, ...this._pending];
|
|
183
|
-
this.enforceCap();
|
|
184
|
-
this.logError('Failed to push spans - will retry on next flush', e);
|
|
185
|
-
} else {
|
|
186
|
-
// Permanent (client-side) rejection - drop the batch so it can't deadlock the
|
|
187
|
-
// pipe (e.g. a session mismatch or oversized payload would fail forever).
|
|
188
|
-
this.logError('Server rejected span batch - dropping', e);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
194
|
-
private enforceCap() {
|
|
195
|
-
const {_pending} = this,
|
|
196
|
-
{MAX_PENDING} = TraceService;
|
|
197
|
-
if (_pending.length > MAX_PENDING) {
|
|
198
|
-
_pending.splice(0, _pending.length - MAX_PENDING);
|
|
199
|
-
}
|
|
173
|
+
return this.pushPendingInternalAsync();
|
|
200
174
|
}
|
|
201
175
|
|
|
202
176
|
/**
|
|
@@ -237,7 +211,45 @@ export class TraceService extends HoistService {
|
|
|
237
211
|
|
|
238
212
|
@debounced(5 * SECONDS)
|
|
239
213
|
private pushPendingBuffered() {
|
|
240
|
-
void this.
|
|
214
|
+
void this.pushPendingInternalAsync();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Flush all pending spans to the server. */
|
|
218
|
+
private async pushPendingInternalAsync() {
|
|
219
|
+
const spans = this._pending;
|
|
220
|
+
if (isEmpty(spans)) return;
|
|
221
|
+
|
|
222
|
+
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
223
|
+
this._pending = [];
|
|
224
|
+
try {
|
|
225
|
+
await terminationSafePostJson({
|
|
226
|
+
url: 'xh/submitSpans',
|
|
227
|
+
body: spans.map(s => s.toJSON()),
|
|
228
|
+
params: {
|
|
229
|
+
clientUsername: XH.getUsername()
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
} catch (e) {
|
|
233
|
+
if (isRetryableError(e)) {
|
|
234
|
+
// Transient failure - re-queue the batch (ahead of newer spans) to retry on the
|
|
235
|
+
// next flush, then bound the buffer in case the outage is prolonged.
|
|
236
|
+
this._pending = [...spans, ...this._pending];
|
|
237
|
+
this.enforceCap();
|
|
238
|
+
this.logError('Failed to push spans - will retry on next flush', e);
|
|
239
|
+
} else {
|
|
240
|
+
// Permanent (client-side) rejection - drop the batch so it can't deadlock the
|
|
241
|
+
// pipe (e.g. a session mismatch or oversized payload would fail forever).
|
|
242
|
+
this.logError('Server rejected span batch - dropping', e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Bound the pending buffer, silently dropping oldest spans (failed pushes are logged). */
|
|
248
|
+
private enforceCap() {
|
|
249
|
+
const {MAX_PENDING} = TraceService;
|
|
250
|
+
if (this._pending.length > MAX_PENDING) {
|
|
251
|
+
this._pending = takeRight(this._pending, MAX_PENDING);
|
|
252
|
+
}
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
/**
|
package/svc/TrackService.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService,
|
|
7
|
+
import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
|
|
8
8
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
9
9
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
10
10
|
import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
|
|
11
11
|
import {isEmpty, isNil, isString} from 'lodash';
|
|
12
|
+
import {terminationSafePostJson} from './impl/Fetch';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Primary service for tracking any activity that an application's admins want to track.
|
|
@@ -23,8 +24,14 @@ export class TrackService extends HoistService {
|
|
|
23
24
|
private oncePerSessionSent = new Map();
|
|
24
25
|
private pending: PlainObject[] = [];
|
|
25
26
|
|
|
26
|
-
override async initAsync(
|
|
27
|
-
|
|
27
|
+
override async initAsync() {
|
|
28
|
+
// Flush on page teardown while the page is still alive
|
|
29
|
+
this.addReaction({
|
|
30
|
+
track: () => XH.pageState,
|
|
31
|
+
run: () => {
|
|
32
|
+
if (!XH.pageIsVisible) this.pushPendingAsync();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
get conf(): ActivityTrackingConfig {
|
|
@@ -94,25 +101,29 @@ export class TrackService extends HoistService {
|
|
|
94
101
|
|
|
95
102
|
/**
|
|
96
103
|
* Flush the queue of pending activity tracking messages to the server.
|
|
97
|
-
*
|
|
104
|
+
*
|
|
105
|
+
* Not typically called by applications. Called automatically by the framework via a
|
|
106
|
+
* debounce and when the page is hidden/terminated.
|
|
98
107
|
*/
|
|
99
108
|
async pushPendingAsync() {
|
|
100
109
|
const {pending} = this;
|
|
101
110
|
if (isEmpty(pending)) return;
|
|
102
111
|
|
|
112
|
+
// Clear synchronously with the capture, so overlapping flushes cannot post twice.
|
|
113
|
+
this.pending = [];
|
|
114
|
+
|
|
103
115
|
await this.runner()
|
|
104
116
|
.span('push')
|
|
105
|
-
.run(
|
|
106
|
-
|
|
107
|
-
await XH.postJson(
|
|
117
|
+
.run(ctx =>
|
|
118
|
+
terminationSafePostJson(
|
|
108
119
|
{
|
|
109
120
|
url: 'xh/track',
|
|
110
121
|
body: {entries: pending},
|
|
111
122
|
params: {clientUsername: XH.getUsername()}
|
|
112
123
|
},
|
|
113
124
|
ctx
|
|
114
|
-
)
|
|
115
|
-
|
|
125
|
+
)
|
|
126
|
+
);
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
//------------------
|
|
@@ -120,7 +131,7 @@ export class TrackService extends HoistService {
|
|
|
120
131
|
//------------------
|
|
121
132
|
@debounced(10 * SECONDS)
|
|
122
133
|
private pushPendingBuffered() {
|
|
123
|
-
this.pushPendingAsync();
|
|
134
|
+
void this.pushPendingAsync();
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
private toServerJson(options: TrackOptions): PlainObject {
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import {CallContextLike, XH} from '@xh/hoist/core';
|
|
8
|
+
import type {FetchOptions} from '../FetchService';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Post a JSON body, using `fetch({keepalive: true})` when the page is no longer visible so the
|
|
12
|
+
* request can survive teardown (e.g. a flush as `XH.pageState` goes `hidden`/`frozen`/`terminated`).
|
|
13
|
+
*
|
|
14
|
+
* Keepalive bodies share a single browser-wide 64KB budget; if exceeded the request is never sent
|
|
15
|
+
* and we retry once uncapped (without keepalive), which still completes while the page is alive (the
|
|
16
|
+
* common `hidden` case). Real server errors are re-thrown, not re-posted.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export async function terminationSafePostJson(
|
|
21
|
+
opts: FetchOptions,
|
|
22
|
+
ctx?: CallContextLike
|
|
23
|
+
): Promise<any> {
|
|
24
|
+
if (XH.pageIsVisible) return XH.postJson(opts, ctx);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
return await XH.postJson({...opts, fetchOpts: {...opts.fetchOpts, keepalive: true}}, ctx);
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
// Retry uncapped if keepalive was never sent (over-budget); re-throw real server errors.
|
|
30
|
+
if (e.isServerUnavailable) return XH.postJson(opts, ctx);
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {Exception} from '@xh/hoist/exception';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Copy the given text to the system clipboard.
|
|
@@ -53,5 +53,5 @@ function copyViaExecCommand(text: string): void {
|
|
|
53
53
|
selection.removeAllRanges();
|
|
54
54
|
document.body.removeChild(span);
|
|
55
55
|
}
|
|
56
|
-
if (!success) throw
|
|
56
|
+
if (!success) throw Exception.create('Clipboard copy not allowed');
|
|
57
57
|
}
|
package/utils/js/TestUtils.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {LayoutProps, ResolvedLayoutProps, PlainObject} from '@xh/hoist/core';
|
|
7
|
+
import type {LayoutProps, ResolvedLayoutProps, PlainObject} from '@xh/hoist/core';
|
|
8
8
|
import {forOwn, isEmpty, isNumber, isString, isNil, omit, pick} from 'lodash';
|
|
9
9
|
|
|
10
10
|
const XH_PAD_VAR = 'var(--xh-pad-px)';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {Content} from '@xh/hoist/core';
|
|
7
|
+
import type {Content} from '@xh/hoist/core';
|
|
8
8
|
import {ReactElement, cloneElement, createElement, isValidElement} from 'react';
|
|
9
9
|
import {isFunction, isNil} from 'lodash';
|
|
10
10
|
import {renderToStaticMarkup as reactRenderToStaticMarkup} from 'react-dom/server';
|