@xh/hoist 80.0.0-SNAPSHOT.1768264663674 → 80.0.0-SNAPSHOT.1768360784265

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/admin/tabs/activity/tracking/ActivityTracking.scss +7 -7
  3. package/admin/tabs/userData/roles/details/RoleDetails.scss +6 -6
  4. package/build/types/cmp/form/FormModel.d.ts +1 -1
  5. package/build/types/cmp/form/field/BaseFieldModel.d.ts +7 -3
  6. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +3 -3
  7. package/build/types/cmp/grid/Grid.d.ts +2 -2
  8. package/build/types/data/Field.d.ts +2 -1
  9. package/build/types/data/Store.d.ts +7 -4
  10. package/build/types/data/StoreRecord.d.ts +5 -0
  11. package/build/types/data/cube/CubeField.d.ts +3 -2
  12. package/build/types/data/impl/RecordValidator.d.ts +9 -10
  13. package/build/types/data/impl/StoreValidator.d.ts +8 -7
  14. package/build/types/data/index.d.ts +2 -0
  15. package/build/types/data/validation/Rule.d.ts +5 -40
  16. package/build/types/data/validation/Types.d.ts +56 -0
  17. package/build/types/data/validation/constraints.d.ts +1 -1
  18. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  19. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  20. package/cmp/form/FormModel.ts +2 -2
  21. package/cmp/form/field/BaseFieldModel.ts +38 -18
  22. package/cmp/form/field/SubformsFieldModel.ts +5 -5
  23. package/cmp/grid/Grid.scss +31 -8
  24. package/cmp/grid/columns/Column.ts +24 -10
  25. package/cmp/input/HoistInput.scss +19 -1
  26. package/cmp/input/HoistInputModel.ts +10 -2
  27. package/data/Field.ts +2 -1
  28. package/data/Store.ts +16 -4
  29. package/data/StoreRecord.ts +11 -0
  30. package/data/cube/CubeField.ts +8 -1
  31. package/data/cube/row/BaseRow.ts +4 -2
  32. package/data/impl/RecordValidator.ts +46 -28
  33. package/data/impl/StoreValidator.ts +22 -9
  34. package/data/index.ts +2 -0
  35. package/data/validation/Rule.ts +12 -52
  36. package/data/validation/Types.ts +81 -0
  37. package/data/validation/constraints.ts +2 -1
  38. package/desktop/appcontainer/OptionsDialog.scss +1 -1
  39. package/desktop/cmp/form/FormField.scss +128 -43
  40. package/desktop/cmp/form/FormField.ts +74 -40
  41. package/desktop/cmp/input/CodeInput.scss +13 -1
  42. package/desktop/cmp/input/RadioInput.scss +16 -4
  43. package/desktop/cmp/input/SwitchInput.scss +23 -5
  44. package/desktop/cmp/input/TextArea.scss +9 -1
  45. package/desktop/cmp/rest/impl/RestForm.scss +1 -1
  46. package/desktop/cmp/viewmanager/ViewManager.scss +7 -15
  47. package/kit/blueprint/styles.scss +4 -4
  48. package/kit/onsen/styles.scss +10 -2
  49. package/mobile/cmp/form/FormField.scss +52 -19
  50. package/mobile/cmp/form/FormField.ts +30 -21
  51. package/package.json +1 -1
  52. package/styles/XH.scss +1 -0
  53. package/styles/vars.scss +70 -12
  54. package/tsconfig.tsbuildinfo +1 -1
@@ -6,11 +6,15 @@
6
6
  */
7
7
 
8
8
  import {HoistBase} from '@xh/hoist/core';
9
+ import {
10
+ StoreValidationMessagesMap,
11
+ StoreValidationResultsMap,
12
+ ValidationState
13
+ } from '@xh/hoist/data';
9
14
  import {computed, makeObservable, runInAction, observable} from '@xh/hoist/mobx';
10
15
  import {sumBy, chunk} from 'lodash';
11
16
  import {findIn} from '@xh/hoist/utils/js';
12
- import {RecordErrorMap, RecordValidator} from './RecordValidator';
13
- import {ValidationState} from '../validation/ValidationState';
17
+ import {RecordValidator} from './RecordValidator';
14
18
  import {Store} from '../Store';
15
19
  import {StoreRecordId} from '../StoreRecord';
16
20
 
