@xh/hoist 53.0.0 → 53.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 +41 -7
- package/admin/differ/DifferModel.js +4 -1
- package/cmp/grid/GridModel.js +1 -1
- package/cmp/grid/impl/ColumnWidthCalculator.js +1 -1
- package/data/Store.js +34 -1
- package/data/StoreRecord.js +6 -1
- package/data/impl/RecordValidator.js +59 -37
- package/data/impl/StoreValidator.js +57 -18
- package/desktop/appcontainer/Toast.scss +1 -0
- package/desktop/cmp/dock/DockViewModel.js +4 -1
- package/desktop/cmp/dock/impl/Dock.scss +8 -0
- package/desktop/cmp/modalsupport/ModalSupport.js +1 -0
- package/desktop/cmp/modalsupport/ModalSupport.scss +7 -0
- package/desktop/cmp/modalsupport/ModalSupportModel.js +1 -7
- package/desktop/cmp/modalsupport/ModalSupportOptions.js +10 -1
- package/package.json +1 -1
- package/utils/js/BrowserUtils.js +5 -1
- package/data/impl/RecordFieldValidator.js +0 -150
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v53.2.0 - 2022-11-15
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
* New convenience methods `Store.errors`, `Store.errorCount`, and `StoreRecord.allErrors` for getting
|
|
7
|
+
easy access to validation errors in the data package.
|
|
8
|
+
* A new flag `Store.validationIsComplex` which governs whether non-changed
|
|
9
|
+
uncommitted records need to be revalidated when any record in the store is changed. This flag
|
|
10
|
+
defaults to `false`, which should be correct for most applications. Set to `true` for stores with
|
|
11
|
+
validations that depend on other editable record values in the store (e.g. unique constraints).
|
|
12
|
+
|
|
13
|
+
### ⚙️ Technical
|
|
14
|
+
* Major performance improvements to validation of records in stores. This is a critical fix for
|
|
15
|
+
applications that do bulk insertion of hundreds of rows or greater in editable grids.
|
|
16
|
+
|
|
17
|
+
## v53.1.0 - 2022-11-03
|
|
18
|
+
|
|
19
|
+
### 🎁 New Features
|
|
20
|
+
* `PanelModel` now supports `modalSupport.defaultModal` option to allow rendering a Panel in an
|
|
21
|
+
initially modal state.
|
|
22
|
+
|
|
23
|
+
### 🐞 Bug Fixes
|
|
24
|
+
* Fixed layout issues caused by top-level DOM elements created by `ModalSupport`
|
|
25
|
+
and `ColumnWidthCalculator` (grid auto-sizing). Resolved occasional gaps between select inputs and
|
|
26
|
+
their drop-down menus.
|
|
27
|
+
* Fix desktop styling bug where buttons inside a `Toast` could be rendered with a different color
|
|
28
|
+
than the rest of the toast contents.
|
|
29
|
+
* Fix `GridModel` bug where `Store` would fail to recognize dot-separated field names as paths
|
|
30
|
+
when provided as part of a field spec in object form.
|
|
31
|
+
|
|
32
|
+
### ⚙️ Technical
|
|
33
|
+
|
|
34
|
+
* Snap info (if available) from the `navigator.connection` global within the built-in call to track
|
|
35
|
+
each application load.
|
|
36
|
+
|
|
3
37
|
## v53.0.0 - 2022-10-19
|
|
4
38
|
|
|
5
39
|
### 🎁 New Features
|
|
6
40
|
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
* The `HOIST_ADMIN_READER` role can be assigned to users in the `roles` soft-config.
|
|
41
|
+
* The Hoist Admin Console is now accessible in a read-only capacity to users assigned the
|
|
42
|
+
new `HOIST_ADMIN_READER` role.
|
|
43
|
+
* The pre-existing `HOIST_ADMIN` role inherits this new role, and is still required to take any
|
|
44
|
+
actions that modify data.
|
|
12
45
|
|
|
13
46
|
### 💥 Breaking Changes
|
|
14
|
-
|
|
15
|
-
|
|
47
|
+
|
|
48
|
+
* Requires `hoist-core >= 14.4` to support the new `HOIST_ADMIN_READER` role described above. (Core
|
|
49
|
+
upgrade _not_ required otherwise.)
|
|
16
50
|
|
|
17
51
|
## v52.0.2 - 2022-10-13
|
|
18
52
|
|
|
@@ -81,6 +81,7 @@ export class DifferModel extends HoistModel {
|
|
|
81
81
|
|
|
82
82
|
this.url = entityName + 'DiffAdmin';
|
|
83
83
|
|
|
84
|
+
const rendererIsComplex = true;
|
|
84
85
|
this.gridModel = new GridModel({
|
|
85
86
|
store: {
|
|
86
87
|
idSpec: data => {
|
|
@@ -107,17 +108,19 @@ export class DifferModel extends HoistModel {
|
|
|
107
108
|
hidden: true
|
|
108
109
|
},
|
|
109
110
|
...this.columnFields.map(it => {
|
|
110
|
-
const colDef = {renderer: this.fieldRenderer, maxWidth: 200};
|
|
111
|
+
const colDef = {renderer: this.fieldRenderer, rendererIsComplex, maxWidth: 200};
|
|
111
112
|
return isString(it) ? {field: it, ...colDef} : {...colDef, ...it};
|
|
112
113
|
}),
|
|
113
114
|
{
|
|
114
115
|
field: 'localValue',
|
|
115
116
|
flex: true,
|
|
117
|
+
rendererIsComplex,
|
|
116
118
|
renderer: this.valueRenderer
|
|
117
119
|
},
|
|
118
120
|
{
|
|
119
121
|
field: 'remoteValue',
|
|
120
122
|
flex: true,
|
|
123
|
+
rendererIsComplex,
|
|
121
124
|
renderer: this.valueRenderer
|
|
122
125
|
}
|
|
123
126
|
],
|
package/cmp/grid/GridModel.js
CHANGED
|
@@ -1445,7 +1445,7 @@ export class GridModel extends HoistModel {
|
|
|
1445
1445
|
const newFields = [];
|
|
1446
1446
|
forEach(leafColsByFieldName, (col, name) => {
|
|
1447
1447
|
if (name !== 'id' && !storeFieldNames.includes(name)) {
|
|
1448
|
-
newFields.push({
|
|
1448
|
+
newFields.push({displayName: col.displayName, ...col.fieldSpec, name});
|
|
1449
1449
|
}
|
|
1450
1450
|
});
|
|
1451
1451
|
|
|
@@ -82,7 +82,7 @@ export class ColumnWidthCalculator {
|
|
|
82
82
|
levelMaxes = await Promise.all(levelTasks);
|
|
83
83
|
return max(levelMaxes);
|
|
84
84
|
} else {
|
|
85
|
-
return this.calcLevelWidthAsync(gridModel, records, column, options);
|
|
85
|
+
return await this.calcLevelWidthAsync(gridModel, records, column, options);
|
|
86
86
|
}
|
|
87
87
|
} catch (e) {
|
|
88
88
|
console.warn(`Error calculating max data width for column "${column.colId}".`, e);
|
package/data/Store.js
CHANGED
|
@@ -13,12 +13,15 @@ import {
|
|
|
13
13
|
castArray,
|
|
14
14
|
defaultsDeep,
|
|
15
15
|
differenceBy,
|
|
16
|
+
flatMapDeep,
|
|
16
17
|
isArray,
|
|
17
18
|
isEmpty,
|
|
18
19
|
isFunction,
|
|
19
20
|
isNil,
|
|
20
21
|
isString,
|
|
21
|
-
|
|
22
|
+
values,
|
|
23
|
+
remove as lodashRemove,
|
|
24
|
+
uniq
|
|
22
25
|
} from 'lodash';
|
|
23
26
|
import {Field} from './Field';
|
|
24
27
|
import {parseFilter} from './filter/Utils';
|
|
@@ -60,6 +63,9 @@ export class Store extends HoistBase {
|
|
|
60
63
|
/** @member {boolean} */
|
|
61
64
|
freezeData;
|
|
62
65
|
|
|
66
|
+
/** @member {boolean} */
|
|
67
|
+
validationIsComplex;
|
|
68
|
+
|
|
63
69
|
/** @member {Filter} */
|
|
64
70
|
@observable.ref filter;
|
|
65
71
|
|
|
@@ -127,6 +133,8 @@ export class Store extends HoistBase {
|
|
|
127
133
|
* object creation, and raw data processing when reloading reference-identical data.
|
|
128
134
|
* Should not be used if a processRawData function that depends on external state is
|
|
129
135
|
* provided, as this function will be circumvented on subsequent reloads. Default false.
|
|
136
|
+
* @param {boolean} [c.validationIsComplex] - set to true to always validate all uncommitted
|
|
137
|
+
* records on every change to uncommitted records (add, modify, or remove). Default false.
|
|
130
138
|
* @param {Object} [c.experimental] - flags for experimental features. These features are
|
|
131
139
|
* designed for early client-access and testing, but are not yet part of the Hoist API.
|
|
132
140
|
* @param {Object[]} [c.data] - source data to load.
|
|
@@ -144,6 +152,7 @@ export class Store extends HoistBase {
|
|
|
144
152
|
freezeData = true,
|
|
145
153
|
idEncodesTreePath = false,
|
|
146
154
|
reuseRecords = false,
|
|
155
|
+
validationIsComplex = false,
|
|
147
156
|
experimental,
|
|
148
157
|
data
|
|
149
158
|
}) {
|
|
@@ -161,6 +170,7 @@ export class Store extends HoistBase {
|
|
|
161
170
|
this.freezeData = freezeData;
|
|
162
171
|
this.idEncodesTreePath = idEncodesTreePath;
|
|
163
172
|
this.reuseRecords = reuseRecords;
|
|
173
|
+
this.validationIsComplex = validationIsComplex;
|
|
164
174
|
this.lastUpdated = Date.now();
|
|
165
175
|
|
|
166
176
|
this.resetRecords();
|
|
@@ -692,6 +702,21 @@ export class Store extends HoistBase {
|
|
|
692
702
|
return this._current.maxDepth; // maxDepth should not be effected by filtering.
|
|
693
703
|
}
|
|
694
704
|
|
|
705
|
+
/** @return {StoreErrorMap} - Map of StoreRecord IDs to StoreRecord-level error maps. */
|
|
706
|
+
get errors() {
|
|
707
|
+
return this.validator.errors;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** @return {number} - count of all validation errors for the store. */
|
|
711
|
+
get errorCount() {
|
|
712
|
+
return this.validator.errorCount;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** @return {string[]} - Array of all errors for this store. */
|
|
716
|
+
get allErrors() {
|
|
717
|
+
return uniq(flatMapDeep(this.errors, values));
|
|
718
|
+
}
|
|
719
|
+
|
|
695
720
|
/**
|
|
696
721
|
* Get a record by ID, or null if no matching record found.
|
|
697
722
|
*
|
|
@@ -1007,3 +1032,11 @@ function isChildDataObject(obj) {
|
|
|
1007
1032
|
* @property {Object} rawData - data for the child records to be added. Can include a `children`
|
|
1008
1033
|
* property that will be processed into new (grand)child records.
|
|
1009
1034
|
*/
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* @typedef {Object.<string, string[]>} RecordErrorMap - map of Field names -> Field-level error lists.
|
|
1038
|
+
*/
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* @typedef {Object.<StoreRecordId, RecordErrorMap>} StoreErrorMap - map of StoreRecord IDs -> StoreRecord-level error maps.
|
|
1042
|
+
*/
|
package/data/StoreRecord.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
8
|
-
import {isNil} from 'lodash';
|
|
8
|
+
import {isNil, flatMap} from 'lodash';
|
|
9
9
|
import {ValidationState} from './validation/ValidationState';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -152,6 +152,11 @@ export class StoreRecord {
|
|
|
152
152
|
return this.validator?.errors ?? {};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/** @return {string[]} - Array of all errors for this record. */
|
|
156
|
+
get allErrors() {
|
|
157
|
+
return flatMap(this.errors);
|
|
158
|
+
}
|
|
159
|
+
|
|
155
160
|
/** @return {number} - count of all validation errors for the record. */
|
|
156
161
|
get errorCount() {
|
|
157
162
|
return this.validator?.errorCount ?? 0;
|
|
@@ -5,21 +5,25 @@
|
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {HoistBase} from '@xh/hoist/core';
|
|
9
8
|
import {ValidationState} from '@xh/hoist/data';
|
|
10
|
-
import {computed
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import {computed} from '@xh/hoist/mobx';
|
|
10
|
+
import {compact, flatten, isEmpty, mapValues, values} from 'lodash';
|
|
11
|
+
import {makeObservable, observable, runInAction} from 'mobx';
|
|
12
|
+
import {TaskObserver} from '../../core';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Computes validation state for a StoreRecord
|
|
16
16
|
* @private
|
|
17
17
|
*/
|
|
18
|
-
export class RecordValidator
|
|
18
|
+
export class RecordValidator {
|
|
19
19
|
|
|
20
20
|
/** @member {StoreRecord} */
|
|
21
21
|
record;
|
|
22
22
|
|
|
23
|
+
@observable.ref _fieldErrors = null;
|
|
24
|
+
_validationTask = TaskObserver.trackLast();
|
|
25
|
+
_validationRunId = 0;
|
|
26
|
+
|
|
23
27
|
/** @member {StoreRecordId} */
|
|
24
28
|
get id() {
|
|
25
29
|
return this.record.id;
|
|
@@ -46,34 +50,28 @@ export class RecordValidator extends HoistBase {
|
|
|
46
50
|
/** @return {RecordErrorMap} - map of field names -> field-level errors. */
|
|
47
51
|
@computed.struct
|
|
48
52
|
get errors() {
|
|
49
|
-
return this.
|
|
53
|
+
return this._fieldErrors ?? {};
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
/** @return {number} - count of all validation errors for the record. */
|
|
53
57
|
@computed
|
|
54
58
|
get errorCount() {
|
|
55
|
-
return
|
|
59
|
+
return flatten(values(this._fieldErrors)).length;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/** @return {boolean} - true if any fields are currently recomputing their validation state. */
|
|
59
63
|
@computed
|
|
60
64
|
get isPending() {
|
|
61
|
-
return
|
|
65
|
+
return this._validationTask.isPending;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
_validators = [];
|
|
65
|
-
|
|
66
68
|
/**
|
|
67
69
|
* @param {Object} c - RecordValidator configuration.
|
|
68
70
|
* @param {StoreRecord} c.record - record to validate
|
|
69
71
|
*/
|
|
70
72
|
constructor({record}) {
|
|
71
|
-
super();
|
|
72
|
-
makeObservable(this);
|
|
73
73
|
this.record = record;
|
|
74
|
-
|
|
75
|
-
const {fields} = this.record.store;
|
|
76
|
-
this._validators = fields.map(field => new RecordFieldValidator({record, field}));
|
|
74
|
+
makeObservable(this);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
/**
|
|
@@ -81,36 +79,60 @@ export class RecordValidator extends HoistBase {
|
|
|
81
79
|
* @returns {Promise<boolean>}
|
|
82
80
|
*/
|
|
83
81
|
async validateAsync() {
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
let runId = ++this._validationRunId,
|
|
83
|
+
fieldErrors = {},
|
|
84
|
+
{record} = this,
|
|
85
|
+
fieldsToValidate = record.store.fields.filter(it => !isEmpty(it.rules));
|
|
86
|
+
|
|
87
|
+
const promises = fieldsToValidate.flatMap(field => {
|
|
88
|
+
fieldErrors[field.name] = [];
|
|
89
|
+
return field.rules.map(async (rule) => {
|
|
90
|
+
const result = await this.evaluateRuleAsync(record, field, rule);
|
|
91
|
+
fieldErrors[field.name].push(result);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
await Promise.all(promises).linkTo(this._validationTask);
|
|
95
|
+
|
|
96
|
+
if (runId !== this._validationRunId) return;
|
|
97
|
+
fieldErrors = mapValues(fieldErrors, it => compact(flatten(it)));
|
|
98
|
+
|
|
99
|
+
runInAction(() => this._fieldErrors = fieldErrors);
|
|
100
|
+
|
|
86
101
|
return this.isValid;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
/** @return {ValidationState} - the current validation state for the record. */
|
|
90
105
|
getValidationState() {
|
|
91
106
|
const VS = ValidationState,
|
|
92
|
-
|
|
93
|
-
if (states.includes(VS.NotValid)) return VS.NotValid;
|
|
94
|
-
if (states.includes(VS.Unknown)) return VS.Unknown;
|
|
95
|
-
return VS.Valid;
|
|
96
|
-
}
|
|
107
|
+
{_fieldErrors} = this;
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
if (_fieldErrors === null) return VS.Unknown; // Before executing any rules
|
|
110
|
+
|
|
111
|
+
return (values(_fieldErrors).some(errors => !isEmpty(errors))) ?
|
|
112
|
+
VS.NotValid :
|
|
113
|
+
VS.Valid;
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
async evaluateRuleAsync(record, field, rule) {
|
|
117
|
+
const values = record.getValues(),
|
|
118
|
+
{name, displayName} = field,
|
|
119
|
+
value = record.get(name);
|
|
120
|
+
|
|
121
|
+
if (this.ruleIsActive(record, field, rule)) {
|
|
122
|
+
const promises = rule.check.map(async (constraint) => {
|
|
123
|
+
const fieldState = {value, name, displayName, record};
|
|
124
|
+
return await constraint(fieldState, values);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const ret = await Promise.all(promises);
|
|
128
|
+
return compact(flatten(ret));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [];
|
|
111
132
|
}
|
|
112
|
-
}
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
ruleIsActive(record, field, rule) {
|
|
135
|
+
const {when} = rule;
|
|
136
|
+
return !when || when(field, record.getValues());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {HoistBase} from '@xh/hoist/core';
|
|
9
9
|
import {computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
10
|
-
import {
|
|
10
|
+
import {sumBy, chunk} from 'lodash';
|
|
11
|
+
import {runInAction} from 'mobx';
|
|
12
|
+
import {logDebug, findIn} from '../../utils/js';
|
|
13
|
+
import {ValidationState} from '../validation/ValidationState';
|
|
11
14
|
|
|
12
15
|
import {RecordValidator} from './RecordValidator';
|
|
13
|
-
import {ValidationState} from '../validation/ValidationState';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Computes validation state for a Store's uncommitted Records
|
|
@@ -48,17 +50,21 @@ export class StoreValidator extends HoistBase {
|
|
|
48
50
|
/** @return {number} - count of all validation errors for the store. */
|
|
49
51
|
@computed
|
|
50
52
|
get errorCount() {
|
|
51
|
-
return sumBy(this.
|
|
53
|
+
return sumBy(this.validators, 'errorCount');
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** @return {boolean} - true if any records are currently recomputing their validation state. */
|
|
55
57
|
@computed
|
|
56
58
|
get isPending() {
|
|
57
|
-
return
|
|
59
|
+
return findIn(this._validators, it => it.isPending);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get validators() {
|
|
63
|
+
return this.mapValidators();
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
/** @member {RecordValidator
|
|
61
|
-
@observable.ref _validators =
|
|
66
|
+
/** @member {Map<StoreRecordId, RecordValidator>} */
|
|
67
|
+
@observable.ref _validators = new Map();
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* @param {Object} c - StoreValidator configuration.
|
|
@@ -80,15 +86,14 @@ export class StoreValidator extends HoistBase {
|
|
|
80
86
|
* @returns {Promise<boolean>}
|
|
81
87
|
*/
|
|
82
88
|
async validateAsync() {
|
|
83
|
-
|
|
84
|
-
await Promise.all(promises);
|
|
89
|
+
await this.validateInChunksAsync(this.validators);
|
|
85
90
|
return this.isValid;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
/** @return {ValidationState} - the current validation state for the store. */
|
|
89
94
|
getValidationState() {
|
|
90
95
|
const VS = ValidationState,
|
|
91
|
-
states =
|
|
96
|
+
states = this.mapValidators(v => v.validationState);
|
|
92
97
|
if (states.includes(VS.NotValid)) return VS.NotValid;
|
|
93
98
|
if (states.includes(VS.Unknown)) return VS.Unknown;
|
|
94
99
|
return VS.Valid;
|
|
@@ -106,7 +111,7 @@ export class StoreValidator extends HoistBase {
|
|
|
106
111
|
* @return {RecordValidator}
|
|
107
112
|
*/
|
|
108
113
|
findRecordValidator(id) {
|
|
109
|
-
return
|
|
114
|
+
return this._validators.get(id);
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
//---------------------------------------
|
|
@@ -117,12 +122,46 @@ export class StoreValidator extends HoistBase {
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
async syncValidatorsAsync() {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const isComplex = this.store.validationIsComplex,
|
|
126
|
+
currValidators = this._validators,
|
|
127
|
+
newValidators = new Map(),
|
|
128
|
+
toValidate = [];
|
|
129
|
+
|
|
130
|
+
this.uncommittedRecords.forEach(record => {
|
|
131
|
+
const {id} = record;
|
|
132
|
+
|
|
133
|
+
// Re-use existing validators to preserve validation state and avoid churn.
|
|
134
|
+
let validator = currValidators.get(id);
|
|
135
|
+
|
|
136
|
+
// 1) If exists validator for an unchanged record, no need to validate
|
|
137
|
+
if (!isComplex && validator?.record == record) {
|
|
138
|
+
newValidators.set(id, validator);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2) Otherwise create/update the validator, and trigger validation
|
|
143
|
+
if (!validator) {
|
|
144
|
+
validator = new RecordValidator({record});
|
|
145
|
+
} else {
|
|
146
|
+
validator.record = record;
|
|
147
|
+
}
|
|
148
|
+
newValidators.set(id, validator);
|
|
149
|
+
toValidate.push(validator);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await this.validateInChunksAsync(toValidate);
|
|
153
|
+
runInAction(() => this._validators = newValidators);
|
|
123
154
|
}
|
|
124
|
-
}
|
|
125
155
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
156
|
+
async validateInChunksAsync(validators) {
|
|
157
|
+
logDebug(`Validating ${validators.length} records`, this);
|
|
158
|
+
const validateChunks = chunk(validators, 100);
|
|
159
|
+
for (let chunk of validateChunks) {
|
|
160
|
+
await Promise.all(chunk.map(v => v.validateAsync()));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mapValidators(fn = undefined) {
|
|
165
|
+
return Array.from(this._validators.values(), fn);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -111,7 +111,10 @@ export class DockViewModel extends HoistModel {
|
|
|
111
111
|
this.refreshContextModel = new ManagedRefreshContextModel(this);
|
|
112
112
|
|
|
113
113
|
this.modalSupportModel = new ModalSupportModel({
|
|
114
|
-
width: width ?? null,
|
|
114
|
+
width: width ?? null,
|
|
115
|
+
height: height ?? null,
|
|
116
|
+
defaultModal: !docked,
|
|
117
|
+
canOutsideClickClose: false
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
margin-right: var(--xh-pad-px);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
.xh-modal-support__inline {
|
|
35
|
+
// Don't flex-shrink docked dock views, allowing them to clip the container.
|
|
36
|
+
flex: none;
|
|
37
|
+
// Workaround for this class getting `width: 100%` for 1 frame while waiting for
|
|
38
|
+
// an observable ref. See ModalSupport.js.
|
|
39
|
+
width: auto !important;
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
.xh-dock-view {
|
|
@@ -11,6 +11,7 @@ import '@xh/hoist/desktop/register';
|
|
|
11
11
|
import {dialog} from '@xh/hoist/kit/blueprint';
|
|
12
12
|
import {Children} from 'react';
|
|
13
13
|
import {createPortal} from 'react-dom';
|
|
14
|
+
import './ModalSupport.scss';
|
|
14
15
|
import {ModalSupportModel} from './ModalSupportModel';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -35,6 +35,7 @@ export class ModalSupportModel extends HoistModel {
|
|
|
35
35
|
this.hostNode = this.createHostNode();
|
|
36
36
|
|
|
37
37
|
this.options = opts instanceof ModalSupportOptions ? opts : new ModalSupportOptions(opts);
|
|
38
|
+
this.isModal = this.options.defaultModal;
|
|
38
39
|
|
|
39
40
|
const {inlineRef, modalRef, hostNode} = this;
|
|
40
41
|
this.addReaction({
|
|
@@ -54,20 +55,13 @@ export class ModalSupportModel extends HoistModel {
|
|
|
54
55
|
const hostNode = document.createElement('div');
|
|
55
56
|
hostNode.style.all = 'inherit';
|
|
56
57
|
hostNode.classList.add('xh-modal-support__host');
|
|
57
|
-
document.body.appendChild(hostNode);
|
|
58
58
|
return hostNode;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
* Toggle the current state of `isModal`
|
|
63
|
-
*/
|
|
64
61
|
toggleIsModal() {
|
|
65
62
|
this.setIsModal(!this.isModal);
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
/**
|
|
69
|
-
* Destroy the `hostNode` DOM Element
|
|
70
|
-
*/
|
|
71
65
|
destroy() {
|
|
72
66
|
this.hostNode.remove();
|
|
73
67
|
super.destroy();
|
|
@@ -13,6 +13,8 @@ export class ModalSupportOptions {
|
|
|
13
13
|
width;
|
|
14
14
|
/** @member {?String|number} */
|
|
15
15
|
height;
|
|
16
|
+
/** @member {boolean} */
|
|
17
|
+
defaultModal;
|
|
16
18
|
/** @member boolean */
|
|
17
19
|
canOutsideClickClose;
|
|
18
20
|
/**
|
|
@@ -20,11 +22,18 @@ export class ModalSupportOptions {
|
|
|
20
22
|
* @param {Object} opts
|
|
21
23
|
* @param {String|number} [width] - css width
|
|
22
24
|
* @param {String|number} [height] - css height
|
|
25
|
+
* @param {boolean} [defaultModal] - begin in modal mode?
|
|
23
26
|
* @param {boolean} [canOutsideClickClose]
|
|
24
27
|
*/
|
|
25
|
-
constructor({
|
|
28
|
+
constructor({
|
|
29
|
+
width = '90vw',
|
|
30
|
+
height = '90vh',
|
|
31
|
+
defaultModal = false,
|
|
32
|
+
canOutsideClickClose = true
|
|
33
|
+
} = {}) {
|
|
26
34
|
this.width = width;
|
|
27
35
|
this.height = height;
|
|
36
|
+
this.defaultModal = defaultModal;
|
|
28
37
|
this.canOutsideClickClose = canOutsideClickClose;
|
|
29
38
|
}
|
|
30
39
|
}
|
package/package.json
CHANGED
package/utils/js/BrowserUtils.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {pick} from 'lodash';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Extract information about the client browser window and
|
|
10
|
+
* Extract information (if available) about the client browser's window, screen, and network speed.
|
|
11
11
|
*/
|
|
12
12
|
export function getClientDeviceInfo() {
|
|
13
13
|
const data = pick(window, 'screen', 'devicePixelRatio', 'screenX', 'screenY', 'innerWidth', 'innerHeight', 'outerWidth', 'outerHeight');
|
|
@@ -18,5 +18,9 @@ export function getClientDeviceInfo() {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
if (window.navigator.connection) {
|
|
22
|
+
data.connection = pick(window.navigator.connection, 'downlink', 'effectiveType', 'rtt');
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
return data;
|
|
22
26
|
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* This file belongs to Hoist, an application development toolkit
|
|
3
|
-
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
-
*
|
|
5
|
-
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {HoistBase, managed, TaskObserver} from '@xh/hoist/core';
|
|
9
|
-
import {ValidationState} from '@xh/hoist/data';
|
|
10
|
-
import {computed, makeObservable, observable, runInAction} from '@xh/hoist/mobx';
|
|
11
|
-
import {compact, flatten, isEmpty, isNil} from 'lodash';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Computes validation state for a Field on a StoreRecord instance
|
|
15
|
-
* @private
|
|
16
|
-
*/
|
|
17
|
-
export class RecordFieldValidator extends HoistBase {
|
|
18
|
-
|
|
19
|
-
/** @member {StoreRecord} */
|
|
20
|
-
record;
|
|
21
|
-
|
|
22
|
-
/** @member {Field} */
|
|
23
|
-
field;
|
|
24
|
-
|
|
25
|
-
/** @return {string} */
|
|
26
|
-
get id() {
|
|
27
|
-
return this.field.name;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** @return {Rule[]} */
|
|
31
|
-
get rules() {
|
|
32
|
-
return this.field.rules;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** @return {boolean} - true if the field is confirmed to be Valid. */
|
|
36
|
-
@computed
|
|
37
|
-
get isValid() {
|
|
38
|
-
return this.validationState === ValidationState.Valid;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** @return {boolean} - true if the field is confirmed to be NotValid. */
|
|
42
|
-
@computed
|
|
43
|
-
get isNotValid() {
|
|
44
|
-
return this.validationState === ValidationState.NotValid;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** @return {ValidationState} - the current validation state of the field. */
|
|
48
|
-
@computed
|
|
49
|
-
get validationState() {
|
|
50
|
-
return this.getValidationState();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** @return {string[]} - all validation errors for the field. */
|
|
54
|
-
@computed.struct
|
|
55
|
-
get errors() {
|
|
56
|
-
return this.getErrorList();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @return {number} - count of all validation errors for the field. */
|
|
60
|
-
@computed
|
|
61
|
-
get errorCount() {
|
|
62
|
-
return this.errors.length;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** @return {boolean} - true if any rules are currently recomputing their validation state. */
|
|
66
|
-
get isPending() {
|
|
67
|
-
return this._validationTask.isPending;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// An array with the result of evaluating each rule. Each element will be array of strings
|
|
71
|
-
// containing any validation errors for the rule. If validation for the rule has not
|
|
72
|
-
// completed will contain null
|
|
73
|
-
@observable _errors;
|
|
74
|
-
|
|
75
|
-
@managed _validationTask = TaskObserver.trackLast();
|
|
76
|
-
_validationRunId = 0;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {Object} c - RecordFieldValidator configuration.
|
|
80
|
-
* @param {StoreRecord} c.record - record to validate
|
|
81
|
-
* @param {Field} c.field - Field to validate
|
|
82
|
-
*/
|
|
83
|
-
constructor({record, field}) {
|
|
84
|
-
super();
|
|
85
|
-
makeObservable(this);
|
|
86
|
-
this.record = record;
|
|
87
|
-
this.field = field;
|
|
88
|
-
this._errors = this.rules.map(() => null);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Recompute validations for the record field and return true if valid.
|
|
93
|
-
* @returns {Promise<boolean>}
|
|
94
|
-
*/
|
|
95
|
-
async validateAsync() {
|
|
96
|
-
await this.evaluateAsync().linkTo(this._validationTask);
|
|
97
|
-
return this.isValid;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** @return {ValidationState} - the current validation state for the record field. */
|
|
101
|
-
getValidationState() {
|
|
102
|
-
const VS = ValidationState,
|
|
103
|
-
{_errors} = this;
|
|
104
|
-
if (_errors.some(e => !isEmpty(e))) return VS.NotValid;
|
|
105
|
-
if (_errors.some(e => isNil(e))) return VS.Unknown;
|
|
106
|
-
return VS.Valid;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** @member {string[]} - all validation errors for this field. */
|
|
110
|
-
getErrorList() {
|
|
111
|
-
return compact(flatten(this._errors));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
//---------------------------------------
|
|
115
|
-
// Implementation
|
|
116
|
-
//---------------------------------------
|
|
117
|
-
async evaluateAsync() {
|
|
118
|
-
const runId = ++this._validationRunId;
|
|
119
|
-
const promises = this.rules.map(async (rule, idx) => {
|
|
120
|
-
const result = await this.evaluateRuleAsync(rule);
|
|
121
|
-
if (runId === this._validationRunId) {
|
|
122
|
-
runInAction(() => this._errors[idx] = result);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
await Promise.all(promises).linkTo(this._validationTask);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async evaluateRuleAsync(rule) {
|
|
129
|
-
const {record, field} = this;
|
|
130
|
-
if (this.ruleIsActive(rule)) {
|
|
131
|
-
const promises = rule.check.map(async (constraint) => {
|
|
132
|
-
const {name, displayName} = field,
|
|
133
|
-
value = record.get(name),
|
|
134
|
-
fieldState = {value, name, displayName, record};
|
|
135
|
-
|
|
136
|
-
return await constraint(fieldState, record.getValues());
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const ret = await Promise.all(promises);
|
|
140
|
-
return compact(flatten(ret));
|
|
141
|
-
}
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
ruleIsActive(rule) {
|
|
146
|
-
const {record, field} = this,
|
|
147
|
-
{when} = rule;
|
|
148
|
-
return !when || when(field, record.getValues());
|
|
149
|
-
}
|
|
150
|
-
}
|