@xh/hoist 59.0.3 → 59.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 +59 -2
- package/admin/AdminUtils.ts +23 -0
- package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +2 -1
- package/admin/tabs/activity/feedback/FeedbackPanel.ts +3 -3
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +2 -1
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -2
- package/admin/tabs/general/alertBanner/AlertBannerModel.ts +2 -2
- package/admin/tabs/general/config/ConfigPanelModel.ts +2 -2
- package/admin/tabs/general/users/UserModel.ts +2 -2
- package/admin/tabs/monitor/MonitorResultsModel.ts +48 -11
- package/admin/tabs/monitor/MonitorResultsPanel.ts +71 -8
- package/admin/tabs/server/connectionpool/ConnPoolMonitorModel.ts +2 -2
- package/admin/tabs/server/ehcache/EhCacheModel.ts +2 -2
- package/admin/tabs/server/environment/ServerEnvModel.ts +3 -3
- package/admin/tabs/server/logLevel/LogLevelPanel.ts +3 -3
- package/admin/tabs/server/logViewer/LogViewerModel.ts +2 -2
- package/admin/tabs/server/memory/MemoryMonitorModel.ts +2 -2
- package/admin/tabs/server/services/ServiceModel.ts +3 -3
- package/admin/tabs/server/websocket/WebSocketModel.ts +3 -2
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +2 -2
- package/admin/tabs/userData/prefs/PreferenceModel.ts +2 -2
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -3
- package/appcontainer/ExceptionDialogModel.ts +6 -0
- package/cmp/ag-grid/AgGrid.scss +27 -5
- package/cmp/error/ErrorBoundary.ts +68 -0
- package/cmp/error/ErrorBoundaryModel.ts +77 -0
- package/cmp/grid/Grid.scss +10 -0
- package/cmp/grid/GridModel.ts +137 -33
- package/cmp/grid/columns/ColumnGroup.ts +11 -0
- package/cmp/markdown/Markdown.ts +32 -0
- package/cmp/markdown/index.ts +1 -0
- package/core/XH.ts +5 -9
- package/core/exception/ExceptionHandler.ts +15 -0
- package/data/RecordAction.ts +1 -1
- package/data/cube/Query.ts +37 -3
- package/data/cube/View.ts +16 -4
- package/data/cube/row/BaseRow.ts +2 -2
- package/desktop/appcontainer/AppContainer.ts +17 -3
- package/desktop/appcontainer/Banner.scss +25 -0
- package/desktop/appcontainer/Banner.ts +3 -2
- package/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +4 -1
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -2
- package/desktop/cmp/dash/container/impl/DashContainerUtils.ts +4 -4
- package/desktop/cmp/dash/container/impl/DashContainerView.ts +2 -1
- package/desktop/cmp/dock/DockViewModel.ts +10 -8
- package/desktop/cmp/dock/impl/DockContainer.ts +1 -0
- package/desktop/cmp/dock/impl/DockView.ts +2 -1
- package/desktop/cmp/error/ErrorMessage.scss +1 -0
- package/desktop/cmp/error/ErrorMessage.ts +61 -23
- package/desktop/cmp/input/Checkbox.scss +13 -0
- package/desktop/cmp/input/Checkbox.ts +2 -0
- package/desktop/cmp/modalsupport/ModalSupport.scss +2 -0
- package/desktop/cmp/panel/Panel.ts +37 -14
- package/desktop/cmp/panel/PanelModel.ts +35 -7
- package/desktop/cmp/panel/impl/PanelHeader.scss +5 -0
- package/desktop/cmp/panel/impl/PanelHeader.ts +53 -38
- package/desktop/cmp/tab/impl/Tab.ts +2 -1
- package/dynamics/desktop.ts +15 -17
- package/dynamics/mobile.ts +10 -8
- package/inspector/Inspector.scss +5 -10
- package/inspector/InspectorPanel.ts +2 -0
- package/inspector/instances/InstancesModel.ts +1 -1
- package/kit/react-markdown/index.ts +11 -0
- package/mobile/appcontainer/AppContainer.ts +17 -3
- package/mobile/appcontainer/Banner.scss +25 -0
- package/mobile/appcontainer/Banner.ts +3 -2
- package/mobile/cmp/error/ErrorMessage.ts +58 -18
- package/mobile/cmp/menu/impl/Menu.scss +7 -1
- package/mobile/cmp/navigator/PageModel.ts +1 -0
- package/mobile/cmp/navigator/impl/Page.ts +2 -1
- package/mobile/cmp/panel/Panel.ts +5 -1
- package/mobile/cmp/tab/impl/Tab.ts +2 -1
- package/package.json +5 -3
- package/styles/vars.scss +2 -0
- package/svc/AlertBannerService.ts +2 -2
- package/svc/GridExportService.ts +1 -1
- package/admin/tabs/monitor/MonitorResultsToolbar.ts +0 -66
- package/appcontainer/ErrorBoundary.ts +0 -36
package/core/XH.ts
CHANGED
|
@@ -56,6 +56,7 @@ import '../styles/XH.scss';
|
|
|
56
56
|
import {ModelSelector, HoistModel, RefreshContextModel} from './model';
|
|
57
57
|
import {HoistAppModel, BannerSpec, ToastSpec, MessageSpec, HoistUser, TaskObserver} from './';
|
|
58
58
|
import {CancelFn} from 'router5/types/types/base';
|
|
59
|
+
import {apiDeprecated} from '@xh/hoist/utils/js';
|
|
59
60
|
|
|
60
61
|
export const MIN_HOIST_CORE_VERSION = '16.0';
|
|
61
62
|
|
|
@@ -576,16 +577,11 @@ export class XHApi {
|
|
|
576
577
|
this.exceptionHandler.handleException(exception, options);
|
|
577
578
|
}
|
|
578
579
|
|
|
579
|
-
/**
|
|
580
|
-
* Show an exception. This method is an alias for {@link ExceptionHandler.showException}.
|
|
581
|
-
*
|
|
582
|
-
* Intended to be used for the deferred / user-initiated showing of exceptions that have
|
|
583
|
-
* already been appropriately logged. Apps should typically prefer {@link handleException}.
|
|
584
|
-
*
|
|
585
|
-
* @param exception - thrown object, will be coerced into a {@link HoistException}.
|
|
586
|
-
* @param options - provides further control over how the exception is shown and/or logged.
|
|
587
|
-
*/
|
|
588
580
|
showException(exception: unknown, options?: ExceptionHandlerOptions) {
|
|
581
|
+
apiDeprecated('showException', {
|
|
582
|
+
msg: 'Use XH.exceptionHandler.showException instead',
|
|
583
|
+
v: '62'
|
|
584
|
+
});
|
|
589
585
|
this.exceptionHandler.showException(exception, options);
|
|
590
586
|
}
|
|
591
587
|
|
|
@@ -156,6 +156,21 @@ export class ExceptionHandler {
|
|
|
156
156
|
XH.appContainerModel.exceptionDialogModel.show(e, opts);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Show an exception in full detail, with ability to report and copy to clipboard.
|
|
161
|
+
* Intended to be used for the deferred / user-initiated showing of exceptions that have
|
|
162
|
+
* already been appropriately logged. Apps should typically prefer {@link handleException}.
|
|
163
|
+
*
|
|
164
|
+
* @param exception - thrown object, will be coerced into a {@link HoistException}.
|
|
165
|
+
* @param options - provides further control over how the exception is shown.
|
|
166
|
+
*/
|
|
167
|
+
showExceptionDetails(exception: unknown, options?: ExceptionHandlerOptions) {
|
|
168
|
+
if (XH.pageState == 'terminated' || XH.pageState == 'frozen') return;
|
|
169
|
+
|
|
170
|
+
const {e, opts} = this.parseArgs(exception, options);
|
|
171
|
+
XH.appContainerModel.exceptionDialogModel.showDetails(e, opts);
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
/**
|
|
160
175
|
* Create a server-side exception entry. Client metadata will be set automatically.
|
|
161
176
|
*
|
package/data/RecordAction.ts
CHANGED
package/data/cube/Query.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {Filter, parseFilter, StoreRecord} from '@xh/hoist/data';
|
|
8
|
+
import {BucketSpecFn, Filter, LockFn, OmitFn, parseFilter, StoreRecord} from '@xh/hoist/data';
|
|
9
9
|
import {isEqual, find} from 'lodash';
|
|
10
10
|
import {FilterLike, FilterTestFn} from '../filter/Types';
|
|
11
11
|
import {CubeField} from './CubeField';
|
|
@@ -51,6 +51,25 @@ export interface QueryConfig {
|
|
|
51
51
|
|
|
52
52
|
/** True to include leaf nodes in return.*/
|
|
53
53
|
includeLeaves?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional function to be called for each aggregate node to determine if it should be "locked",
|
|
57
|
+
* preventing drill-down into its children. Defaults to Cube.lockFn.
|
|
58
|
+
*/
|
|
59
|
+
lockFn?: LockFn;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional function to be called for each dimension during row generation to determine if the
|
|
63
|
+
* children of that dimension should be bucketed into additional dynamic dimensions.
|
|
64
|
+
* Defaults to Cube.bucketSpecFn.
|
|
65
|
+
*/
|
|
66
|
+
bucketSpecFn?: BucketSpecFn;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Optional function to be called on all single child rows during view processing.
|
|
70
|
+
* Return true to omit the row. Defaults to Cube.omitFn.
|
|
71
|
+
*/
|
|
72
|
+
omitFn?: OmitFn;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
/** {@inheritDoc QueryConfig} */
|
|
@@ -61,6 +80,9 @@ export class Query {
|
|
|
61
80
|
readonly includeRoot: boolean;
|
|
62
81
|
readonly includeLeaves: boolean;
|
|
63
82
|
readonly cube: Cube;
|
|
83
|
+
readonly lockFn: LockFn;
|
|
84
|
+
readonly bucketSpecFn: BucketSpecFn;
|
|
85
|
+
readonly omitFn: OmitFn;
|
|
64
86
|
|
|
65
87
|
private readonly _testFn: FilterTestFn;
|
|
66
88
|
|
|
@@ -70,7 +92,10 @@ export class Query {
|
|
|
70
92
|
dimensions,
|
|
71
93
|
filter = null,
|
|
72
94
|
includeRoot = false,
|
|
73
|
-
includeLeaves = false
|
|
95
|
+
includeLeaves = false,
|
|
96
|
+
lockFn = cube.lockFn,
|
|
97
|
+
bucketSpecFn = cube.bucketSpecFn,
|
|
98
|
+
omitFn = cube.omitFn
|
|
74
99
|
}: QueryConfig) {
|
|
75
100
|
this.cube = cube;
|
|
76
101
|
this.fields = this.parseFields(fields);
|
|
@@ -78,6 +103,9 @@ export class Query {
|
|
|
78
103
|
this.includeRoot = includeRoot;
|
|
79
104
|
this.includeLeaves = includeLeaves;
|
|
80
105
|
this.filter = parseFilter(filter);
|
|
106
|
+
this.lockFn = lockFn;
|
|
107
|
+
this.bucketSpecFn = bucketSpecFn;
|
|
108
|
+
this.omitFn = omitFn;
|
|
81
109
|
|
|
82
110
|
this._testFn = this.filter?.getTestFn(this.cube.store) ?? null;
|
|
83
111
|
}
|
|
@@ -89,6 +117,9 @@ export class Query {
|
|
|
89
117
|
filter: this.filter,
|
|
90
118
|
includeRoot: this.includeRoot,
|
|
91
119
|
includeLeaves: this.includeLeaves,
|
|
120
|
+
lockFn: this.lockFn,
|
|
121
|
+
bucketSpecFn: this.bucketSpecFn,
|
|
122
|
+
omitFn: this.omitFn,
|
|
92
123
|
cube: this.cube,
|
|
93
124
|
...overrides
|
|
94
125
|
};
|
|
@@ -121,7 +152,10 @@ export class Query {
|
|
|
121
152
|
isEqual(this.dimensions, other.dimensions) &&
|
|
122
153
|
this.cube === other.cube &&
|
|
123
154
|
this.includeRoot === other.includeRoot &&
|
|
124
|
-
this.includeLeaves === other.includeLeaves
|
|
155
|
+
this.includeLeaves === other.includeLeaves &&
|
|
156
|
+
this.bucketSpecFn == other.bucketSpecFn &&
|
|
157
|
+
this.omitFn == other.omitFn &&
|
|
158
|
+
this.lockFn == other.lockFn
|
|
125
159
|
);
|
|
126
160
|
}
|
|
127
161
|
|
package/data/cube/View.ts
CHANGED
|
@@ -99,7 +99,7 @@ export class View extends HoistBase {
|
|
|
99
99
|
const {query, stores = [], connect = false} = config;
|
|
100
100
|
|
|
101
101
|
this.query = query;
|
|
102
|
-
this.stores =
|
|
102
|
+
this.stores = this.parseStores(stores);
|
|
103
103
|
this._rowCache = new Map();
|
|
104
104
|
this.fullUpdate();
|
|
105
105
|
|
|
@@ -185,7 +185,7 @@ export class View extends HoistBase {
|
|
|
185
185
|
|
|
186
186
|
/** Set stores to be loaded/reloaded with data from this view. */
|
|
187
187
|
setStores(stores: Some<Store>) {
|
|
188
|
-
this.stores =
|
|
188
|
+
this.stores = this.parseStores(stores);
|
|
189
189
|
this.loadStores();
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -351,9 +351,9 @@ export class View extends HoistBase {
|
|
|
351
351
|
parentId: string,
|
|
352
352
|
appliedDimensions: PlainObject
|
|
353
353
|
): BaseRow[] {
|
|
354
|
-
if (!this.
|
|
354
|
+
if (!this.query.bucketSpecFn) return rows;
|
|
355
355
|
|
|
356
|
-
const bucketSpec = this.
|
|
356
|
+
const bucketSpec = this.query.bucketSpecFn(rows);
|
|
357
357
|
if (!bucketSpec) return rows;
|
|
358
358
|
|
|
359
359
|
if (!this.query.includeLeaves && rows[0]?.isLeaf) return rows;
|
|
@@ -463,6 +463,18 @@ export class View extends HoistBase {
|
|
|
463
463
|
return this.fields.every(({aggregator}) => !aggregator || aggregator.dependsOnChildrenOnly);
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
private parseStores(stores: Some<Store>): Store[] {
|
|
467
|
+
const ret = castArray(stores);
|
|
468
|
+
|
|
469
|
+
// Views mutate the rows they feed to connected stores -- `reuseRecords` not appropriate
|
|
470
|
+
throwIf(
|
|
471
|
+
ret.some(s => s.reuseRecords),
|
|
472
|
+
'Store.reuseRecords cannot be used on a Store that is connected to a Cube View'
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
return ret;
|
|
476
|
+
}
|
|
477
|
+
|
|
466
478
|
override destroy() {
|
|
467
479
|
this.disconnect();
|
|
468
480
|
super.destroy();
|
package/data/cube/row/BaseRow.ts
CHANGED
|
@@ -59,7 +59,7 @@ export abstract class BaseRow {
|
|
|
59
59
|
let dataChildren = this.getVisibleChildrenDatas();
|
|
60
60
|
|
|
61
61
|
// 2) If omitting ourselves, we are done, return visible children.
|
|
62
|
-
if (!isLeaf && view.
|
|
62
|
+
if (!isLeaf && view.query.omitFn?.(this as any)) return dataChildren;
|
|
63
63
|
|
|
64
64
|
// 3) Otherwise, we can attach this data to the children data and return.
|
|
65
65
|
|
|
@@ -89,7 +89,7 @@ export abstract class BaseRow {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Skip all children in a locked node
|
|
92
|
-
if (view.
|
|
92
|
+
if (view.query.lockFn?.(this as any)) {
|
|
93
93
|
this.locked = true;
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {AppContainerModel} from '@xh/hoist/appcontainer/AppContainerModel';
|
|
8
8
|
import {fragment, frame, vframe, viewport} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {createElement, hoistCmp, refreshContextView, uses, XH} from '@xh/hoist/core';
|
|
10
|
-
import {errorBoundary} from '@xh/hoist/
|
|
10
|
+
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
11
11
|
import {changelogDialog} from '@xh/hoist/desktop/appcontainer/ChangelogDialog';
|
|
12
12
|
import {suspendPanel} from '@xh/hoist/desktop/appcontainer/SuspendPanel';
|
|
13
13
|
import {dockContainerImpl} from '@xh/hoist/desktop/cmp/dock/impl/DockContainer';
|
|
@@ -40,6 +40,7 @@ import {optionsDialog} from './OptionsDialog';
|
|
|
40
40
|
import {toastSource} from './ToastSource';
|
|
41
41
|
import {versionBar} from './VersionBar';
|
|
42
42
|
import {ReactElement} from 'react';
|
|
43
|
+
import {errorMessage} from '../cmp/error/ErrorMessage';
|
|
43
44
|
|
|
44
45
|
installDesktopImpls({
|
|
45
46
|
tabContainerImpl,
|
|
@@ -52,7 +53,8 @@ installDesktopImpls({
|
|
|
52
53
|
ColChooserModel,
|
|
53
54
|
ColumnHeaderFilterModel,
|
|
54
55
|
useContextMenu,
|
|
55
|
-
ModalSupportModel
|
|
56
|
+
ModalSupportModel,
|
|
57
|
+
errorMessage
|
|
56
58
|
});
|
|
57
59
|
/**
|
|
58
60
|
* Top-level wrapper for Desktop applications.
|
|
@@ -73,7 +75,19 @@ export const AppContainer = hoistCmp({
|
|
|
73
75
|
|
|
74
76
|
return fragment(
|
|
75
77
|
hotkeysProvider(
|
|
76
|
-
errorBoundary(
|
|
78
|
+
errorBoundary({
|
|
79
|
+
modelConfig: {
|
|
80
|
+
errorHandler: {
|
|
81
|
+
title: 'Critical Error',
|
|
82
|
+
message:
|
|
83
|
+
XH.clientAppName +
|
|
84
|
+
' encountered a critical error and cannot be displayed.',
|
|
85
|
+
requireReload: true
|
|
86
|
+
},
|
|
87
|
+
errorRenderer: () => null
|
|
88
|
+
},
|
|
89
|
+
item: viewForState()
|
|
90
|
+
}),
|
|
77
91
|
// Modal component helpers rendered here at top-level to support display of messages
|
|
78
92
|
// and exceptions at any point during the app lifecycle.
|
|
79
93
|
exceptionDialog(),
|
|
@@ -21,6 +21,31 @@ body.xh-app .xh-banner {
|
|
|
21
21
|
overflow: hidden;
|
|
22
22
|
white-space: nowrap;
|
|
23
23
|
text-overflow: ellipsis;
|
|
24
|
+
|
|
25
|
+
// Disallow most markdown styling except **bold**, *italics* and (links)
|
|
26
|
+
h1,
|
|
27
|
+
h2,
|
|
28
|
+
h3,
|
|
29
|
+
h4,
|
|
30
|
+
p,
|
|
31
|
+
ul,
|
|
32
|
+
li,
|
|
33
|
+
a,
|
|
34
|
+
code {
|
|
35
|
+
display: inline;
|
|
36
|
+
margin: unset;
|
|
37
|
+
padding: unset;
|
|
38
|
+
color: unset;
|
|
39
|
+
font-weight: unset;
|
|
40
|
+
font-size: unset;
|
|
41
|
+
font-family: unset;
|
|
42
|
+
list-style: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Render links with an underline
|
|
46
|
+
a {
|
|
47
|
+
text-decoration: underline;
|
|
48
|
+
}
|
|
24
49
|
}
|
|
25
50
|
|
|
26
51
|
&__action-button,
|
|
@@ -10,7 +10,8 @@ import {hframe, div} from '@xh/hoist/cmp/layout';
|
|
|
10
10
|
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
|
|
11
11
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
12
12
|
import {Icon} from '@xh/hoist/icon';
|
|
13
|
-
import {
|
|
13
|
+
import {markdown} from '@xh/hoist/cmp/markdown';
|
|
14
|
+
import {isFunction, isEmpty, isString} from 'lodash';
|
|
14
15
|
import classNames from 'classnames';
|
|
15
16
|
|
|
16
17
|
import './Banner.scss';
|
|
@@ -43,7 +44,7 @@ export const banner = hoistCmp.factory({
|
|
|
43
44
|
icon,
|
|
44
45
|
div({
|
|
45
46
|
className: 'xh-banner__message',
|
|
46
|
-
item: message,
|
|
47
|
+
item: isString(message) ? markdown({content: message}) : message,
|
|
47
48
|
onClick
|
|
48
49
|
})
|
|
49
50
|
]
|
|
@@ -14,6 +14,7 @@ import {popover, Position} from '@xh/hoist/kit/blueprint';
|
|
|
14
14
|
import {button} from '../../../button';
|
|
15
15
|
import {panel} from '../../../panel';
|
|
16
16
|
import {DashCanvasViewModel} from '../DashCanvasViewModel';
|
|
17
|
+
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Implementation component to show an item within a DashCanvas. This component
|
|
@@ -46,7 +47,9 @@ export const dashCanvasView = hoistCmp.factory({
|
|
|
46
47
|
...headerProps,
|
|
47
48
|
item: box({
|
|
48
49
|
ref: useOnResize(dims => model.onContentsResized(dims), {debounce: 100}),
|
|
49
|
-
item:
|
|
50
|
+
item: errorBoundary(
|
|
51
|
+
elementFromContent(viewSpec.content, {flex: 1, viewModel: model})
|
|
52
|
+
),
|
|
50
53
|
flex: autoHeight ? 'none' : 'auto'
|
|
51
54
|
})
|
|
52
55
|
});
|
|
@@ -22,7 +22,7 @@ import {wait} from '@xh/hoist/promise';
|
|
|
22
22
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
23
23
|
import {debounced, ensureUniqueBy, throwIf} from '@xh/hoist/utils/js';
|
|
24
24
|
import {createObservableRef} from '@xh/hoist/utils/react';
|
|
25
|
-
import {cloneDeep, defaultsDeep, find, isFinite, isNil, reject, startCase} from 'lodash';
|
|
25
|
+
import {cloneDeep, defaultsDeep, find, isFinite, isNil, last, reject, startCase} from 'lodash';
|
|
26
26
|
import {createRoot} from 'react-dom/client';
|
|
27
27
|
import {DashConfig, DashModel} from '../';
|
|
28
28
|
import {DashViewModel, DashViewState} from '../DashViewModel';
|
|
@@ -290,7 +290,8 @@ export class DashContainerModel extends DashModel<
|
|
|
290
290
|
|
|
291
291
|
if (!isFinite(index)) index = container.contentItems.length;
|
|
292
292
|
container.addChild(goldenLayoutConfig(viewSpec), index);
|
|
293
|
-
|
|
293
|
+
const stack = container.isStack ? container : last(container.contentItems);
|
|
294
|
+
wait(1).then(() => this.onStackActiveItemChange(stack));
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
/**
|
|
@@ -46,8 +46,8 @@ function convertGLToStateInner(configItems = [], contentItems = [], dashContaine
|
|
|
46
46
|
|
|
47
47
|
ret.push(view);
|
|
48
48
|
} else {
|
|
49
|
-
const {type, width, height, activeItemIndex, content} = configItem,
|
|
50
|
-
container = {type} as PlainObject;
|
|
49
|
+
const {type, width, height, activeItemIndex, content, isClosable} = configItem,
|
|
50
|
+
container = {type, allowRemove: isClosable} as PlainObject;
|
|
51
51
|
|
|
52
52
|
if (isFinite(width)) container.width = round(width, 2);
|
|
53
53
|
if (isFinite(height)) container.height = round(height, 2);
|
|
@@ -138,12 +138,12 @@ function convertStateToGLInner(items = [], viewSpecs = [], containerSize, contai
|
|
|
138
138
|
const content = convertStateToGLInner(item.content, viewSpecs, itemSize, item).filter(
|
|
139
139
|
it => !isNil(it)
|
|
140
140
|
);
|
|
141
|
-
if (!content.length) return null;
|
|
141
|
+
if (!content.length && item.allowRemove) return null;
|
|
142
142
|
|
|
143
143
|
// Below is a workaround for issue https://github.com/golden-layout/golden-layout/issues/418
|
|
144
144
|
// GoldenLayouts can sometimes export its state with an out-of-bounds `activeItemIndex`.
|
|
145
145
|
// If we encounter this, we overwrite `activeItemIndex` to point to the last item.
|
|
146
|
-
const ret = {...item, content};
|
|
146
|
+
const ret = {...item, content, isClosable: item.allowRemove};
|
|
147
147
|
if (
|
|
148
148
|
type === 'stack' &&
|
|
149
149
|
isFinite(ret.activeItemIndex) &&
|
|
@@ -9,6 +9,7 @@ import {hoistCmp, refreshContextView, uses} from '@xh/hoist/core';
|
|
|
9
9
|
import {elementFromContent} from '@xh/hoist/utils/react';
|
|
10
10
|
import {useRef} from 'react';
|
|
11
11
|
import {DashViewModel} from '../../DashViewModel';
|
|
12
|
+
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Implementation component to show an item within a DashContainer. This component
|
|
@@ -44,7 +45,7 @@ export const dashContainerView = hoistCmp.factory({
|
|
|
44
45
|
className,
|
|
45
46
|
item: refreshContextView({
|
|
46
47
|
model: refreshContextModel,
|
|
47
|
-
item: elementFromContent(viewSpec.content, {flex: 1})
|
|
48
|
+
item: errorBoundary(elementFromContent(viewSpec.content, {flex: 1}))
|
|
48
49
|
})
|
|
49
50
|
});
|
|
50
51
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
RefreshContextModel,
|
|
14
14
|
RefreshMode,
|
|
15
15
|
RenderMode,
|
|
16
|
-
|
|
16
|
+
Awaitable
|
|
17
17
|
} from '@xh/hoist/core';
|
|
18
18
|
import {ModalSupportModel} from '@xh/hoist/desktop/cmp/modalsupport/ModalSupportModel';
|
|
19
19
|
import '@xh/hoist/desktop/register';
|
|
@@ -53,6 +53,8 @@ export interface DockViewConfig {
|
|
|
53
53
|
allowClose?: boolean;
|
|
54
54
|
/** true (default) to allow popping out of the dock and displaying in a modal Dialog. */
|
|
55
55
|
allowDialog?: boolean;
|
|
56
|
+
/** Awaitable callback invoked on close. Return false to prevent close. */
|
|
57
|
+
onClose?: () => Awaitable<boolean | void>;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
|
@@ -75,6 +77,7 @@ export class DockViewModel extends HoistModel {
|
|
|
75
77
|
collapsedWidth: number;
|
|
76
78
|
allowClose: boolean;
|
|
77
79
|
allowDialog: boolean;
|
|
80
|
+
onClose?: () => Awaitable<boolean | void>;
|
|
78
81
|
|
|
79
82
|
containerModel: DockContainerModel;
|
|
80
83
|
@managed refreshContextModel: RefreshContextModel;
|
|
@@ -109,7 +112,8 @@ export class DockViewModel extends HoistModel {
|
|
|
109
112
|
docked = true,
|
|
110
113
|
collapsed = false,
|
|
111
114
|
allowClose = true,
|
|
112
|
-
allowDialog = true
|
|
115
|
+
allowDialog = true,
|
|
116
|
+
onClose
|
|
113
117
|
}: DockViewConfig) {
|
|
114
118
|
super();
|
|
115
119
|
makeObservable(this);
|
|
@@ -128,6 +132,7 @@ export class DockViewModel extends HoistModel {
|
|
|
128
132
|
this.collapsed = collapsed;
|
|
129
133
|
this.allowClose = allowClose;
|
|
130
134
|
this.allowDialog = allowDialog;
|
|
135
|
+
this.onClose = onClose;
|
|
131
136
|
|
|
132
137
|
this._renderMode = renderMode;
|
|
133
138
|
this._refreshMode = refreshMode;
|
|
@@ -195,11 +200,8 @@ export class DockViewModel extends HoistModel {
|
|
|
195
200
|
// Actions
|
|
196
201
|
//-----------------------
|
|
197
202
|
close() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
override destroy() {
|
|
202
|
-
XH.safeDestroy(this.content);
|
|
203
|
-
super.destroy();
|
|
203
|
+
Promise.resolve(this.onClose?.()).then(v => {
|
|
204
|
+
if (v !== false) this.containerModel.removeView(this.id);
|
|
205
|
+
});
|
|
204
206
|
}
|
|
205
207
|
}
|
|
@@ -15,6 +15,7 @@ import {elementFromContent} from '@xh/hoist/utils/react';
|
|
|
15
15
|
import classNames from 'classnames';
|
|
16
16
|
import {useRef} from 'react';
|
|
17
17
|
import './Dock.scss';
|
|
18
|
+
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
18
19
|
|
|
19
20
|
interface DockViewProps extends HoistProps<DockViewModel> {
|
|
20
21
|
/** True to style docked headers with reduced padding and font-size. */
|
|
@@ -57,7 +58,7 @@ export const dockView = hoistCmp.factory<DockViewProps>({
|
|
|
57
58
|
model: refreshContextModel,
|
|
58
59
|
item: div({
|
|
59
60
|
className: 'xh-dock-view__body',
|
|
60
|
-
item: elementFromContent(model.content)
|
|
61
|
+
item: errorBoundary(elementFromContent(model.content))
|
|
61
62
|
})
|
|
62
63
|
});
|
|
63
64
|
|
|
@@ -4,36 +4,52 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {div, frame, p} from '@xh/hoist/cmp/layout';
|
|
8
|
-
import {hoistCmp, HoistProps
|
|
7
|
+
import {div, filler, frame, hbox, p} from '@xh/hoist/cmp/layout';
|
|
8
|
+
import {hoistCmp, HoistProps} from '@xh/hoist/core';
|
|
9
9
|
import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
|
|
10
10
|
import '@xh/hoist/desktop/register';
|
|
11
|
-
import {
|
|
12
|
-
import {isValidElement, ReactNode
|
|
11
|
+
import {isNil, isString} from 'lodash';
|
|
12
|
+
import {isValidElement, ReactNode} from 'react';
|
|
13
13
|
|
|
14
14
|
import './ErrorMessage.scss';
|
|
15
|
+
import {Icon} from '@xh/hoist/icon';
|
|
15
16
|
|
|
16
17
|
export interface ErrorMessageProps extends HoistProps {
|
|
17
18
|
/**
|
|
18
19
|
* If provided, will render a "Retry" button that calls this function.
|
|
19
20
|
* Use `actionButtonProps` for further control over this button.
|
|
20
21
|
*/
|
|
21
|
-
actionFn?: (
|
|
22
|
+
actionFn?: (error: unknown) => void;
|
|
23
|
+
|
|
22
24
|
/**
|
|
23
25
|
* If provided, component will render an inline action button - prompting to user to take some
|
|
24
26
|
* action that might resolve the error, such as retrying a failed data load.
|
|
25
27
|
*/
|
|
26
28
|
actionButtonProps?: ButtonProps;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* If provided, will render a "Details" button that calls this function.
|
|
32
|
+
* Use `detailsButtonProps` for further control over this button. Default false.
|
|
33
|
+
*/
|
|
34
|
+
detailsFn?: (error: unknown) => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* If provided, component will render an inline details button.
|
|
38
|
+
*/
|
|
39
|
+
detailsButtonProps?: ButtonProps;
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Error to display. If undefined, this component will look for an error property on its model.
|
|
29
43
|
* If no error is found, this component will not be displayed.
|
|
30
44
|
*/
|
|
31
|
-
error?:
|
|
45
|
+
error?: unknown;
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Message to display for the error.
|
|
34
49
|
* Defaults to the error, or any 'message' property contained within it.
|
|
35
50
|
*/
|
|
36
51
|
message?: ReactNode;
|
|
52
|
+
|
|
37
53
|
/** Optional title to display above the message. */
|
|
38
54
|
title?: ReactNode;
|
|
39
55
|
}
|
|
@@ -43,38 +59,53 @@ export interface ErrorMessageProps extends HoistProps {
|
|
|
43
59
|
*/
|
|
44
60
|
export const [ErrorMessage, errorMessage] = hoistCmp.withFactory<ErrorMessageProps>({
|
|
45
61
|
className: 'xh-error-message',
|
|
46
|
-
render(
|
|
47
|
-
{
|
|
62
|
+
render(props, ref) {
|
|
63
|
+
let {
|
|
48
64
|
className,
|
|
49
65
|
model,
|
|
50
|
-
error =
|
|
66
|
+
error = model?.['error'],
|
|
51
67
|
message,
|
|
52
68
|
title,
|
|
53
69
|
actionFn,
|
|
54
|
-
actionButtonProps
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (actionFn) {
|
|
59
|
-
actionButtonProps = {...actionButtonProps, onClick: actionFn};
|
|
60
|
-
}
|
|
70
|
+
actionButtonProps,
|
|
71
|
+
detailsFn,
|
|
72
|
+
detailsButtonProps
|
|
73
|
+
} = props;
|
|
61
74
|
|
|
62
75
|
if (isNil(error)) return null;
|
|
63
76
|
|
|
64
77
|
if (!message) {
|
|
65
78
|
if (isString(error)) {
|
|
66
|
-
message = error
|
|
79
|
+
message = error;
|
|
67
80
|
} else if (error.message) {
|
|
68
81
|
message = error.message;
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
|
|
85
|
+
if (actionFn) {
|
|
86
|
+
actionButtonProps = {...actionButtonProps, onClick: error => actionFn(error)};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (detailsFn) {
|
|
90
|
+
detailsButtonProps = {...detailsButtonProps, onClick: error => detailsFn(error)};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let buttons = [],
|
|
94
|
+
buttonBar = null;
|
|
95
|
+
if (detailsButtonProps) buttons.push(detailsButton(detailsButtonProps));
|
|
96
|
+
if (actionButtonProps) buttons.push(actionButton(actionButtonProps));
|
|
97
|
+
if (buttons.length == 1) {
|
|
98
|
+
buttonBar = buttons[0];
|
|
99
|
+
} else if (buttons.length == 2) {
|
|
100
|
+
buttonBar = hbox(buttons[0], filler(), buttons[1]);
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
return frame({
|
|
104
|
+
ref,
|
|
73
105
|
className,
|
|
74
106
|
item: div({
|
|
75
|
-
ref,
|
|
76
107
|
className: 'xh-error-message__inner',
|
|
77
|
-
items: [titleCmp({title}), messageCmp({message}),
|
|
108
|
+
items: [titleCmp({title}), messageCmp({message, error}), buttonBar]
|
|
78
109
|
})
|
|
79
110
|
});
|
|
80
111
|
}
|
|
@@ -92,11 +123,18 @@ const messageCmp = hoistCmp.factory(({message}) => {
|
|
|
92
123
|
return null;
|
|
93
124
|
});
|
|
94
125
|
|
|
95
|
-
const actionButton = hoistCmp.factory(
|
|
96
|
-
if (isEmpty(actionButtonProps)) return null;
|
|
126
|
+
const actionButton = hoistCmp.factory<ButtonProps>(props => {
|
|
97
127
|
return button({
|
|
98
128
|
text: 'Retry',
|
|
99
|
-
|
|
100
|
-
...
|
|
129
|
+
icon: Icon.refresh(),
|
|
130
|
+
...props
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const detailsButton = hoistCmp.factory<ButtonProps>(props => {
|
|
135
|
+
return button({
|
|
136
|
+
text: 'Details',
|
|
137
|
+
icon: Icon.detail(),
|
|
138
|
+
...props
|
|
101
139
|
});
|
|
102
140
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
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 © 2023 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.xh-check-box.xh-input-disabled input:checked ~ .bp4-control-indicator {
|
|
9
|
+
background-color: var(--xh-input-disabled-bg) !important;
|
|
10
|
+
&::before {
|
|
11
|
+
background-image: var(--xh-input-disabled-checkmark-svg) !important;
|
|
12
|
+
}
|
|
13
|
+
}
|