@xh/hoist 80.0.0-SNAPSHOT.1768323341476 → 80.0.0-SNAPSHOT.1768415875152

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 (51) hide show
  1. package/CHANGELOG.md +27 -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/impl/RecordValidator.d.ts +9 -10
  12. package/build/types/data/impl/StoreValidator.d.ts +8 -7
  13. package/build/types/data/index.d.ts +1 -0
  14. package/build/types/data/validation/Rule.d.ts +5 -40
  15. package/build/types/data/validation/Types.d.ts +56 -0
  16. package/build/types/data/validation/constraints.d.ts +1 -1
  17. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  18. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  19. package/cmp/form/FormModel.ts +2 -2
  20. package/cmp/form/field/BaseFieldModel.ts +38 -18
  21. package/cmp/form/field/SubformsFieldModel.ts +5 -5
  22. package/cmp/grid/Grid.scss +31 -8
  23. package/cmp/grid/columns/Column.ts +24 -10
  24. package/cmp/input/HoistInput.scss +19 -1
  25. package/cmp/input/HoistInputModel.ts +10 -2
  26. package/data/Field.ts +2 -1
  27. package/data/Store.ts +16 -4
  28. package/data/StoreRecord.ts +11 -0
  29. package/data/impl/RecordValidator.ts +46 -28
  30. package/data/impl/StoreValidator.ts +22 -9
  31. package/data/index.ts +1 -0
  32. package/data/validation/Rule.ts +12 -52
  33. package/data/validation/Types.ts +81 -0
  34. package/data/validation/constraints.ts +2 -1
  35. package/desktop/appcontainer/OptionsDialog.scss +1 -1
  36. package/desktop/cmp/form/FormField.scss +136 -42
  37. package/desktop/cmp/form/FormField.ts +74 -40
  38. package/desktop/cmp/input/CodeInput.scss +13 -1
  39. package/desktop/cmp/input/RadioInput.scss +16 -4
  40. package/desktop/cmp/input/SwitchInput.scss +23 -5
  41. package/desktop/cmp/input/TextArea.scss +9 -1
  42. package/desktop/cmp/rest/impl/RestForm.scss +1 -1
  43. package/desktop/cmp/viewmanager/ViewManager.scss +7 -15
  44. package/kit/blueprint/styles.scss +4 -4
  45. package/kit/onsen/styles.scss +10 -2
  46. package/mobile/cmp/form/FormField.scss +52 -19
  47. package/mobile/cmp/form/FormField.ts +30 -21
  48. package/package.json +1 -1
  49. package/styles/XH.scss +1 -0
  50. package/styles/vars.scss +77 -12
  51. 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
@@ -42,3 +42,4 @@ export * from './cube/ViewRowData';
42
42
  export * from './validation/constraints';
43
43
  export * from './validation/Rule';
44
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,39 @@
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);
11
+ padding: var(--xh-form-field-padding);
12
+ margin: var(--xh-form-field-margin);
13
+
14
+ &__label {
15
+ // Common label attrs
16
+ color: var(--xh-form-field-label-color);
17
+ font-size: var(--xh-form-field-label-font-size);
18
+ font-style: var(--xh-form-field-label-font-style);
19
+ font-weight: var(--xh-form-field-label-font-weight);
20
+ text-transform: var(--xh-form-field-label-text-transform);
21
+
22
+ // Borders + padding - editable, not inline - the default case.
23
+ border-bottom: var(--xh-form-field-label-border-bottom);
24
+ margin: var(--xh-form-field-label-margin);
25
+ padding: var(--xh-form-field-label-padding);
26
+
27
+ // Borders + padding - readonly, not inline - commonly used to add bottom border when using
28
+ // forms as a kind of detail view, as readonly fields often lack other visual boundaries.
29
+ .xh-form-field--readonly:not(.xh-form-field--inline) & {
30
+ border-bottom: var(--xh-form-field-readonly-label-border-bottom);
31
+ margin: var(--xh-form-field-readonly-label-margin);
32
+ padding: var(--xh-form-field-readonly-label-padding);
33
+ }
13
34
 
14
- .xh-form-field-label {
15
- padding: 0 0 3px;
35
+ // Borders + padding - inline, both editable and readonly.
36
+ .xh-form-field--inline & {
37
+ border-bottom: var(--xh-form-field-inline-label-border-bottom);
38
+ margin: var(--xh-form-field-inline-label-margin);
39
+ padding: var(--xh-form-field-inline-label-padding);
40
+ }
16
41
  }
17
42
 
18
- .xh-form-field-inner {
43
+ &__inner {
19
44
  // Used for unsizeable children
20
45
  &--block {
21
46
  display: block;
@@ -45,63 +70,72 @@
45
70
  flex: 1;
46
71
  }
47
72
  }
48
- }
49
73
 
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
- }
74
+ &__info-msg,
75
+ &__validation-msg {
76
+ font-size: var(--xh-form-field-msg-font-size);
77
+ line-height: var(--xh-form-field-msg-line-height);
78
+ margin: var(--xh-form-field-msg-margin);
79
+ text-transform: var(--xh-form-field-msg-text-transform);
80
+ }
81
+
82
+ &__validation-msg {
83
+ &--error {
84
+ color: var(--xh-form-field-invalid-color);
85
+ }
86
+
87
+ &--info {
88
+ color: var(--xh-form-field-info-color);
89
+ }
58
90
 
59
- .xh-form-field-error-msg {
60
- color: var(--xh-red);
91
+ &--warning {
92
+ color: var(--xh-form-field-warning-color);
93
+ }
94
+ }
61
95
  }