@@ -41,7 +45,7 @@ export class StoreValidator extends HoistBase {
41
45
 
42
46
  /** Map of StoreRecord IDs to StoreRecord-level error maps. */
43
47
  @computed.struct
44
- get errors(): StoreErrorMap {
48
+ get errors(): StoreValidationMessagesMap {
45
49
  return this.getErrorMap();
46
50
  }
47
51
 
@@ -51,6 +55,12 @@ export class StoreValidator extends HoistBase {
51
55
  return sumBy(this.validators, 'errorCount');
52
56
  }
53
57
 
58
+ /** Map of StoreRecord IDs to StoreRecord-level ValidationResults maps. */
59
+ @computed.struct
60
+ get validationResults(): StoreValidationResultsMap {
61
+ return this.getValidationResultsMap();
62
+ }
63
+
54
64
  /** True if any records are currently recomputing their validation state. */
55
65
  @computed
56
66
  get isPending() {
@@ -76,7 +86,7 @@ export class StoreValidator extends HoistBase {
76
86
  }
77
87
 
78
88
  /**
79
- * Recompute validations for the store and return true if valid.
89
+ * Recompute ValidationResults for the store and return true if valid.
80
90
  */
81
91
  async validateAsync(): Promise<boolean> {
82
92
  await this.validateInChunksAsync(this.validators);
@@ -92,12 +102,18 @@ export class StoreValidator extends HoistBase {
92
102
  }
93
103
 
94
104
  /** @returns map of StoreRecord IDs to StoreRecord-level error maps. */
95
- getErrorMap(): StoreErrorMap {
96
- const ret = {};
105
+ getErrorMap(): StoreValidationMessagesMap {
106
+ const ret: StoreValidationMessagesMap = {};
97
107
  this._validators.forEach(v => (ret[v.id] = v.errors));
98
108
  return ret;
99
109
  }
100
110
 
111
+ getValidationResultsMap(): StoreValidationResultsMap {
112
+ const ret: StoreValidationResultsMap = {};
113
+ this._validators.forEach(v => (ret[v.id] = v.validationResults));
114
+ return ret;
115
+ }
116
+
101
117
  /**
102
118
  * @param id - ID of RecordValidator (should match record.id)
103
119
  */
@@ -155,6 +171,3 @@ export class StoreValidator extends HoistBase {
155
171
  return Array.from(this._validators.values(), fn);
156
172
  }
157
173
  }
158
-
159
- /** Map of StoreRecord IDs to StoreRecord-level error maps. */
160
- export type StoreErrorMap = Record<StoreRecordId, RecordErrorMap>;
package/data/index.ts CHANGED
@@ -19,6 +19,7 @@ export * from './filter/FunctionFilter';
19
19
  export * from './filter/Types';
20
20
  export * from './filter/Utils';
21
21
 
22
+ export * from './cube/aggregate/AggregationContext';
22
23
  export * from './cube/aggregate/Aggregator';
23
24
  export * from './cube/aggregate/AverageAggregator';
24
25
  export * from './cube/aggregate/AverageStrictAggregator';
@@ -41,3 +42,4 @@ export * from './cube/ViewRowData';
41
42
  export * from './validation/constraints';
42
43
  export * from './validation/Rule';
43
44
  export * from './validation/ValidationState';
45
+ export * from './validation/Types';
@@ -4,10 +4,8 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {Awaitable, PlainObject, Some} from '../../core';
8
- import {castArray} from 'lodash';
9
- import {StoreRecord} from '../StoreRecord';
10
- import {BaseFieldModel} from '../../cmp/form';
7
+ import {Constraint, RuleSpec, ValidationResult, ValidationSeverity, When} from '@xh/hoist/data';
8
+ import {castArray, groupBy, isEmpty} from 'lodash';
11
9
 
12
10
  /**
13
11
  * Immutable object representing a validation rule.
@@ -23,54 +21,16 @@ export class Rule {
23
21
  }
24
22
 
25
23
  /**
26
- * Function to validate a value.
24
+ * Utility to determine the maximum severity from a list of ValidationResults.
27
25
  *
28
- * @param fieldState - context w/value for the constraint's target Field.
29
- * @param allValues - current values for all fields in form, keyed by field name.
30
- * @returns String(s) or array of strings describing errors, or null or undefined if rule passes
31
- * successfully. May return a Promise of strings for async validation.
26
+ * @param validationResults - list of ValidationResults to evaluate.
27
+ * @returns The highest severity level found, or null if none.
32
28
  */
33
- export type Constraint<T = any> = (
34
- fieldState: FieldState<T>,
35
- allValues: PlainObject
36
- ) => Awaitable<Some<string>>;
37
-
38
- /**
39
- * Function to determine when to perform validation on a value.
40
- *
41
- * @param entity - the entity being evaluated. Typically a field for store validation or
42
- * a BaseFieldModel for Form validation.
43
- * @param allValues - current values for all fields in form or record, keyed by field name.
44
- * @returns true if this rule is currently active.
45
- */
46
- export type When = (entity: any, allValues: PlainObject) => boolean;
47
-
48
- export interface FieldState<T = any> {
49
- /** Current value of the field */
50
- value: T;
51
-
52
- /** Name of the field */
53
- name: string;
54
-
55
- /** Display name of the field */
56
- displayName: string;
57
-
58
- /** Record being validated, if validating Store data. */
59
- record?: StoreRecord;
60
-
61
- /** FieldModel being validated, if validating Form data. */
62
- fieldModel?: BaseFieldModel;
63
- }
64
-
65
- export interface RuleSpec {
66
- /** Function(s) to perform validation. */
67
- check: Some<Constraint>;
68
-
69
- /**
70
- * Function to determine when this rule is active.
71
- * If not specified rule is considered to be always active.
72
- */
73
- when?: When;
29
+ export function maxSeverity(validationResults: ValidationResult[]): ValidationSeverity {
30
+ if (isEmpty(validationResults)) return null;
31
+ const bySeverity = groupBy(validationResults, 'severity');
32
+ if ('error' in bySeverity) return 'error';
33
+ if ('warning' in bySeverity) return 'warning';
34
+ if ('info' in bySeverity) return 'info';
35
+ return null;
74
36
  }
75
-
76
- export type RuleLike = RuleSpec | Constraint | Rule;
@@ -0,0 +1,81 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import type {BaseFieldModel} from '@xh/hoist/cmp/form';
8
+ import type {Awaitable, PlainObject, Some} from '@xh/hoist/core';
9
+ import type {Rule, StoreRecord, StoreRecordId} from '@xh/hoist/data';
10
+
11
+ /**
12
+ * Function to validate a value.
13
+ *
14
+ * @param fieldState - context w/value for the constraint's target Field.
15
+ * @param allValues - current values for all fields in form, keyed by field name.
16
+ * @returns ValidationResult(s) or string(s) describing errors or null / undefined if rule passes.
17
+ * May return a Promise resolving to the same for async validation.
18
+ */
19
+ export type Constraint<T = any> = (
20
+ fieldState: FieldState<T>,
21
+ allValues: PlainObject
22
+ ) => Awaitable<Some<string | ValidationResult>>;
23
+
24
+ /**
25
+ * Function to determine when to perform validation on a value.
26
+ *
27
+ * @param entity - the entity being evaluated. Typically a field for store validation or
28
+ * a BaseFieldModel for Form validation.
29
+ * @param allValues - current values for all fields in form or record, keyed by field name.
30
+ * @returns true if this rule is currently active.
31
+ */
32
+ export type When = (entity: any, allValues: PlainObject) => boolean;
33
+
34
+ export interface FieldState<T = any> {
35
+ /** Current value of the field */
36
+ value: T;
37
+
38
+ /** Name of the field */
39
+ name: string;
40
+
41
+ /** Display name of the field */
42
+ displayName: string;
43
+
44
+ /** Record being validated, if validating Store data. */
45
+ record?: StoreRecord;
46
+
47
+ /** FieldModel being validated, if validating Form data. */
48
+ fieldModel?: BaseFieldModel;
49
+ }
50
+
51
+ export interface RuleSpec {
52
+ /** Function(s) to perform validation. */
53
+ check: Some<Constraint>;
54
+
55
+ /**
56
+ * Function to determine when this rule is active.
57
+ * If not specified rule is considered to be always active.
58
+ */
59
+ when?: When;
60
+ }
61
+
62
+ export type RuleLike = RuleSpec | Constraint | Rule;
63
+
64
+ export interface ValidationResult {
65
+ severity: ValidationSeverity;
66
+ message: string;
67
+ }
68
+
69
+ export type ValidationSeverity = 'error' | 'warning' | 'info';
70
+
71
+ /** Map of StoreRecord IDs to StoreRecord-level messages maps. */
72
+ export type StoreValidationMessagesMap = Record<StoreRecordId, RecordValidationMessagesMap>;
73
+
74
+ /** Map of StoreRecord IDs to StoreRecord-level ValidationResults maps. */
75
+ export type StoreValidationResultsMap = Record<StoreRecordId, RecordValidationResultsMap>;
76
+
77
+ /** Map of Field names to Field-level Validation lists. */
78
+ export type RecordValidationResultsMap = Record<string, ValidationResult[]>;
79
+
80
+ /** Map of Field names to Field-level validation message lists. */
81
+ export type RecordValidationMessagesMap = Record<string, string[]>;
@@ -4,11 +4,12 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {Constraint} from '@xh/hoist/data';
7
8
  import {LocalDate} from '@xh/hoist/utils/datetime';
8
9
  import {pluralize} from '@xh/hoist/utils/js';
9
10
  import {isArray, isEmpty, isFinite, isNil, isString, uniq} from 'lodash';
10
11
  import moment from 'moment';
11
- import {Constraint} from './Rule';
12
+
12
13
  /**
13
14
  * A set of validation functions to assist in form field validation.
14
15
  */
@@ -8,7 +8,7 @@
8
8
  .xh-options-dialog {
9
9
  width: 500px;
10
10
 
11
- .xh-form-field-label {
11
+ .xh-form-field__label {
12
12
  min-width: 120px !important;
13
13
  }
14
14
  }
@@ -8,14 +8,30 @@
8
8
  .xh-form-field {
9
9
  display: flex;
10
10
  flex-direction: column;
11
- padding: 3px;
12
- margin: 0 0 var(--xh-pad-px);
13
-
14
- .xh-form-field-label {
15
- padding: 0 0 3px;
11
+ padding: var(--xh-form-field-padding);
12
+ margin: var(--xh-form-field-margin);
13
+
14
+ &__label {
15
+ color: var(--xh-form-field-label-color);
16
+ font-size: var(--xh-form-field-label-font-size);
17
+ font-style: var(--xh-form-field-label-font-style);
18
+ font-weight: var(--xh-form-field-label-font-weight);
19
+ text-transform: var(--xh-form-field-label-text-transform);
20
+
21
+ // Borders + padding (not inline)
22
+ border-bottom: var(--xh-form-field-label-border-bottom);
23
+ margin: var(--xh-form-field-label-margin);
24
+ padding: var(--xh-form-field-label-padding);
25
+
26
+ // Borders + padding (inline)
27
+ .xh-form-field--inline & {
28
+ border-bottom: var(--xh-form-field-label-inline-border-bottom);
29
+ margin: var(--xh-form-field-label-inline-margin);
30
+ padding: var(--xh-form-field-label-inline-padding);
31
+ }
16
32
  }
17
33
 
18
- .xh-form-field-inner {
34
+ &__inner {
19
35
  // Used for unsizeable children
20
36
  &--block {
21
37
  display: block;
@@ -45,63 +61,72 @@
45
61
  flex: 1;
46
62
  }
47
63
  }
48
- }
49
64
 
50
- .xh-form-field-info,
51
- .xh-form-field-error-msg {
52
- font-size: var(--xh-font-size-small-px);
53
- line-height: calc(var(--xh-font-size-small-px) + var(--xh-pad-px));
54
- white-space: nowrap;
55
- text-overflow: ellipsis;
56
- overflow: hidden;
57
- }
65
+ &__info-msg,
66
+ &__validation-msg {
67
+ font-size: var(--xh-form-field-msg-font-size);
68
+ line-height: var(--xh-form-field-msg-line-height);
69
+ margin: var(--xh-form-field-msg-margin);
70
+ text-transform: var(--xh-form-field-msg-text-transform);
71
+ }
72
+
73
+ &__validation-msg {
74
+ &--error {
75
+ color: var(--xh-form-field-invalid-color);
76
+ }
77
+
78
+ &--info {
79
+ color: var(--xh-form-field-info-color);
80
+ }
58
81
 
59
- .xh-form-field-error-msg {
60
- color: var(--xh-red);
82
+ &--warning {
83
+ color: var(--xh-form-field-warning-color);
84
+ }
85
+ }
61
86
  }
62
87
 
63
- &.xh-form-field-inline {
88
+ &--inline {
64
89
  flex-direction: row;
65
90
  align-items: baseline;
66
91
 
67
- &.xh-form-field-json-input {
92
+ &.xh-form-field--json-input {
68
93
  align-items: start;
69
94
  }
70
95
 
71
- .xh-form-field-label {
96
+ .xh-form-field__label {
72
97
  padding: 0 var(--xh-pad-half-px) 0 0;
73
98
  }
74
99
  }
75
100
 
76
- &.xh-form-field-readonly {
77
- &.xh-form-field-json-input {
78
- .xh-form-field-inner {
79
- &--flex {
80
- border: var(--xh-border-solid);
81
- padding: var(--xh-pad-half-px);
82
- background-color: var(--xh-input-disabled-bg);
83
- font-family: var(--xh-font-family-mono);
84
- }
85
- }
86
- }
87
-
88
- .xh-form-field-error-msg {
101
+ &--readonly {
102
+ .xh-form-field__validation-msg {
89
103
  display: none;
90
104
  }
91
105
 
92
- .xh-form-field-inner {
93
- &--flex {
94
- overflow-y: auto;
95
- }
106
+ .xh-form-field__inner--flex {
107
+ overflow-y: auto;
96
108
  }
97
109
 
98
- .xh-form-field-readonly-display {
110
+ .xh-form-field__readonly-display {
99
111
  padding: 6px 0;
100
112
  white-space: pre-wrap;
101
113
  }
114
+
115
+ &.xh-form-field--json-input {
116
+ .xh-form-field__inner--flex {
117
+ background-color: var(--xh-input-disabled-bg);
118
+ border: var(--xh-border-solid);
119
+ font-family: var(--xh-font-family-mono);
120
+ padding: var(--xh-pad-half-px);
121
+ }
122
+ }
102
123
  }
103
124
 
104
- &.xh-form-field-invalid:not(.xh-form-field-readonly) {
125
+ &.xh-form-field--invalid:not(.xh-form-field--readonly) {
126
+ .xh-form-field__label {
127
+ color: var(--xh-form-field-invalid-color);
128
+ }
129
+
105
130
  .xh-check-box span {
106
131
  box-shadow: var(--xh-form-field-invalid-box-shadow) !important;
107
132
  }
@@ -119,16 +144,76 @@
119
144
  }
120
145
 
121
146
  .xh-text-input > svg {
122
- color: var(--xh-intent-danger);
147
+ color: var(--xh-form-field-invalid-color);
123
148
  }
124
149
 
125
150
  .xh-text-area.textarea {
126
151
  border: var(--xh-form-field-invalid-border) !important;
127
152
  }
128
153
  }
154
+
155
+ &.xh-form-field--warning:not(.xh-form-field--readonly) {
156
+ .xh-form-field__label {
157
+ color: var(--xh-form-field-warning-color);
158
+ }
159
+
160
+ .xh-check-box span {
161
+ box-shadow: var(--xh-form-field-warning-box-shadow) !important;
162
+ }
163
+
164
+ .xh-button-group-input button.xh-button {
165
+ box-shadow: var(--xh-form-field-warning-box-shadow);
166
+ }
167
+
168
+ .xh-slider span {
169
+ box-shadow: var(--xh-form-field-warning-box-shadow);
170
+ }
171
+
172
+ div.xh-select__control {
173
+ border: var(--xh-form-field-warning-border);
174
+ }
175
+
176
+ .xh-text-input > svg {
177
+ color: var(--xh-form-field-warning-color);
178
+ }
179
+
180
+ .xh-text-area.textarea {
181
+ border: var(--xh-form-field-warning-border) !important;
182
+ }
183
+ }
184
+
185
+ &.xh-form-field--info:not(.xh-form-field--readonly) {
186
+ .xh-form-field__label {
187
+ color: var(--xh-form-field-info-color);
188
+ }
189
+
190
+ .xh-check-box span {
191
+ box-shadow: var(--xh-form-field-info-box-shadow) !important;
192
+ }
193
+
194
+ .xh-button-group-input button.xh-button {
195
+ box-shadow: var(--xh-form-field-info-box-shadow);
196
+ }
197
+
198
+ .xh-slider span {
199
+ box-shadow: var(--xh-form-field-info-box-shadow);
200
+ }
201
+
202
+ div.xh-select__control {
203
+ border: var(--xh-form-field-info-border);
204
+ }
205
+
206
+ .xh-text-input > svg {
207
+ color: var(--xh-form-field-info-color);
208
+ }
209
+
210
+ .xh-text-area.textarea {
211
+ border: var(--xh-form-field-info-border) !important;
212
+ }
213
+ }
129
214
  }
130
215
 
131
- ul.xh-form-field-error-tooltip {
216
+ ul.xh-form-field__validation-tooltip {
132
217
  margin: 0;
133
218
  padding: 0 1em 0 2em;
134
219
  }
@@ -141,12 +226,12 @@ ul.xh-form-field-error-tooltip {
141
226
  align-items: baseline;
142
227
  overflow: visible !important;
143
228
 
144
- .xh-form-field-inner--flex {
229
+ .xh-form-field__inner--flex {
145
230
  flex-direction: row;
146
231
  align-items: center;
147
232
  }
148
233
 
149
- .xh-form-field-error-msg {
234
+ .xh-form-field__validation-msg {
150
235
  margin: 0 var(--xh-pad-px);
151
236
  }
152
237
  }