@xh/hoist 47.0.1 → 47.1.2
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 +35 -2
- package/admin/tabs/general/about/AboutPanel.scss +19 -17
- package/appcontainer/MessageModel.js +4 -1
- package/appcontainer/ThemeModel.js +28 -5
- package/cmp/clock/Clock.js +3 -2
- package/cmp/form/field/SubformsFieldModel.js +7 -2
- package/cmp/grid/GridModel.js +11 -0
- package/cmp/grid/impl/ColumnWidthCalculator.js +44 -8
- package/core/AppSpec.js +9 -4
- package/core/XH.js +24 -3
- package/data/filter/FieldFilter.js +25 -6
- package/desktop/appcontainer/AppContainer.js +9 -1
- package/desktop/appcontainer/ImpersonationBar.js +1 -0
- package/desktop/appcontainer/Message.js +9 -3
- package/desktop/cmp/appOption/ThemeAppOption.js +5 -4
- package/desktop/cmp/input/Select.js +1 -1
- package/mobile/appcontainer/AppContainer.js +9 -1
- package/mobile/appcontainer/Message.js +10 -6
- package/mobile/cmp/appOption/ThemeAppOption.js +12 -6
- package/mobile/cmp/form/FormField.js +1 -1
- package/mobile/cmp/input/Select.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v47.
|
|
3
|
+
## v47.1.2 - 2022-04-01
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### 🐞 Bug Fixes
|
|
6
|
+
|
|
7
|
+
* `FieldFilter`'s check of `committedData` is now null safe. A record with no `committedData` will not be filtered out.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## v47.1.1 - 2022-03-26
|
|
11
|
+
|
|
12
|
+
### 🎁 New Features
|
|
13
|
+
|
|
14
|
+
* New "sync with system" theme option - sets the Hoist theme to light/dark based on the user's OS.
|
|
15
|
+
* Added `cancelAlign` config to `XH.message()` and variants. Customize to "left" to render
|
|
16
|
+
Cancel and Confirm actions separated by a filler.
|
|
17
|
+
* Added `GridModel.restoreDefaultsFn`, an optional function called after `restoreDefaultsAsync`.
|
|
18
|
+
Allows apps to run additional, app-specific logic after a grid has been reset (e.g. resetting
|
|
19
|
+
other, related preferences or state not managed by `GridModel` directly).
|
|
20
|
+
* Added `AppSpec.lockoutPanel`, allowing apps to specify a custom component.
|
|
21
|
+
|
|
22
|
+
### 🐞 Bug Fixes
|
|
23
|
+
|
|
24
|
+
* Fixed column auto-sizing when `headerName` is/returns an element.
|
|
25
|
+
* Fixed bug where subforms were not properly registering as dirty.
|
|
26
|
+
* Fixed an issue where `Select` inputs would commit `null` whilst clearing the text input.
|
|
27
|
+
* Fixed `Clock` component bug introduced in v47 (configured timezone was not respected).
|
|
28
|
+
|
|
29
|
+
### 📚 Libraries
|
|
30
|
+
|
|
31
|
+
* @blueprintjs/core `3.53 -> 3.54`
|
|
32
|
+
* @blueprintjs/datetime `3.23 -> 3.24`
|
|
33
|
+
|
|
34
|
+
[Commit Log](https://github.com/xh/hoist-react/compare/v47.0.1...v47.1.1)
|
|
35
|
+
|
|
36
|
+
## v47.0.1 - 2022-03-06
|
|
6
37
|
|
|
7
38
|
### 🐞 Bug Fixes
|
|
8
39
|
|
|
9
40
|
* Fix to mobile `ColChooser` error re. internal model handling.
|
|
10
41
|
|
|
42
|
+
[Commit Log](https://github.com/xh/hoist-react/compare/v47.0.0...v47.0.1)
|
|
43
|
+
|
|
11
44
|
## v47.0.0 - 2022-03-04
|
|
12
45
|
|
|
13
46
|
### 🎁 New Features
|
|
@@ -20,27 +20,29 @@
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
table {
|
|
23
|
-
margin: var(--xh-pad-px);
|
|
24
|
-
border-spacing: 0;
|
|
25
|
-
border-collapse: collapse;
|
|
26
|
-
min-width: 500px;
|
|
27
23
|
background-color: var(--xh-bg);
|
|
28
|
-
|
|
24
|
+
border-collapse: collapse;
|
|
25
|
+
border-spacing: 0;
|
|
26
|
+
margin: var(--xh-pad-px);
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
tr {
|
|
29
|
+
border: var(--xh-border-solid);
|
|
30
|
+
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
th {
|
|
33
|
+
background-color: var(--xh-bg-alt);
|
|
34
|
+
padding: var(--xh-pad-half-px);
|
|
35
|
+
text-align: right;
|
|
36
|
+
vertical-align: top;
|
|
37
|
+
width: 180px;
|
|
38
|
+
}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
td {
|
|
41
|
+
max-width: 600px;
|
|
42
|
+
min-width: 600px;
|
|
43
|
+
padding: var(--xh-pad-half-px);
|
|
44
|
+
word-wrap: break-word;
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
&__blurb {
|
|
@@ -23,6 +23,7 @@ export class MessageModel extends HoistModel {
|
|
|
23
23
|
input;
|
|
24
24
|
confirmProps;
|
|
25
25
|
cancelProps;
|
|
26
|
+
cancelAlign;
|
|
26
27
|
onConfirm;
|
|
27
28
|
onCancel;
|
|
28
29
|
messageKey;
|
|
@@ -41,6 +42,7 @@ export class MessageModel extends HoistModel {
|
|
|
41
42
|
input,
|
|
42
43
|
confirmProps = {},
|
|
43
44
|
cancelProps = {},
|
|
45
|
+
cancelAlign = 'right',
|
|
44
46
|
onConfirm,
|
|
45
47
|
onCancel,
|
|
46
48
|
|
|
@@ -73,6 +75,7 @@ export class MessageModel extends HoistModel {
|
|
|
73
75
|
|
|
74
76
|
this.confirmProps = this.parseButtonProps(confirmProps, () => this.doConfirmAsync(), confirmText, confirmIntent);
|
|
75
77
|
this.cancelProps = this.parseButtonProps(cancelProps, () => this.doCancel(), cancelText, cancelIntent);
|
|
78
|
+
this.cancelAlign = cancelAlign;
|
|
76
79
|
|
|
77
80
|
this.onConfirm = onConfirm;
|
|
78
81
|
this.onCancel = onCancel;
|
|
@@ -135,7 +138,7 @@ export class MessageModel extends HoistModel {
|
|
|
135
138
|
|
|
136
139
|
/**
|
|
137
140
|
* @typedef {Object} MessageInput
|
|
138
|
-
* @property {Element} [item] - the
|
|
141
|
+
* @property {Element} [item] - the React element to render; should be a HoistInput, defaults to a
|
|
139
142
|
* platform appropriate TextInput.
|
|
140
143
|
* @property {Rule[]} [rules] - validation constraints to apply.
|
|
141
144
|
* @property {*} [initialValue] - initial value for the input.
|
|
@@ -13,8 +13,8 @@ import {action, observable, makeObservable} from '@xh/hoist/mobx';
|
|
|
13
13
|
* @private
|
|
14
14
|
*/
|
|
15
15
|
export class ThemeModel extends HoistModel {
|
|
16
|
-
|
|
17
|
-
@observable darkTheme
|
|
16
|
+
/** @member {boolean} */
|
|
17
|
+
@observable darkTheme;
|
|
18
18
|
|
|
19
19
|
constructor() {
|
|
20
20
|
super();
|
|
@@ -23,7 +23,7 @@ export class ThemeModel extends HoistModel {
|
|
|
23
23
|
|
|
24
24
|
@action
|
|
25
25
|
toggleTheme() {
|
|
26
|
-
this.
|
|
26
|
+
this.setTheme(this.darkTheme ? 'light' : 'dark');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
@action
|
|
@@ -32,10 +32,33 @@ export class ThemeModel extends HoistModel {
|
|
|
32
32
|
classList.toggle('xh-dark', value);
|
|
33
33
|
classList.toggle('bp3-dark', value);
|
|
34
34
|
this.darkTheme = value;
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@action
|
|
39
|
+
setTheme(value) {
|
|
40
|
+
switch (value) {
|
|
41
|
+
case 'system':
|
|
42
|
+
this.setDarkTheme(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
43
|
+
break;
|
|
44
|
+
case 'dark':
|
|
45
|
+
this.setDarkTheme(true);
|
|
46
|
+
break;
|
|
47
|
+
case 'light':
|
|
48
|
+
this.setDarkTheme(false);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw XH.exception("Unrecognized value for theme pref. Must be either 'system', 'dark', or 'light'.");
|
|
52
|
+
}
|
|
53
|
+
XH.setPref('xhTheme', value);
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
init() {
|
|
39
|
-
this.
|
|
57
|
+
this.setTheme(XH.getPref('xhTheme'));
|
|
58
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
|
|
59
|
+
if (XH.getPref('xhTheme') === 'system') {
|
|
60
|
+
this.setDarkTheme(event.matches);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
40
63
|
}
|
|
41
64
|
}
|
package/cmp/clock/Clock.js
CHANGED
|
@@ -86,7 +86,7 @@ class LocalModel extends HoistModel {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
async loadTimezoneOffsetAsync() {
|
|
89
|
-
const {timezone} = this;
|
|
89
|
+
const {timezone} = this.componentProps;
|
|
90
90
|
|
|
91
91
|
try {
|
|
92
92
|
if (!timezone) {
|
|
@@ -95,10 +95,11 @@ class LocalModel extends HoistModel {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
const offsetResp = await XH.fetchJson({
|
|
99
99
|
url: 'xh/getTimeZoneOffset',
|
|
100
100
|
params: {timeZoneId: timezone}
|
|
101
101
|
});
|
|
102
|
+
this.offset = offsetResp.offset;
|
|
102
103
|
this.offsetException = null;
|
|
103
104
|
} catch (e) {
|
|
104
105
|
XH.handleException(e, {showAlert: false, logOnServer: false});
|
|
@@ -8,7 +8,7 @@ import {managed, XH} from '@xh/hoist/core';
|
|
|
8
8
|
import {ValidationState} from '@xh/hoist/data';
|
|
9
9
|
import {action, computed, makeObservable, override} from '@xh/hoist/mobx';
|
|
10
10
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
11
|
-
import {clone, defaults, flatMap, isArray, partition, without} from 'lodash';
|
|
11
|
+
import {clone, defaults, isEqual, flatMap, isArray, partition, without} from 'lodash';
|
|
12
12
|
import {executeIfFunction, withDefault} from '../../../utils/js';
|
|
13
13
|
import {FormModel} from '../FormModel';
|
|
14
14
|
import {BaseFieldModel} from './BaseFieldModel';
|
|
@@ -121,7 +121,12 @@ export class SubformsFieldModel extends BaseFieldModel {
|
|
|
121
121
|
|
|
122
122
|
@computed
|
|
123
123
|
get isDirty() {
|
|
124
|
-
|
|
124
|
+
// Catch changed values within subforms, as well as adds/deletes/sorts
|
|
125
|
+
const {value, initialValue} = this;
|
|
126
|
+
return (
|
|
127
|
+
value.some(s => s.isDirty) ||
|
|
128
|
+
!isEqual(initialValue.map(s => s.getData()), value.map(s => s.getData()))
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
@override
|
package/cmp/grid/GridModel.js
CHANGED
|
@@ -111,6 +111,8 @@ export class GridModel extends HoistModel {
|
|
|
111
111
|
useVirtualColumns;
|
|
112
112
|
/** @member {GridAutosizeOptions} */
|
|
113
113
|
autosizeOptions;
|
|
114
|
+
/** @member {function} */
|
|
115
|
+
restoreDefaultsFn;
|
|
114
116
|
/** @member {ReactNode} */
|
|
115
117
|
restoreDefaultsWarning;
|
|
116
118
|
/** @member {boolean} */
|
|
@@ -218,6 +220,9 @@ export class GridModel extends HoistModel {
|
|
|
218
220
|
* GridFilterModel, or boolean `true` to enable default. Desktop only.
|
|
219
221
|
* @param {(ColChooserModelConfig|boolean)} [c.colChooserModel] - config with which to create a
|
|
220
222
|
* ColChooserModel, or boolean `true` to enable default.
|
|
223
|
+
* @param {function} [c.restoreDefaultsFn] - Async function to be called when the user triggers
|
|
224
|
+
* GridModel.restoreDefaultsAsync(). This function will be called after the built-in
|
|
225
|
+
* defaults have been restored, and can be used to restore application specific defaults.
|
|
221
226
|
* @param {?ReactNode} [c.restoreDefaultsWarning] - Confirmation warning to be presented to
|
|
222
227
|
* user before restoring default grid state. Set to null to skip user confirmation.
|
|
223
228
|
* @param {GridModelPersistOptions} [c.persistWith] - options governing persistence.
|
|
@@ -359,6 +364,7 @@ export class GridModel extends HoistModel {
|
|
|
359
364
|
contextMenu,
|
|
360
365
|
useVirtualColumns = false,
|
|
361
366
|
autosizeOptions = {},
|
|
367
|
+
restoreDefaultsFn,
|
|
362
368
|
restoreDefaultsWarning = GridModel.DEFAULT_RESTORE_DEFAULTS_WARNING,
|
|
363
369
|
fullRowEditing = false,
|
|
364
370
|
clicksToEdit = 2,
|
|
@@ -398,6 +404,7 @@ export class GridModel extends HoistModel {
|
|
|
398
404
|
fillMode: 'none'
|
|
399
405
|
}
|
|
400
406
|
);
|
|
407
|
+
this.restoreDefaultsFn = restoreDefaultsFn;
|
|
401
408
|
this.restoreDefaultsWarning = restoreDefaultsWarning;
|
|
402
409
|
this.fullRowEditing = fullRowEditing;
|
|
403
410
|
this.clicksToExpand = clicksToExpand;
|
|
@@ -486,6 +493,10 @@ export class GridModel extends HoistModel {
|
|
|
486
493
|
await this.autosizeAsync();
|
|
487
494
|
}
|
|
488
495
|
|
|
496
|
+
if (this.restoreDefaultsFn) {
|
|
497
|
+
await this.restoreDefaultsFn();
|
|
498
|
+
}
|
|
499
|
+
|
|
489
500
|
return true;
|
|
490
501
|
}
|
|
491
502
|
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* Copyright © 2021 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {XH} from '@xh/hoist/core';
|
|
8
9
|
import {stripTags} from '@xh/hoist/utils/js';
|
|
9
|
-
import {forOwn, groupBy, isEmpty, isFunction, isNil, map, max, min,
|
|
10
|
+
import {forOwn, groupBy, isEmpty, isArray, isFunction, isNil, isString, map, max, min, sortBy} from 'lodash';
|
|
11
|
+
import {isValidElement} from 'react';
|
|
12
|
+
import {renderToStaticMarkup} from 'react-dom/server';
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Calculates the column width required to display column. Used by GridAutoSizeService.
|
|
@@ -151,33 +154,48 @@ export class ColumnWidthCalculator {
|
|
|
151
154
|
// Autosize header cell
|
|
152
155
|
//------------------
|
|
153
156
|
getHeaderWidth(gridModel, column, includeHeaderIcons, bufferPx) {
|
|
154
|
-
const {colId,
|
|
157
|
+
const {colId, agOptions, sortable, filterable} = column,
|
|
155
158
|
{sizingMode} = gridModel,
|
|
156
|
-
headerHtml =
|
|
159
|
+
headerHtml = this.getHeaderHtml(gridModel, column),
|
|
160
|
+
headerClass = this.getHeaderClass(gridModel, column),
|
|
157
161
|
showSort = sortable && (includeHeaderIcons || gridModel.sortBy.find(sorter => sorter.colId === colId)),
|
|
158
162
|
showMenu = (agOptions?.suppressMenu === false || filterable) && includeHeaderIcons;
|
|
159
163
|
|
|
160
164
|
// Render to a hidden header cell to calculate the max displayed width
|
|
161
165
|
const headerEl = this.getHeaderEl();
|
|
162
|
-
this.setHeaderClassNames(sizingMode, showSort, showMenu);
|
|
166
|
+
this.setHeaderClassNames(sizingMode, showSort, showMenu, headerClass);
|
|
163
167
|
headerEl.firstChild.innerHTML = headerHtml;
|
|
164
168
|
return Math.ceil(headerEl.clientWidth) + bufferPx;
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
getHeaderHtml(gridModel, column) {
|
|
172
|
+
const {headerName} = column,
|
|
173
|
+
headerValue = isFunction(headerName) ? headerName({column, gridModel}) : headerName;
|
|
174
|
+
|
|
175
|
+
if (isString(headerValue)) {
|
|
176
|
+
return headerValue;
|
|
177
|
+
} else if (isValidElement(headerValue)) {
|
|
178
|
+
return renderToStaticMarkup(headerValue);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw XH.exception('Unable to get column header html because value is not a string or valid react element');
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
resetHeaderClassNames() {
|
|
168
185
|
const headerEl = this.getHeaderEl();
|
|
169
186
|
headerEl.classList.remove(...headerEl.classList);
|
|
170
187
|
headerEl.classList.add('xh-grid-autosize-header');
|
|
171
188
|
}
|
|
172
189
|
|
|
173
|
-
setHeaderClassNames(sizingMode, showSort, showMenu) {
|
|
190
|
+
setHeaderClassNames(sizingMode, showSort, showMenu, headerClass) {
|
|
174
191
|
this.resetHeaderClassNames();
|
|
175
192
|
this.getHeaderEl().classList.add(
|
|
176
193
|
'xh-grid-autosize-header--active',
|
|
177
|
-
`xh-grid-autosize-header--${sizingMode}
|
|
178
|
-
showSort ? 'xh-grid-autosize-header--sort' : null,
|
|
179
|
-
showMenu ? 'xh-grid-autosize-header--menu' : null
|
|
194
|
+
`xh-grid-autosize-header--${sizingMode}`
|
|
180
195
|
);
|
|
196
|
+
if (showSort) this.getHeaderEl().classList.add('xh-grid-autosize-header--sort');
|
|
197
|
+
if (showMenu) this.getHeaderEl().classList.add('xh-grid-autosize-header--menu');
|
|
198
|
+
if (!isEmpty(headerClass)) this.getHeaderEl().classList.add(...headerClass.split(' '));
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
getHeaderEl() {
|
|
@@ -200,6 +218,24 @@ export class ColumnWidthCalculator {
|
|
|
200
218
|
return this._headerEl;
|
|
201
219
|
}
|
|
202
220
|
|
|
221
|
+
getHeaderClass(gridModel, column) {
|
|
222
|
+
let {headerClass} = column;
|
|
223
|
+
if (isNil(headerClass)) return '';
|
|
224
|
+
|
|
225
|
+
if (isFunction(headerClass)) {
|
|
226
|
+
headerClass = headerClass({column, gridModel});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const ret = [];
|
|
230
|
+
if (isString(headerClass)) {
|
|
231
|
+
ret.push(headerClass);
|
|
232
|
+
} else if (isArray(headerClass)) {
|
|
233
|
+
ret.push(...headerClass);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return ret.join(' ');
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
//------------------
|
|
204
240
|
// Autosize cell
|
|
205
241
|
//------------------
|
package/core/AppSpec.js
CHANGED
|
@@ -40,11 +40,14 @@ export class AppSpec {
|
|
|
40
40
|
* @param {boolean} [c.trackAppLoad] - true (default) to write a track log statement after the
|
|
41
41
|
* app has loaded and fully initialized, including elapsed time of asset loading and init.
|
|
42
42
|
* @param {boolean} [c.webSocketsEnabled] - true to enable Hoist websocket connectivity,
|
|
43
|
-
* establish a connection and initiate a heartbeat
|
|
43
|
+
* establish a connection and initiate a heartbeat.
|
|
44
44
|
* @param {(Class|function)} [c.idlePanel] - optional custom Component to display when App has
|
|
45
|
-
* been suspended.
|
|
45
|
+
* been suspended. The component will receive a single prop -- onReactivate -- a callback
|
|
46
46
|
* called when the user has acknowledged the suspension and wishes to reload the app and
|
|
47
|
-
* continue working.
|
|
47
|
+
* continue working. Specify as a React Component or an element factory.
|
|
48
|
+
* @param {(Class|function)} [c.lockoutPanel] - optional custom Component to display when the
|
|
49
|
+
* user is denied access to app. Intended for apps that implement custom auth flows.
|
|
50
|
+
* See also `lockoutMessage` for a more lightweight customization option.
|
|
48
51
|
* @param {?string} [c.loginMessage] - optional message to show on login form (for non-SSO apps).
|
|
49
52
|
* @param {?string} [c.lockoutMessage] - optional message to show users when denied access to app.
|
|
50
53
|
* @param {boolean} [c.showBrowserContextMenu] - true to show the built-in browser context menu
|
|
@@ -67,6 +70,7 @@ export class AppSpec {
|
|
|
67
70
|
trackAppLoad = true,
|
|
68
71
|
webSocketsEnabled = false,
|
|
69
72
|
idlePanel = null,
|
|
73
|
+
lockoutPanel = null,
|
|
70
74
|
loginMessage = null,
|
|
71
75
|
lockoutMessage = null,
|
|
72
76
|
showBrowserContextMenu = false,
|
|
@@ -96,10 +100,11 @@ export class AppSpec {
|
|
|
96
100
|
this.isMobileApp = isMobileApp;
|
|
97
101
|
this.isSSO = isSSO;
|
|
98
102
|
this.checkAccess = checkAccess;
|
|
99
|
-
this.trackAppLoad = trackAppLoad;
|
|
100
103
|
|
|
104
|
+
this.trackAppLoad = trackAppLoad;
|
|
101
105
|
this.webSocketsEnabled = webSocketsEnabled;
|
|
102
106
|
this.idlePanel = idlePanel;
|
|
107
|
+
this.lockoutPanel = lockoutPanel;
|
|
103
108
|
this.loginMessage = loginMessage;
|
|
104
109
|
this.lockoutMessage = lockoutMessage;
|
|
105
110
|
this.showBrowserContextMenu = showBrowserContextMenu;
|
package/core/XH.js
CHANGED
|
@@ -29,7 +29,13 @@ import {
|
|
|
29
29
|
} from '@xh/hoist/svc';
|
|
30
30
|
import {Timer} from '@xh/hoist/utils/async';
|
|
31
31
|
import {MINUTES} from '@xh/hoist/utils/datetime';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
apiDeprecated,
|
|
34
|
+
checkMinVersion,
|
|
35
|
+
getClientDeviceInfo,
|
|
36
|
+
throwIf,
|
|
37
|
+
withDebug
|
|
38
|
+
} from '@xh/hoist/utils/js';
|
|
33
39
|
import {camelCase, compact, flatten, isBoolean, isString, uniqueId} from 'lodash';
|
|
34
40
|
import ReactDOM from 'react-dom';
|
|
35
41
|
import parser from 'ua-parser-js';
|
|
@@ -324,9 +330,22 @@ class XHClass extends HoistBase {
|
|
|
324
330
|
return this.acm.themeModel.toggleTheme();
|
|
325
331
|
}
|
|
326
332
|
|
|
327
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* Enable/disable the dark theme directly (useful for custom app option controls).
|
|
335
|
+
* @param {boolean} value
|
|
336
|
+
* @deprecated
|
|
337
|
+
*/
|
|
328
338
|
setDarkTheme(value) {
|
|
329
|
-
|
|
339
|
+
apiDeprecated('setDarkTheme', {v: '50', msg: 'Use setTheme instead.'});
|
|
340
|
+
this.setTheme(value ? 'dark' : 'light');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Sets the theme directly (useful for custom app option controls).
|
|
345
|
+
* @param {string} value - 'light', 'dark', or 'system'
|
|
346
|
+
*/
|
|
347
|
+
setTheme(value) {
|
|
348
|
+
return this.acm.themeModel.setTheme(value);
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
/** Is the app currently rendering in dark theme? */
|
|
@@ -974,6 +993,8 @@ window['XH'] = XH;
|
|
|
974
993
|
* @property {Object} [cancelProps] - props for secondary cancel button.
|
|
975
994
|
* Must provide either text or icon for button to be displayed, or use a preconfigured
|
|
976
995
|
* helper such as `XH.alert()` or `XH.confirm()` for default buttons.
|
|
996
|
+
* @property {string} [cancelAlign] - specify 'left' to place the Cancel button (if shown) on the
|
|
997
|
+
* left edge of the dialog toolbar, with a filler between it and Confirm.
|
|
977
998
|
* @property {function} [onConfirm] - Callback to execute when confirm is clicked.
|
|
978
999
|
* @property {function} [onCancel] - Callback to execute when cancel is clicked.
|
|
979
1000
|
*/
|
|
@@ -80,14 +80,15 @@ export class FieldFilter extends Filter {
|
|
|
80
80
|
|
|
81
81
|
if (store) {
|
|
82
82
|
const storeField = store.getField(field);
|
|
83
|
-
if (!storeField) return () => true; // Ignore if field not in store
|
|
83
|
+
if (!storeField) return () => true; // Ignore (do not filter out) if field not in store
|
|
84
84
|
|
|
85
85
|
const fieldType = storeField.type;
|
|
86
86
|
value = isArray(value) ?
|
|
87
87
|
value.map(v => parseFieldValue(v, fieldType)) :
|
|
88
88
|
parseFieldValue(value, fieldType);
|
|
89
89
|
}
|
|
90
|
-
const getVal = store ? r => r.committedData[field] : r => r[field]
|
|
90
|
+
const getVal = store ? r => r.committedData[field] : r => r[field],
|
|
91
|
+
doNotFilter = r => store && isNil(r.committedData); // Ignore (do not filter out) record if part of a store and it has no committed data
|
|
91
92
|
|
|
92
93
|
if (FieldFilter.ARRAY_OPERATORS.includes(op)) {
|
|
93
94
|
value = castArray(value);
|
|
@@ -96,48 +97,66 @@ export class FieldFilter extends Filter {
|
|
|
96
97
|
switch (op) {
|
|
97
98
|
case '=':
|
|
98
99
|
return r => {
|
|
100
|
+
if (doNotFilter(r)) return true;
|
|
99
101
|
let v = getVal(r);
|
|
100
102
|
if (isNil(v) || v === '') v = null;
|
|
101
103
|
return value.includes(v);
|
|
102
104
|
};
|
|
103
105
|
case '!=':
|
|
104
106
|
return r => {
|
|
107
|
+
if (doNotFilter(r)) return true;
|
|
105
108
|
let v = getVal(r);
|
|
106
109
|
if (isNil(v) || v === '') v = null;
|
|
107
110
|
return !value.includes(v);
|
|
108
111
|
};
|
|
109
112
|
case '>':
|
|
110
113
|
return r => {
|
|
114
|
+
if (doNotFilter(r)) return true;
|
|
111
115
|
const v = getVal(r);
|
|
112
116
|
return !isNil(v) && v > value;
|
|
113
117
|
};
|
|
114
118
|
case '>=':
|
|
115
119
|
return r => {
|
|
120
|
+
if (doNotFilter(r)) return true;
|
|
116
121
|
const v = getVal(r);
|
|
117
122
|
return !isNil(v) && v >= value;
|
|
118
123
|
};
|
|
119
124
|
case '<':
|
|
120
125
|
return r => {
|
|
126
|
+
if (doNotFilter(r)) return true;
|
|
121
127
|
const v = getVal(r);
|
|
122
128
|
return !isNil(v) && v < value;
|
|
123
129
|
};
|
|
124
130
|
case '<=':
|
|
125
131
|
return r => {
|
|
132
|
+
if (doNotFilter(r)) return true;
|
|
126
133
|
const v = getVal(r);
|
|
127
134
|
return !isNil(v) && v <= value;
|
|
128
135
|
};
|
|
129
136
|
case 'like':
|
|
130
137
|
regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
|
|
131
|
-
return r =>
|
|
138
|
+
return r => {
|
|
139
|
+
if (doNotFilter(r)) return true;
|
|
140
|
+
return regExps.some(re => re.test(getVal(r)));
|
|
141
|
+
};
|
|
132
142
|
case 'not like':
|
|
133
143
|
regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
|
|
134
|
-
return r =>
|
|
144
|
+
return r => {
|
|
145
|
+
if (doNotFilter(r)) return true;
|
|
146
|
+
regExps.every(re => !re.test(getVal(r)));
|
|
147
|
+
};
|
|
135
148
|
case 'begins':
|
|
136
149
|
regExps = value.map(v => new RegExp('^' + escapeRegExp(v), 'i'));
|
|
137
|
-
return r =>
|
|
150
|
+
return r => {
|
|
151
|
+
if (doNotFilter(r)) return true;
|
|
152
|
+
regExps.some(re => re.test(getVal(r)));
|
|
153
|
+
};
|
|
138
154
|
case 'ends':
|
|
139
155
|
regExps = value.map(v => new RegExp(escapeRegExp(v) + '$', 'i'));
|
|
140
|
-
return r =>
|
|
156
|
+
return r => {
|
|
157
|
+
if (doNotFilter(r)) return true;
|
|
158
|
+
regExps.some(re => re.test(getVal(r)));
|
|
159
|
+
};
|
|
141
160
|
default:
|
|
142
161
|
throw XH.exception(`Unknown operator: ${op}`);
|
|
143
162
|
}
|
|
@@ -96,7 +96,7 @@ function viewForState() {
|
|
|
96
96
|
case S.LOGIN_REQUIRED:
|
|
97
97
|
return loginPanel();
|
|
98
98
|
case S.ACCESS_DENIED:
|
|
99
|
-
return
|
|
99
|
+
return lockoutView();
|
|
100
100
|
case S.RUNNING:
|
|
101
101
|
return appContainerView();
|
|
102
102
|
case S.SUSPENDED:
|
|
@@ -107,6 +107,14 @@ function viewForState() {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const lockoutView = hoistCmp.factory({
|
|
111
|
+
displayName: 'LockoutView',
|
|
112
|
+
render() {
|
|
113
|
+
const content = XH.appSpec.lockoutPanel ?? lockoutPanel;
|
|
114
|
+
return elementFromContent(content);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
110
118
|
const appContainerView = hoistCmp.factory({
|
|
111
119
|
displayName: 'AppContainerView',
|
|
112
120
|
|
|
@@ -59,7 +59,7 @@ const inputCmp = hoistCmp.factory(
|
|
|
59
59
|
item: withDefault(input.item, textInput({
|
|
60
60
|
autoFocus: true,
|
|
61
61
|
selectOnFocus: true,
|
|
62
|
-
onKeyDown: evt => {if (evt.key
|
|
62
|
+
onKeyDown: evt => {if (evt.key === 'Enter') model.doConfirmAsync();}
|
|
63
63
|
}))
|
|
64
64
|
})
|
|
65
65
|
});
|
|
@@ -68,13 +68,19 @@ const inputCmp = hoistCmp.factory(
|
|
|
68
68
|
|
|
69
69
|
const bbar = hoistCmp.factory(
|
|
70
70
|
({model}) => {
|
|
71
|
-
const {confirmProps, cancelProps, formModel} = model,
|
|
72
|
-
ret = [
|
|
71
|
+
const {confirmProps, cancelProps, cancelAlign, formModel} = model,
|
|
72
|
+
ret = [];
|
|
73
73
|
|
|
74
74
|
if (cancelProps) {
|
|
75
75
|
ret.push(button(cancelProps));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
if (cancelAlign === 'left') {
|
|
79
|
+
ret.push(filler());
|
|
80
|
+
} else {
|
|
81
|
+
ret.unshift(filler());
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
if (confirmProps) {
|
|
79
85
|
// Merge in formModel.isValid here in render stage to get reactivity.
|
|
80
86
|
ret.push(formModel ?
|
|
@@ -22,14 +22,15 @@ export const themeAppOption = ({formFieldProps, inputProps} = {}) => {
|
|
|
22
22
|
label: 'Theme',
|
|
23
23
|
item: buttonGroupInput({
|
|
24
24
|
items: [
|
|
25
|
-
button({value:
|
|
26
|
-
button({value:
|
|
25
|
+
button({value: 'light', text: 'Light', icon: Icon.sun(), width: '33.33%'}),
|
|
26
|
+
button({value: 'dark', text: 'Dark', icon: Icon.moon(), width: '33.33%'}),
|
|
27
|
+
button({value: 'system', text: 'System', icon: Icon.sync(), width: '33.33%'})
|
|
27
28
|
],
|
|
28
29
|
...inputProps
|
|
29
30
|
}),
|
|
30
31
|
...formFieldProps
|
|
31
32
|
},
|
|
32
|
-
|
|
33
|
-
valueSetter: (v) => XH.
|
|
33
|
+
prefName: 'xhTheme',
|
|
34
|
+
valueSetter: (v) => XH.setTheme(v)
|
|
34
35
|
};
|
|
35
36
|
};
|
|
@@ -303,7 +303,6 @@ class Model extends HoistInputModel {
|
|
|
303
303
|
if (action === 'input-change') {
|
|
304
304
|
this.inputValue = value;
|
|
305
305
|
this.inputValueChangedSinceSelect = true;
|
|
306
|
-
if (!value) this.noteValueChange(null);
|
|
307
306
|
} else if (action === 'input-blur') {
|
|
308
307
|
this.inputValue = null;
|
|
309
308
|
this.inputValueChangedSinceSelect = false;
|
|
@@ -652,6 +651,7 @@ const cmp = hoistCmp.factory(
|
|
|
652
651
|
if (model.manageInputValue) {
|
|
653
652
|
rsProps.inputValue = model.inputValue || '';
|
|
654
653
|
rsProps.onInputChange = model.onInputChange;
|
|
654
|
+
rsProps.controlShouldRenderValue = !model.hasFocus;
|
|
655
655
|
rsProps.onMenuOpen = () => {
|
|
656
656
|
wait().then(()=> {
|
|
657
657
|
const selectedEl = document.getElementsByClassName('xh-select__option--is-selected')[0];
|
|
@@ -84,7 +84,7 @@ function viewForState() {
|
|
|
84
84
|
case S.LOGIN_REQUIRED:
|
|
85
85
|
return loginPanel();
|
|
86
86
|
case S.ACCESS_DENIED:
|
|
87
|
-
return
|
|
87
|
+
return lockoutView();
|
|
88
88
|
case S.RUNNING:
|
|
89
89
|
return appContainerView();
|
|
90
90
|
case S.SUSPENDED:
|
|
@@ -95,6 +95,14 @@ function viewForState() {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const lockoutView = hoistCmp.factory({
|
|
99
|
+
displayName: 'LockoutView',
|
|
100
|
+
render() {
|
|
101
|
+
const content = XH.appSpec.lockoutPanel ?? lockoutPanel;
|
|
102
|
+
return elementFromContent(content);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
98
106
|
const appContainerView = hoistCmp.factory({
|
|
99
107
|
displayName: 'AppContainerView',
|
|
100
108
|
|
|
@@ -25,8 +25,8 @@ export const message = hoistCmp.factory({
|
|
|
25
25
|
model: uses(MessageModel),
|
|
26
26
|
|
|
27
27
|
render({model}) {
|
|
28
|
-
const isOpen = model
|
|
29
|
-
{icon, title, message, formModel, cancelProps,
|
|
28
|
+
const isOpen = model?.isOpen,
|
|
29
|
+
{icon, title, message, formModel, confirmProps, cancelProps, cancelAlign} = model,
|
|
30
30
|
buttons = [];
|
|
31
31
|
|
|
32
32
|
if (!isOpen) return null;
|
|
@@ -35,6 +35,14 @@ export const message = hoistCmp.factory({
|
|
|
35
35
|
buttons.push(button({minimal: true, ...cancelProps}));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
if (cancelProps || confirmProps) {
|
|
39
|
+
if (cancelAlign === 'left') {
|
|
40
|
+
buttons.push(filler());
|
|
41
|
+
} else {
|
|
42
|
+
buttons.unshift(filler());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
if (confirmProps) {
|
|
39
47
|
// Merge in formModel.isValid here in render stage to get reactivity.
|
|
40
48
|
buttons.push(formModel ?
|
|
@@ -43,10 +51,6 @@ export const message = hoistCmp.factory({
|
|
|
43
51
|
);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
if (buttons.length) {
|
|
47
|
-
buttons.unshift(filler());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
54
|
return dialog({
|
|
51
55
|
isOpen,
|
|
52
56
|
icon,
|
|
@@ -22,16 +22,22 @@ export const themeAppOption = ({formFieldProps, inputProps} = {}) => {
|
|
|
22
22
|
item: buttonGroupInput({
|
|
23
23
|
items: [
|
|
24
24
|
button({
|
|
25
|
-
value:
|
|
25
|
+
value: 'light',
|
|
26
26
|
text: 'Light',
|
|
27
27
|
icon: Icon.sun(),
|
|
28
|
-
width: '
|
|
28
|
+
width: '33.33%'
|
|
29
29
|
}),
|
|
30
30
|
button({
|
|
31
|
-
value:
|
|
31
|
+
value: 'dark',
|
|
32
32
|
text: 'Dark',
|
|
33
33
|
icon: Icon.moon(),
|
|
34
|
-
width: '
|
|
34
|
+
width: '33.33%'
|
|
35
|
+
}),
|
|
36
|
+
button({
|
|
37
|
+
value: 'system',
|
|
38
|
+
text: 'System',
|
|
39
|
+
icon: Icon.sync(),
|
|
40
|
+
width: '33.33%'
|
|
35
41
|
})
|
|
36
42
|
],
|
|
37
43
|
width: '100%',
|
|
@@ -39,7 +45,7 @@ export const themeAppOption = ({formFieldProps, inputProps} = {}) => {
|
|
|
39
45
|
}),
|
|
40
46
|
...formFieldProps
|
|
41
47
|
},
|
|
42
|
-
|
|
43
|
-
valueSetter: (v) => XH.
|
|
48
|
+
prefName: 'xhTheme',
|
|
49
|
+
valueSetter: (v) => XH.setTheme(v)
|
|
44
50
|
};
|
|
45
51
|
};
|
|
@@ -209,7 +209,7 @@ const editableChild = hoistCmp.factory({
|
|
|
209
209
|
model,
|
|
210
210
|
bind: 'value',
|
|
211
211
|
disabled: props.disabled || disabled,
|
|
212
|
-
ref: composeRefs(model
|
|
212
|
+
ref: composeRefs(model?._boundInputRef, child.ref)
|
|
213
213
|
};
|
|
214
214
|
|
|
215
215
|
// If FormField is sized and item doesn't specify its own dimensions,
|
|
@@ -288,7 +288,6 @@ class Model extends HoistInputModel {
|
|
|
288
288
|
if (action === 'input-change') {
|
|
289
289
|
this.inputValue = value;
|
|
290
290
|
this.inputValueChangedSinceSelect = true;
|
|
291
|
-
if (!value) this.noteValueChange(null);
|
|
292
291
|
} else if (action === 'input-blur') {
|
|
293
292
|
this.inputValue = null;
|
|
294
293
|
this.inputValueChangedSinceSelect = false;
|
|
@@ -615,6 +614,7 @@ const cmp = hoistCmp.factory(
|
|
|
615
614
|
if (model.manageInputValue) {
|
|
616
615
|
rsProps.inputValue = model.inputValue || '';
|
|
617
616
|
rsProps.onInputChange = model.onInputChange;
|
|
617
|
+
rsProps.controlShouldRenderValue = !model.hasFocus;
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
if (model.asyncMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "47.
|
|
3
|
+
"version": "47.1.2",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@blueprintjs/core": "~3.
|
|
30
|
-
"@blueprintjs/datetime": "~3.
|
|
29
|
+
"@blueprintjs/core": "~3.54.0",
|
|
30
|
+
"@blueprintjs/datetime": "~3.24.0",
|
|
31
31
|
"@fortawesome/fontawesome-pro": "~5.15.4",
|
|
32
32
|
"@fortawesome/fontawesome-svg-core": "~1.3.0",
|
|
33
33
|
"@fortawesome/pro-light-svg-icons": "~5.15.4",
|