@xh/hoist 46.0.0 → 46.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 +29 -1
- package/admin/tabs/activity/clienterrors/ClientErrorDetail.js +8 -29
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.js +7 -28
- package/admin/tabs/general/alertBanner/AlertBannerPanel.js +2 -5
- package/admin/tabs/server/websocket/WebSocketModel.js +34 -20
- package/admin/tabs/server/websocket/WebSocketPanel.js +27 -7
- package/appcontainer/FeedbackDialogModel.js +2 -2
- package/cmp/ag-grid/AgGridModel.js +16 -9
- package/cmp/grid/Grid.js +13 -15
- package/cmp/grid/GridModel.js +10 -10
- package/cmp/grid/columns/Column.js +1 -1
- package/core/XH.js +23 -3
- package/data/Store.js +5 -5
- package/data/StoreRecord.js +18 -6
- package/desktop/appcontainer/AppContainer.js +13 -10
- package/desktop/appcontainer/SuspendPanel.js +54 -0
- package/desktop/appcontainer/SuspendPanel.scss +21 -0
- package/desktop/cmp/contextmenu/StoreContextMenu.js +1 -1
- package/desktop/cmp/form/FormField.js +38 -20
- package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.js +4 -4
- package/kit/onsen/styles.scss +3 -3
- package/mobile/appcontainer/AppContainer.js +10 -5
- package/mobile/appcontainer/SuspendPanel.js +55 -0
- package/mobile/appcontainer/SuspendPanel.scss +51 -0
- package/mobile/cmp/form/FormField.js +32 -16
- package/package.json +1 -1
- package/svc/GridExportService.js +3 -3
- package/svc/IdleService.js +4 -10
- package/svc/WebSocketService.js +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v46.1.0 - 2022-02-07
|
|
4
|
+
|
|
5
|
+
### Technical
|
|
6
|
+
* This release modifies our workaround to handle the ag-Grid v26 changes to cast all of their node
|
|
7
|
+
ids to strings. The initial approach in v46.0.0 -- matching the ag-Grid behavior by casting all
|
|
8
|
+
`StoreRecord` ids to strings -- was deemed too problematic for applications and has been reverted.
|
|
9
|
+
Numerical ids in Store are once again fully supported.
|
|
10
|
+
|
|
11
|
+
In order to accommodate the ag-Grid changes, applications that are using ag-Grid APIs
|
|
12
|
+
(e.g. `agApi.getNode()` ) with `StoreRecord` should be sure to use the new property `StoreRecord.agId`
|
|
13
|
+
to locate and compare records. We expect such usages to be rare in application code.
|
|
14
|
+
|
|
15
|
+
### 🎁 New Features
|
|
16
|
+
|
|
17
|
+
* `XH.showFeedbackDialog()` now takes an optional message to pre-populate within the dialog.
|
|
18
|
+
* Admins can now force suspension of individual client apps from the Server > WebSockets tab.
|
|
19
|
+
Intended to e.g. force an app to stop refreshing an expensive query or polling an endpoint removed
|
|
20
|
+
in a new release. Requires websockets to be enabled on both server and client.
|
|
21
|
+
* `FormField`s no longer need to specify a child input, and will simply render their readonly version
|
|
22
|
+
if no child is specified. This simplifies the common use-case of fields/forms that are always
|
|
23
|
+
readonly.
|
|
24
|
+
|
|
25
|
+
### 🐞 Bug Fixes
|
|
26
|
+
* `FormField` would previously throw if given a child that did not have `propTypes`. This has
|
|
27
|
+
been fixed.
|
|
28
|
+
|
|
29
|
+
[Commit Log](https://github.com/xh/hoist-react/compare/v46.0.0...v46.1.0)
|
|
30
|
+
|
|
3
31
|
## v46.0.0 - 2022-01-25
|
|
4
32
|
|
|
5
33
|
### 🎁 New Features
|
|
6
34
|
|
|
7
|
-
* `ExceptionHandler` provides a collection of
|
|
35
|
+
* `ExceptionHandler` provides a collection of overridable static properties, allowing you to set
|
|
8
36
|
app-wide default behaviour for exception handling.
|
|
9
37
|
* `XH.handleException()` takes new `alertType` option to render error alerts via the familiar
|
|
10
38
|
`dialog` or new `toast` UI.
|
|
@@ -8,7 +8,7 @@ import {form} from '@xh/hoist/cmp/form';
|
|
|
8
8
|
import {a, div, h3, hframe, span, vbox} from '@xh/hoist/cmp/layout';
|
|
9
9
|
import {hoistCmp} from '@xh/hoist/core';
|
|
10
10
|
import {formField} from '@xh/hoist/desktop/cmp/form';
|
|
11
|
-
import {jsonInput
|
|
11
|
+
import {jsonInput} from '@xh/hoist/desktop/cmp/input';
|
|
12
12
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
13
13
|
import {fmtDateTimeSec} from '@xh/hoist/format';
|
|
14
14
|
import {Icon} from '@xh/hoist/icon';
|
|
@@ -35,46 +35,25 @@ export const clientErrorDetail = hoistCmp.factory(
|
|
|
35
35
|
style: {width: '400px'},
|
|
36
36
|
items: [
|
|
37
37
|
h3(Icon.info(), 'Error Info'),
|
|
38
|
-
formField({
|
|
39
|
-
field: 'username',
|
|
40
|
-
item: textInput()
|
|
41
|
-
}),
|
|
38
|
+
formField({field: 'username'}),
|
|
42
39
|
formField({
|
|
43
40
|
field: 'dateCreated',
|
|
44
|
-
item: textInput(),
|
|
45
41
|
readonlyRenderer: fmtDateTimeSec
|
|
46
42
|
}),
|
|
47
|
-
formField({
|
|
48
|
-
field: 'appVersion',
|
|
49
|
-
item: textInput()
|
|
50
|
-
}),
|
|
43
|
+
formField({field: 'appVersion'}),
|
|
51
44
|
formField({
|
|
52
45
|
field: 'userAlerted',
|
|
53
|
-
label: 'User Alerted?'
|
|
54
|
-
item: switchInput()
|
|
55
|
-
}),
|
|
56
|
-
formField({
|
|
57
|
-
field: 'id',
|
|
58
|
-
item: textInput()
|
|
46
|
+
label: 'User Alerted?'
|
|
59
47
|
}),
|
|
48
|
+
formField({field: 'id'}),
|
|
60
49
|
formField({
|
|
61
50
|
field: 'url',
|
|
62
|
-
item: textInput(),
|
|
63
51
|
readonlyRenderer: hyperlinkVal
|
|
64
52
|
}),
|
|
65
53
|
h3(Icon.desktop(), 'Device / Browser'),
|
|
66
|
-
formField({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}),
|
|
70
|
-
formField({
|
|
71
|
-
field: 'browser',
|
|
72
|
-
item: textInput()
|
|
73
|
-
}),
|
|
74
|
-
formField({
|
|
75
|
-
field: 'userAgent',
|
|
76
|
-
item: textInput()
|
|
77
|
-
})
|
|
54
|
+
formField({field: 'device'}),
|
|
55
|
+
formField({field: 'browser'}),
|
|
56
|
+
formField({field: 'userAgent'})
|
|
78
57
|
]
|
|
79
58
|
}),
|
|
80
59
|
vbox({
|
|
@@ -5,7 +5,7 @@ import {storeFilterField} from '@xh/hoist/cmp/store';
|
|
|
5
5
|
import {hoistCmp, uses} from '@xh/hoist/core';
|
|
6
6
|
import {colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
7
7
|
import {formField} from '@xh/hoist/desktop/cmp/form';
|
|
8
|
-
import {jsonInput
|
|
8
|
+
import {jsonInput} from '@xh/hoist/desktop/cmp/input';
|
|
9
9
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
10
10
|
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
|
|
11
11
|
import {dateTimeSecRenderer, numberRenderer} from '@xh/hoist/format';
|
|
@@ -63,7 +63,6 @@ const detailRecForm = hoistCmp.factory(
|
|
|
63
63
|
h3(Icon.info(), 'Activity'),
|
|
64
64
|
formField({
|
|
65
65
|
field: 'username',
|
|
66
|
-
item: textInput(),
|
|
67
66
|
readonlyRenderer: (username) => {
|
|
68
67
|
if (!username) return naSpan();
|
|
69
68
|
const {impersonating} = formModel.values,
|
|
@@ -71,22 +70,14 @@ const detailRecForm = hoistCmp.factory(
|
|
|
71
70
|
return span(username, impSpan);
|
|
72
71
|
}
|
|
73
72
|
}),
|
|
74
|
-
formField({
|
|
75
|
-
|
|
76
|
-
item: textInput()
|
|
77
|
-
}),
|
|
78
|
-
formField({
|
|
79
|
-
field: 'msg',
|
|
80
|
-
item: textArea()
|
|
81
|
-
}),
|
|
73
|
+
formField({field: 'category'}),
|
|
74
|
+
formField({field: 'msg'}),
|
|
82
75
|
formField({
|
|
83
76
|
field: 'dateCreated',
|
|
84
|
-
item: textInput(),
|
|
85
77
|
readonlyRenderer: dateTimeSecRenderer({})
|
|
86
78
|
}),
|
|
87
79
|
formField({
|
|
88
80
|
field: 'elapsed',
|
|
89
|
-
item: textInput(),
|
|
90
81
|
readonlyRenderer: numberRenderer({
|
|
91
82
|
label: 'ms',
|
|
92
83
|
nullDisplay: '-',
|
|
@@ -94,23 +85,11 @@ const detailRecForm = hoistCmp.factory(
|
|
|
94
85
|
formatConfig: {thousandSeparated: false, mantissa: 0}
|
|
95
86
|
})
|
|
96
87
|
}),
|
|
97
|
-
formField({
|
|
98
|
-
field: 'id',
|
|
99
|
-
item: textInput()
|
|
100
|
-
}),
|
|
88
|
+
formField({field: 'id'}),
|
|
101
89
|
h3(Icon.desktop(), 'Device / Browser'),
|
|
102
|
-
formField({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}),
|
|
106
|
-
formField({
|
|
107
|
-
field: 'browser',
|
|
108
|
-
item: textInput()
|
|
109
|
-
}),
|
|
110
|
-
formField({
|
|
111
|
-
field: 'userAgent',
|
|
112
|
-
item: textInput()
|
|
113
|
-
})
|
|
90
|
+
formField({field: 'device'}),
|
|
91
|
+
formField({field: 'browser'}),
|
|
92
|
+
formField({field: 'userAgent'})
|
|
114
93
|
]
|
|
115
94
|
}),
|
|
116
95
|
panel({
|
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
buttonGroupInput,
|
|
16
16
|
dateInput,
|
|
17
17
|
switchInput,
|
|
18
|
-
textArea
|
|
19
|
-
textInput
|
|
18
|
+
textArea
|
|
20
19
|
} from '@xh/hoist/desktop/cmp/input';
|
|
21
20
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
22
21
|
import {dateTimeRenderer} from '@xh/hoist/format';
|
|
@@ -137,14 +136,12 @@ const formPanel = hoistCmp.factory(
|
|
|
137
136
|
omit: !formModel.values.updated,
|
|
138
137
|
field: 'updated',
|
|
139
138
|
className: 'xh-alert-banner-panel__form-panel__fields--ro',
|
|
140
|
-
item: textInput(),
|
|
141
139
|
readonlyRenderer: dateTimeRenderer({})
|
|
142
140
|
}),
|
|
143
141
|
formField({
|
|
144
142
|
omit: !formModel.values.updatedBy,
|
|
145
143
|
field: 'updatedBy',
|
|
146
|
-
className: 'xh-alert-banner-panel__form-panel__fields--ro'
|
|
147
|
-
item: textInput()
|
|
144
|
+
className: 'xh-alert-banner-panel__form-panel__fields--ro'
|
|
148
145
|
})
|
|
149
146
|
]
|
|
150
147
|
})
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2021 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {div, p} from '@xh/hoist/cmp/layout';
|
|
7
8
|
import {HoistModel, managed, XH} from '@xh/hoist/core';
|
|
8
9
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
9
10
|
import {textInput} from '@xh/hoist/desktop/cmp/input';
|
|
10
|
-
import {required} from '@xh/hoist/data';
|
|
11
11
|
import {Icon} from '@xh/hoist/icon';
|
|
12
12
|
import {action, observable, makeObservable} from '@xh/hoist/mobx';
|
|
13
13
|
import {Timer} from '@xh/hoist/utils/async';
|
|
14
14
|
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
15
15
|
import {isDisplayed} from '@xh/hoist/utils/js';
|
|
16
16
|
import * as Col from '@xh/hoist/admin/columns';
|
|
17
|
+
import {isEmpty} from 'lodash';
|
|
17
18
|
import {createRef} from 'react';
|
|
18
19
|
import * as WSCol from './WebSocketColumns';
|
|
19
20
|
|
|
@@ -37,12 +38,13 @@ export class WebSocketModel extends HoistModel {
|
|
|
37
38
|
this.gridModel = new GridModel({
|
|
38
39
|
emptyText: 'No clients connected.',
|
|
39
40
|
enableExport: true,
|
|
41
|
+
selModel: 'multiple',
|
|
40
42
|
store: {
|
|
41
43
|
idSpec: 'key',
|
|
42
44
|
processRawData: row => {
|
|
43
45
|
const authUser = row.authUser.username,
|
|
44
46
|
apparentUser = row.apparentUser.username,
|
|
45
|
-
impersonating = authUser
|
|
47
|
+
impersonating = authUser !== apparentUser;
|
|
46
48
|
|
|
47
49
|
return {
|
|
48
50
|
...row,
|
|
@@ -91,29 +93,41 @@ export class WebSocketModel extends HoistModel {
|
|
|
91
93
|
this.lastRefresh = Date.now();
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
async
|
|
95
|
-
const {
|
|
96
|
-
if (
|
|
96
|
+
async forceSuspendOnSelectedAsync() {
|
|
97
|
+
const {selectedRecords} = this.gridModel;
|
|
98
|
+
if (isEmpty(selectedRecords)) return;
|
|
97
99
|
|
|
98
100
|
const message = await XH.prompt({
|
|
99
|
-
title: '
|
|
100
|
-
icon: Icon.
|
|
101
|
-
confirmProps: {text: '
|
|
102
|
-
|
|
101
|
+
title: 'Force suspend',
|
|
102
|
+
icon: Icon.stopCircle(),
|
|
103
|
+
confirmProps: {text: 'Force Suspend', icon: Icon.stopCircle(), intent: 'danger'},
|
|
104
|
+
cancelProps: {autoFocus: true},
|
|
105
|
+
message: div(
|
|
106
|
+
p(`This action will force ${selectedRecords.length} connected client(s) into suspended mode, halting all background refreshes and other activity, masking the UI, and requiring users to reload the app to continue.`),
|
|
107
|
+
p('If desired, you can enter a message below to display within the suspended app.')
|
|
108
|
+
),
|
|
103
109
|
input: {
|
|
104
|
-
item: textInput({
|
|
105
|
-
initialValue:
|
|
106
|
-
rules: [required]
|
|
110
|
+
item: textInput({placeholder: 'User-facing message (optional)'}),
|
|
111
|
+
initialValue: null
|
|
107
112
|
}
|
|
108
113
|
});
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (message !== false) {
|
|
116
|
+
const tasks = selectedRecords
|
|
117
|
+
.map((rec) => XH.fetchJson({
|
|
118
|
+
url: 'webSocketAdmin/pushToChannel',
|
|
119
|
+
params: {
|
|
120
|
+
channelKey: rec.data.key,
|
|
121
|
+
topic: XH.webSocketService.FORCE_APP_SUSPEND_TOPIC,
|
|
122
|
+
message
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
await Promise.allSettled(tasks).track({
|
|
127
|
+
category: 'Audit',
|
|
128
|
+
message: 'Suspended clients via WebSocket',
|
|
129
|
+
data: {users: selectedRecords.map(it => it.data.user).sort()}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
118
132
|
}
|
|
119
133
|
}
|
|
@@ -6,28 +6,30 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {WebSocketModel} from '@xh/hoist/admin/tabs/server/websocket/WebSocketModel';
|
|
8
8
|
import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
9
|
-
import {filler} from '@xh/hoist/cmp/layout';
|
|
9
|
+
import {filler, box, fragment, p} from '@xh/hoist/cmp/layout';
|
|
10
10
|
import {relativeTimestamp} from '@xh/hoist/cmp/relativetimestamp';
|
|
11
11
|
import {storeFilterField} from '@xh/hoist/cmp/store';
|
|
12
|
-
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
12
|
+
import {XH, creates, hoistCmp} from '@xh/hoist/core';
|
|
13
13
|
import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
|
|
14
14
|
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
15
15
|
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
16
16
|
import {Icon} from '@xh/hoist/icon';
|
|
17
|
+
import {errorMessage} from '@xh/hoist/desktop/cmp/error';
|
|
17
18
|
|
|
18
19
|
export const webSocketPanel = hoistCmp.factory({
|
|
19
20
|
|
|
20
21
|
model: creates(WebSocketModel),
|
|
21
22
|
|
|
22
23
|
render({model}) {
|
|
24
|
+
if (!XH.webSocketService.enabled) return notPresentMessage();
|
|
23
25
|
return panel({
|
|
24
26
|
tbar: [
|
|
25
27
|
button({
|
|
26
|
-
text: '
|
|
27
|
-
icon: Icon.
|
|
28
|
-
intent: '
|
|
29
|
-
disabled: !model.gridModel.
|
|
30
|
-
onClick: () => model.
|
|
28
|
+
text: 'Force suspend',
|
|
29
|
+
icon: Icon.stopCircle(),
|
|
30
|
+
intent: 'danger',
|
|
31
|
+
disabled: !model.gridModel.hasSelection,
|
|
32
|
+
onClick: () => model.forceSuspendOnSelectedAsync()
|
|
31
33
|
}),
|
|
32
34
|
filler(),
|
|
33
35
|
relativeTimestamp({bind: 'lastRefresh'}),
|
|
@@ -43,3 +45,21 @@ export const webSocketPanel = hoistCmp.factory({
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
});
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const notPresentMessage = hoistCmp.factory(
|
|
51
|
+
() => box({
|
|
52
|
+
height: 200,
|
|
53
|
+
width: 1000,
|
|
54
|
+
items: [
|
|
55
|
+
errorMessage({
|
|
56
|
+
error: {
|
|
57
|
+
message: fragment(
|
|
58
|
+
p('WebSockets are not enabled in this application.'),
|
|
59
|
+
p('Please ensure that you have enabled web sockets in your server and client application configuration.')
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
);
|
|
@@ -463,17 +463,16 @@ export class AgGridModel extends HoistModel {
|
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
/** @returns {(string[]
|
|
466
|
+
/** @returns {(string[])} - list of selected row node ids */
|
|
467
467
|
getSelectedRowNodeIds() {
|
|
468
468
|
this.throwIfNotReady();
|
|
469
|
-
|
|
470
|
-
return this.agApi.getSelectedRows().map(it => it.id);
|
|
469
|
+
return this.agApi.getSelectedNodes().map(it => it.id);
|
|
471
470
|
}
|
|
472
471
|
|
|
473
472
|
/**
|
|
474
473
|
* Sets the selected row node ids. Any rows currently selected which are not in the list will be
|
|
475
474
|
* deselected.
|
|
476
|
-
* @param ids {(string[]
|
|
475
|
+
* @param ids {(string[])} - row node ids to mark as selected
|
|
477
476
|
*/
|
|
478
477
|
setSelectedRowNodeIds(ids) {
|
|
479
478
|
this.throwIfNotReady();
|
|
@@ -487,19 +486,27 @@ export class AgGridModel extends HoistModel {
|
|
|
487
486
|
}
|
|
488
487
|
|
|
489
488
|
/**
|
|
490
|
-
* @returns {
|
|
489
|
+
* @returns {string} - the id of the first row in the grid, after sorting and filtering, which
|
|
491
490
|
* has data associated with it (i.e. not a group or other synthetic row).
|
|
492
491
|
*/
|
|
493
492
|
getFirstSelectableRowNodeId() {
|
|
493
|
+
return this.getFirstSelectableRowNode()?.id;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @returns {{Object}} - the first row in the grid, after sorting and filtering, which
|
|
498
|
+
* has data associated with it (i.e. not a group or other synthetic row).
|
|
499
|
+
*/
|
|
500
|
+
getFirstSelectableRowNode() {
|
|
494
501
|
this.throwIfNotReady();
|
|
495
502
|
|
|
496
|
-
let
|
|
503
|
+
let ret = null;
|
|
497
504
|
this.agApi.forEachNodeAfterFilterAndSort(node => {
|
|
498
|
-
if (
|
|
499
|
-
|
|
505
|
+
if (!ret && node.data) {
|
|
506
|
+
ret = node;
|
|
500
507
|
}
|
|
501
508
|
});
|
|
502
|
-
return
|
|
509
|
+
return ret;
|
|
503
510
|
}
|
|
504
511
|
|
|
505
512
|
/**
|
package/cmp/grid/Grid.js
CHANGED
|
@@ -193,7 +193,7 @@ class GridLocalModel extends HoistModel {
|
|
|
193
193
|
immutableData: true,
|
|
194
194
|
rowDataChangeDetectionStrategy: 'IdentityCheck',
|
|
195
195
|
suppressColumnVirtualisation: !model.useVirtualColumns,
|
|
196
|
-
getRowNodeId: (
|
|
196
|
+
getRowNodeId: (record) => record.agId,
|
|
197
197
|
defaultColDef: {
|
|
198
198
|
sortable: true,
|
|
199
199
|
resizable: true,
|
|
@@ -292,7 +292,7 @@ class GridLocalModel extends HoistModel {
|
|
|
292
292
|
|
|
293
293
|
getContextMenuItems = (params) => {
|
|
294
294
|
const {model, agOptions} = this,
|
|
295
|
-
{
|
|
295
|
+
{selModel, contextMenu} = model;
|
|
296
296
|
if (!contextMenu || XH.isMobileApp) return null;
|
|
297
297
|
|
|
298
298
|
let menu = null;
|
|
@@ -303,10 +303,9 @@ class GridLocalModel extends HoistModel {
|
|
|
303
303
|
}
|
|
304
304
|
if (!menu) return null;
|
|
305
305
|
|
|
306
|
-
const
|
|
306
|
+
const record = params.node?.data,
|
|
307
307
|
colId = params.column?.colId,
|
|
308
|
-
|
|
309
|
-
column = isNil(colId) ? null : model.getColumn(colId),
|
|
308
|
+
column = !isNil(colId) ? model.getColumn(colId) : null,
|
|
310
309
|
{selectedRecords} = model;
|
|
311
310
|
|
|
312
311
|
|
|
@@ -637,7 +636,7 @@ class GridLocalModel extends HoistModel {
|
|
|
637
636
|
// Refresh cells in columns with complex renderers
|
|
638
637
|
const refreshCols = visibleCols.filter(c => c.rendererIsComplex);
|
|
639
638
|
if (!isEmpty(refreshCols)) {
|
|
640
|
-
const rowNodes = compact(transaction.update.map(r => agApi.getRowNode(r.
|
|
639
|
+
const rowNodes = compact(transaction.update.map(r => agApi.getRowNode(r.agId))),
|
|
641
640
|
columns = refreshCols.map(c => c.colId);
|
|
642
641
|
agApi.refreshCells({rowNodes, columns, force: true});
|
|
643
642
|
}
|
|
@@ -667,8 +666,8 @@ class GridLocalModel extends HoistModel {
|
|
|
667
666
|
}
|
|
668
667
|
|
|
669
668
|
syncSelection() {
|
|
670
|
-
const {agGridModel, selModel, isReady} = this.model
|
|
671
|
-
|
|
669
|
+
const {agGridModel, selModel, isReady} = this.model;
|
|
670
|
+
const selectedIds = selModel.selectedRecords.map(r => r.agId);
|
|
672
671
|
if (isReady && !isEqual(selectedIds, agGridModel.getSelectedRowNodeIds())) {
|
|
673
672
|
agGridModel.setSelectedRowNodeIds(selectedIds);
|
|
674
673
|
}
|
|
@@ -685,8 +684,8 @@ class GridLocalModel extends HoistModel {
|
|
|
685
684
|
//------------------------
|
|
686
685
|
// Event Handlers on AG Grid.
|
|
687
686
|
//------------------------
|
|
688
|
-
getDataPath = (
|
|
689
|
-
return
|
|
687
|
+
getDataPath = (record) => {
|
|
688
|
+
return record.treePath;
|
|
690
689
|
};
|
|
691
690
|
|
|
692
691
|
// We debounce this handler because the implementation of `AgGridModel.setSelectedRowNodeIds()`
|
|
@@ -762,11 +761,10 @@ class GridLocalModel extends HoistModel {
|
|
|
762
761
|
}
|
|
763
762
|
|
|
764
763
|
processCellForClipboard = ({value, node, column}) => {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
colId = column
|
|
768
|
-
|
|
769
|
-
xhColumn = isNil(colId) ? null : model.getColumn(colId);
|
|
764
|
+
const record = node.data,
|
|
765
|
+
{model} = this,
|
|
766
|
+
{colId} = column,
|
|
767
|
+
xhColumn = !isNil(colId) ? model.getColumn(colId) : null;
|
|
770
768
|
|
|
771
769
|
if (!record || !xhColumn) return value;
|
|
772
770
|
|
package/cmp/grid/GridModel.js
CHANGED
|
@@ -221,7 +221,7 @@ export class GridModel extends HoistModel {
|
|
|
221
221
|
* @param {?ReactNode} [c.restoreDefaultsWarning] - Confirmation warning to be presented to
|
|
222
222
|
* user before restoring default grid state. Set to null to skip user confirmation.
|
|
223
223
|
* @param {GridModelPersistOptions} [c.persistWith] - options governing persistence.
|
|
224
|
-
* @param {?
|
|
224
|
+
* @param {?ReactNode} [c.emptyText] - text/element to display if grid has no records.
|
|
225
225
|
* Defaults to null, in which case no empty text will be shown.
|
|
226
226
|
* @param {boolean} [c.hideEmptyTextBeforeLoad] - true (default) to hide empty text until
|
|
227
227
|
* after the Store has been loaded at least once.
|
|
@@ -559,7 +559,7 @@ export class GridModel extends HoistModel {
|
|
|
559
559
|
|
|
560
560
|
// Get first displayed row with data - i.e. backed by a record, not a full-width group row.
|
|
561
561
|
const {selModel} = this,
|
|
562
|
-
id = this.agGridModel.
|
|
562
|
+
id = this.agGridModel.getFirstSelectableRowNode()?.data.id;
|
|
563
563
|
|
|
564
564
|
if (id != null) {
|
|
565
565
|
selModel.select(id);
|
|
@@ -603,8 +603,8 @@ export class GridModel extends HoistModel {
|
|
|
603
603
|
indices = [];
|
|
604
604
|
|
|
605
605
|
// 1) Expand any selected nodes that are collapsed
|
|
606
|
-
selectedRecords.forEach(({
|
|
607
|
-
for (let row = agApi.getRowNode(
|
|
606
|
+
selectedRecords.forEach(({agId}) => {
|
|
607
|
+
for (let row = agApi.getRowNode(agId)?.parent; row; row = row.parent) {
|
|
608
608
|
if (!row.expanded) {
|
|
609
609
|
agApi.setRowNodeExpanded(row, true);
|
|
610
610
|
}
|
|
@@ -614,8 +614,8 @@ export class GridModel extends HoistModel {
|
|
|
614
614
|
await wait();
|
|
615
615
|
|
|
616
616
|
// 2) Scroll to all selected nodes
|
|
617
|
-
selectedRecords.forEach(({
|
|
618
|
-
const rowIndex = agApi.getRowNode(
|
|
617
|
+
selectedRecords.forEach(({agId}) => {
|
|
618
|
+
const rowIndex = agApi.getRowNode(agId)?.rowIndex;
|
|
619
619
|
if (!isNil(rowIndex)) indices.push(rowIndex);
|
|
620
620
|
});
|
|
621
621
|
|
|
@@ -736,7 +736,7 @@ export class GridModel extends HoistModel {
|
|
|
736
736
|
|
|
737
737
|
/**
|
|
738
738
|
* Set the text displayed when the grid is empty.
|
|
739
|
-
* @param {?
|
|
739
|
+
* @param {?ReactNode} emptyText - text/element to display if grid has no records.
|
|
740
740
|
*/
|
|
741
741
|
@action
|
|
742
742
|
setEmptyText(emptyText) {
|
|
@@ -857,7 +857,7 @@ export class GridModel extends HoistModel {
|
|
|
857
857
|
|
|
858
858
|
// Check required as we may be receiving stale message after unmounting
|
|
859
859
|
if (isReady) {
|
|
860
|
-
selModel.select(agGridModel.
|
|
860
|
+
selModel.select(agGridModel.agApi.getSelectedRows().map(r => r.id));
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
863
|
|
|
@@ -1120,12 +1120,12 @@ export class GridModel extends HoistModel {
|
|
|
1120
1120
|
recToEdit = selectedRecords[0];
|
|
1121
1121
|
} else {
|
|
1122
1122
|
// Or use the first record overall.
|
|
1123
|
-
const firstRowId = agGridModel.
|
|
1123
|
+
const firstRowId = agGridModel.getFirstSelectableRowNode()?.data.id;
|
|
1124
1124
|
recToEdit = store.getById(firstRowId);
|
|
1125
1125
|
}
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
1128
|
-
const rowIndex = agApi.getRowNode(recToEdit?.
|
|
1128
|
+
const rowIndex = agApi.getRowNode(recToEdit?.agId)?.rowIndex;
|
|
1129
1129
|
if (isNil(rowIndex) || rowIndex < 0) {
|
|
1130
1130
|
console.warn(
|
|
1131
1131
|
'Unable to start editing - ' +
|
package/core/XH.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
TrackService,
|
|
28
28
|
WebSocketService
|
|
29
29
|
} from '@xh/hoist/svc';
|
|
30
|
+
import {Timer} from '@xh/hoist/utils/async';
|
|
30
31
|
import {MINUTES} from '@xh/hoist/utils/datetime';
|
|
31
32
|
import {checkMinVersion, getClientDeviceInfo, throwIf, withDebug} from '@xh/hoist/utils/js';
|
|
32
33
|
import {camelCase, compact, flatten, isBoolean, isString, uniqueId} from 'lodash';
|
|
@@ -188,6 +189,7 @@ class XHClass extends HoistBase {
|
|
|
188
189
|
//---------------------------
|
|
189
190
|
// Other State
|
|
190
191
|
//---------------------------
|
|
192
|
+
suspendData = null;
|
|
191
193
|
accessDeniedMessage = null;
|
|
192
194
|
exceptionHandler = new ExceptionHandler();
|
|
193
195
|
|
|
@@ -609,9 +611,12 @@ class XHClass extends HoistBase {
|
|
|
609
611
|
this.acm.changelogDialogModel.show();
|
|
610
612
|
}
|
|
611
613
|
|
|
612
|
-
/**
|
|
613
|
-
|
|
614
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Show a dialog to elicit feedback from the user.
|
|
616
|
+
* @param {string} [message] - optional message to preset within the feedback dialog.
|
|
617
|
+
*/
|
|
618
|
+
showFeedbackDialog({message} = {}) {
|
|
619
|
+
this.acm.feedbackDialogModel.show({message});
|
|
615
620
|
}
|
|
616
621
|
|
|
617
622
|
/** Show the impersonation bar to allow switching users. */
|
|
@@ -805,6 +810,21 @@ class XHClass extends HoistBase {
|
|
|
805
810
|
}
|
|
806
811
|
}
|
|
807
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Suspend all app activity and display, including timers and web sockets.
|
|
815
|
+
*
|
|
816
|
+
* Suspension is a terminal state, requiring user to reload the app.
|
|
817
|
+
* Used for idling, forced version upgrades, and ad-hoc killing of problematic clients.
|
|
818
|
+
* @package - not intended for application use.
|
|
819
|
+
*/
|
|
820
|
+
suspendApp(suspendData) {
|
|
821
|
+
if (XH.appState === AppState.SUSPENDED) return;
|
|
822
|
+
this.suspendData = suspendData;
|
|
823
|
+
XH.setAppState(AppState.SUSPENDED);
|
|
824
|
+
XH.webSocketService.shutdown();
|
|
825
|
+
Timer.cancelAll();
|
|
826
|
+
}
|
|
827
|
+
|
|
808
828
|
//------------------------
|
|
809
829
|
// Implementation
|
|
810
830
|
//------------------------
|