62
96
 
63
- &.xh-form-field-inline {
97
+ &--inline {
64
98
  flex-direction: row;
65
99
  align-items: baseline;
66
100
 
67
- &.xh-form-field-json-input {
101
+ &.xh-form-field--json-input {
68
102
  align-items: start;
69
103
  }
70
104
 
71
- .xh-form-field-label {
105
+ .xh-form-field__label {
72
106
  padding: 0 var(--xh-pad-half-px) 0 0;
73
107
  }
74
108
  }
75
109
 
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 {
110
+ &--readonly {
111
+ .xh-form-field__validation-msg {
89
112
  display: none;
90
113
  }
91
114
 
92
- .xh-form-field-inner {
93
- &--flex {
94
- overflow-y: auto;
95
- }
115
+ .xh-form-field__inner--flex {
116
+ overflow-y: auto;
96
117
  }
97
118
 
98
- .xh-form-field-readonly-display {
119
+ .xh-form-field__readonly-display {
99
120
  padding: 6px 0;
100
121
  white-space: pre-wrap;
101
122
  }
123
+
124
+ &.xh-form-field--json-input {
125
+ .xh-form-field__inner--flex {
126
+ background-color: var(--xh-input-disabled-bg);
127
+ border: var(--xh-border-solid);
128
+ font-family: var(--xh-font-family-mono);
129
+ padding: var(--xh-pad-half-px);
130
+ }
131
+ }
102
132
  }
103
133
 
104
- &.xh-form-field-invalid:not(.xh-form-field-readonly) {
134
+ &.xh-form-field--invalid:not(.xh-form-field--readonly) {
135
+ .xh-form-field__label {
136
+ color: var(--xh-form-field-invalid-color);
137
+ }
138
+
105
139
  .xh-check-box span {
106
140
  box-shadow: var(--xh-form-field-invalid-box-shadow) !important;
107
141
  }
@@ -119,16 +153,76 @@
119
153
  }
120
154
 
121
155
  .xh-text-input > svg {
122
- color: var(--xh-intent-danger);
156
+ color: var(--xh-form-field-invalid-color);
123
157
  }
124
158
 
125
159
  .xh-text-area.textarea {
126
160
  border: var(--xh-form-field-invalid-border) !important;
127
161
  }
128
162
  }
163
+
164
+ &.xh-form-field--warning:not(.xh-form-field--readonly) {
165
+ .xh-form-field__label {
166
+ color: var(--xh-form-field-warning-color);
167
+ }
168
+
169
+ .xh-check-box span {
170
+ box-shadow: var(--xh-form-field-warning-box-shadow) !important;
171
+ }
172
+
173
+ .xh-button-group-input button.xh-button {
174
+ box-shadow: var(--xh-form-field-warning-box-shadow);
175
+ }
176
+
177
+ .xh-slider span {
178
+ box-shadow: var(--xh-form-field-warning-box-shadow);
179
+ }
180
+
181
+ div.xh-select__control {
182
+ border: var(--xh-form-field-warning-border);
183
+ }
184
+
185
+ .xh-text-input > svg {
186
+ color: var(--xh-form-field-warning-color);
187
+ }
188
+
189
+ .xh-text-area.textarea {
190
+ border: var(--xh-form-field-warning-border) !important;
191
+ }
192
+ }
193
+
194
+ &.xh-form-field--info:not(.xh-form-field--readonly) {
195
+ .xh-form-field__label {
196
+ color: var(--xh-form-field-info-color);
197
+ }
198
+
199
+ .xh-check-box span {
200
+ box-shadow: var(--xh-form-field-info-box-shadow) !important;
201
+ }
202
+
203
+ .xh-button-group-input button.xh-button {
204
+ box-shadow: var(--xh-form-field-info-box-shadow);
205
+ }
206
+
207
+ .xh-slider span {
208
+ box-shadow: var(--xh-form-field-info-box-shadow);
209
+ }
210
+
211
+ div.xh-select__control {
212
+ border: var(--xh-form-field-info-border);
213
+ }
214
+
215
+ .xh-text-input > svg {
216
+ color: var(--xh-form-field-info-color);
217
+ }
218
+
219
+ .xh-text-area.textarea {
220
+ border: var(--xh-form-field-info-border) !important;
221
+ }
222
+ }
129
223
  }
130
224
 
131
- ul.xh-form-field-error-tooltip {
225
+ ul.xh-form-field__validation-tooltip {
132
226
  margin: 0;
133
227
  padding: 0 1em 0 2em;
134
228
  }
@@ -141,12 +235,12 @@ ul.xh-form-field-error-tooltip {
141
235
  align-items: baseline;
142
236
  overflow: visible !important;
143
237
 
144
- .xh-form-field-inner--flex {
238
+ .xh-form-field__inner--flex {
145
239
  flex-direction: row;
146
240
  align-items: center;
147
241
  }
148
242
 
149
- .xh-form-field-error-msg {
243
+ .xh-form-field__validation-msg {
150
244
  margin: 0 var(--xh-pad-px);
151
245
  }
152
246
  }