cx 26.4.4 → 26.6.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/build/charts/axis/Axis.d.ts +8 -0
- package/build/charts/axis/Axis.d.ts.map +1 -1
- package/build/charts/axis/Axis.js +18 -1
- package/build/charts/axis/TimeAxis.js +1 -0
- package/build/locale/de-de.js +1 -0
- package/build/locale/en-us.js +1 -0
- package/build/locale/es-es.js +1 -0
- package/build/locale/fr-fr.js +1 -0
- package/build/locale/nl-nl.js +1 -0
- package/build/locale/pt-pt.js +1 -0
- package/build/locale/sr-latn-ba.js +1 -0
- package/build/ui/Format.d.ts.map +1 -1
- package/build/ui/Format.js +26 -2
- package/build/util/Format.d.ts.map +1 -1
- package/build/util/Format.js +6 -0
- package/build/util/date/dateQuarter.d.ts +7 -0
- package/build/util/date/dateQuarter.d.ts.map +1 -0
- package/build/util/date/dateQuarter.js +8 -0
- package/build/util/date/dayBefore.d.ts +12 -0
- package/build/util/date/dayBefore.d.ts.map +1 -0
- package/build/util/date/dayBefore.js +15 -0
- package/build/util/date/index.d.ts +2 -0
- package/build/util/date/index.d.ts.map +1 -1
- package/build/util/date/index.js +2 -0
- package/build/widgets/form/DateTimePicker.d.ts.map +1 -1
- package/build/widgets/form/DateTimePicker.js +53 -31
- package/build/widgets/form/Field.d.ts.map +1 -1
- package/build/widgets/form/Field.js +2 -1
- package/build/widgets/form/LookupField.d.ts +6 -0
- package/build/widgets/form/LookupField.d.ts.map +1 -1
- package/build/widgets/form/LookupField.js +12 -0
- package/build/widgets/form/Wheel.d.ts +8 -0
- package/build/widgets/form/Wheel.d.ts.map +1 -1
- package/build/widgets/form/Wheel.js +30 -7
- package/build/widgets/grid/Grid.d.ts +6 -1
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +3 -2
- package/dist/charts.css +6 -0
- package/dist/charts.js +18 -1
- package/dist/manifest.js +880 -871
- package/dist/ui.js +33 -1
- package/dist/util.js +32 -0
- package/dist/widgets.css +4 -0
- package/dist/widgets.js +243 -175
- package/package.json +1 -1
- package/src/charts/BarGraph.scss +31 -31
- package/src/charts/Legend.scss +57 -57
- package/src/charts/LegendEntry.scss +35 -35
- package/src/charts/LineGraph.scss +28 -28
- package/src/charts/RangeMarker.scss +3 -0
- package/src/charts/axis/Axis.tsx +31 -1
- package/src/charts/axis/TimeAxis.tsx +1 -0
- package/src/charts/helpers/SnapPointFinder.ts +136 -136
- package/src/charts/helpers/ValueAtFinder.ts +72 -72
- package/src/charts/index.scss +1 -0
- package/src/data/AugmentedViewBase.ts +89 -89
- package/src/data/View.ts +301 -301
- package/src/data/createAccessorModelProxy.ts +66 -66
- package/src/locale/de-de.ts +1 -0
- package/src/locale/en-us.ts +1 -0
- package/src/locale/es-es.ts +1 -0
- package/src/locale/fr-fr.ts +1 -0
- package/src/locale/nl-nl.ts +1 -0
- package/src/locale/pt-pt.ts +1 -0
- package/src/locale/sr-latn-ba.ts +1 -0
- package/src/ui/Format.spec.ts +32 -0
- package/src/ui/Format.ts +27 -2
- package/src/ui/Repeater.spec.tsx +181 -181
- package/src/util/Format.spec.ts +11 -0
- package/src/util/Format.ts +7 -0
- package/src/util/date/dateQuarter.ts +8 -0
- package/src/util/date/dayBefore.ts +15 -0
- package/src/util/date/index.ts +2 -0
- package/src/util/scss/include.scss +69 -69
- package/src/widgets/Button.maps.scss +103 -103
- package/src/widgets/form/Calendar.tsx +772 -772
- package/src/widgets/form/DateTimePicker.tsx +453 -392
- package/src/widgets/form/Field.tsx +2 -1
- package/src/widgets/form/LookupField.spec.tsx +149 -0
- package/src/widgets/form/LookupField.tsx +27 -0
- package/src/widgets/form/ValidationGroup.spec.tsx +30 -1
- package/src/widgets/form/Wheel.tsx +36 -7
- package/src/widgets/grid/Grid.scss +663 -657
- package/src/widgets/grid/Grid.tsx +9 -3
- package/src/widgets/grid/variables.scss +47 -47
- package/src/widgets/index.ts +63 -63
- package/src/widgets/nav/MenuItem.scss +150 -150
- package/src/widgets/nav/Tab.ts +122 -122
- package/src/widgets/overlay/Overlay.tsx +1029 -1029
- package/src/widgets/variables.scss +61 -61
|
@@ -312,6 +312,7 @@ export class Field<
|
|
|
312
312
|
data._readOnly = data.readOnly;
|
|
313
313
|
data._viewMode = data.mode === "view" || data.viewMode;
|
|
314
314
|
data._tabOnEnterKey = data.tabOnEnterKey;
|
|
315
|
+
data._visited = data.visited;
|
|
315
316
|
data.validationValue = this.getValidationValue(data);
|
|
316
317
|
instance.parentDisabled = context.parentDisabled;
|
|
317
318
|
instance.parentReadOnly = context.parentReadOnly;
|
|
@@ -366,7 +367,7 @@ export class Field<
|
|
|
366
367
|
);
|
|
367
368
|
data.visited = coalesce(
|
|
368
369
|
context.parentStrict ? context.parentVisited : null,
|
|
369
|
-
data.
|
|
370
|
+
data._visited,
|
|
370
371
|
context.parentVisited,
|
|
371
372
|
);
|
|
372
373
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createAccessorModelProxy } from "../../data/createAccessorModelProxy";
|
|
2
2
|
import { LookupField } from "./LookupField";
|
|
3
|
+
import { Store } from "../../data/Store";
|
|
4
|
+
import { ValidationGroup } from "./ValidationGroup";
|
|
5
|
+
import { bind } from "../../ui/bind";
|
|
6
|
+
import { createTestRenderer } from "../../util/test/createTestRenderer";
|
|
7
|
+
import assert from "assert";
|
|
3
8
|
|
|
4
9
|
interface User {
|
|
5
10
|
id: number;
|
|
@@ -90,4 +95,148 @@ describe("LookupField", () => {
|
|
|
90
95
|
</cx>
|
|
91
96
|
);
|
|
92
97
|
});
|
|
98
|
+
|
|
99
|
+
describe("validateOptionExists", () => {
|
|
100
|
+
const options = [
|
|
101
|
+
{ id: 1, text: "One" },
|
|
102
|
+
{ id: 2, text: "Two" },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
it("reports an error when the selected value is missing from options", async () => {
|
|
106
|
+
let widget = (
|
|
107
|
+
<cx>
|
|
108
|
+
<ValidationGroup errors={bind("errors")}>
|
|
109
|
+
<LookupField
|
|
110
|
+
value={bind("value")}
|
|
111
|
+
text={bind("text")}
|
|
112
|
+
options={options}
|
|
113
|
+
validateOptionExists
|
|
114
|
+
/>
|
|
115
|
+
</ValidationGroup>
|
|
116
|
+
</cx>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let store = new Store();
|
|
120
|
+
store.set("value", 99);
|
|
121
|
+
store.set("text", "Stale");
|
|
122
|
+
|
|
123
|
+
await createTestRenderer(store, widget);
|
|
124
|
+
|
|
125
|
+
let errors = store.get("errors");
|
|
126
|
+
assert.equal(errors.length, 1);
|
|
127
|
+
assert.equal(errors[0].message, "The selected option is no longer available.");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("does not report an error when the selected value matches an option", async () => {
|
|
131
|
+
let widget = (
|
|
132
|
+
<cx>
|
|
133
|
+
<ValidationGroup errors={bind("errors")}>
|
|
134
|
+
<LookupField
|
|
135
|
+
value={bind("value")}
|
|
136
|
+
text={bind("text")}
|
|
137
|
+
options={options}
|
|
138
|
+
validateOptionExists
|
|
139
|
+
/>
|
|
140
|
+
</ValidationGroup>
|
|
141
|
+
</cx>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
let store = new Store();
|
|
145
|
+
store.set("value", 1);
|
|
146
|
+
store.set("text", "One");
|
|
147
|
+
|
|
148
|
+
await createTestRenderer(store, widget);
|
|
149
|
+
|
|
150
|
+
let errors = store.get("errors");
|
|
151
|
+
assert.equal(errors.length, 0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does not report an error when the field is empty", async () => {
|
|
155
|
+
let widget = (
|
|
156
|
+
<cx>
|
|
157
|
+
<ValidationGroup errors={bind("errors")}>
|
|
158
|
+
<LookupField
|
|
159
|
+
value={bind("value")}
|
|
160
|
+
text={bind("text")}
|
|
161
|
+
options={options}
|
|
162
|
+
validateOptionExists
|
|
163
|
+
/>
|
|
164
|
+
</ValidationGroup>
|
|
165
|
+
</cx>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
let store = new Store();
|
|
169
|
+
|
|
170
|
+
await createTestRenderer(store, widget);
|
|
171
|
+
|
|
172
|
+
let errors = store.get("errors");
|
|
173
|
+
assert.equal(errors.length, 0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not report an error when options are not provided (server-side mode)", async () => {
|
|
177
|
+
let widget = (
|
|
178
|
+
<cx>
|
|
179
|
+
<ValidationGroup errors={bind("errors")}>
|
|
180
|
+
<LookupField
|
|
181
|
+
value={bind("value")}
|
|
182
|
+
text={bind("text")}
|
|
183
|
+
onQuery={() => []}
|
|
184
|
+
validateOptionExists
|
|
185
|
+
/>
|
|
186
|
+
</ValidationGroup>
|
|
187
|
+
</cx>
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
let store = new Store();
|
|
191
|
+
store.set("value", 99);
|
|
192
|
+
store.set("text", "Stale");
|
|
193
|
+
|
|
194
|
+
await createTestRenderer(store, widget);
|
|
195
|
+
|
|
196
|
+
let errors = store.get("errors");
|
|
197
|
+
assert.equal(errors.length, 0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("reports an error in multiple mode when some ids are not in options", async () => {
|
|
201
|
+
let widget = (
|
|
202
|
+
<cx>
|
|
203
|
+
<ValidationGroup errors={bind("errors")}>
|
|
204
|
+
<LookupField
|
|
205
|
+
multiple
|
|
206
|
+
values={bind("values")}
|
|
207
|
+
options={options}
|
|
208
|
+
validateOptionExists
|
|
209
|
+
/>
|
|
210
|
+
</ValidationGroup>
|
|
211
|
+
</cx>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
let store = new Store();
|
|
215
|
+
store.set("values", [1, 99]);
|
|
216
|
+
|
|
217
|
+
await createTestRenderer(store, widget);
|
|
218
|
+
|
|
219
|
+
let errors = store.get("errors");
|
|
220
|
+
assert.equal(errors.length, 1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("does not validate by default (back-compat)", async () => {
|
|
224
|
+
let widget = (
|
|
225
|
+
<cx>
|
|
226
|
+
<ValidationGroup errors={bind("errors")}>
|
|
227
|
+
<LookupField value={bind("value")} text={bind("text")} options={options} />
|
|
228
|
+
</ValidationGroup>
|
|
229
|
+
</cx>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let store = new Store();
|
|
233
|
+
store.set("value", 99);
|
|
234
|
+
store.set("text", "Stale");
|
|
235
|
+
|
|
236
|
+
await createTestRenderer(store, widget);
|
|
237
|
+
|
|
238
|
+
let errors = store.get("errors");
|
|
239
|
+
assert.equal(errors.length, 0);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
93
242
|
});
|
|
@@ -117,6 +117,12 @@ interface LookupFieldBaseConfig<TOption = any> extends FieldConfig {
|
|
|
117
117
|
/** Error message displayed if server query throws an exception. */
|
|
118
118
|
queryErrorText?: string;
|
|
119
119
|
|
|
120
|
+
/** Set to `true` to report a validation error when the selected value is not present in `options`. Only applies when `options` is an array. Default is `false`. */
|
|
121
|
+
validateOptionExists?: boolean;
|
|
122
|
+
|
|
123
|
+
/** Error message displayed when the selected value is not present in `options`. */
|
|
124
|
+
invalidOptionText?: string;
|
|
125
|
+
|
|
120
126
|
/** Message to be displayed if no entries match the user query. */
|
|
121
127
|
noResultsText?: string;
|
|
122
128
|
|
|
@@ -292,6 +298,8 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
|
|
|
292
298
|
declare public minOptionsForSearchField: number;
|
|
293
299
|
declare public loadingText: string;
|
|
294
300
|
declare public queryErrorText: string;
|
|
301
|
+
declare public validateOptionExists: boolean;
|
|
302
|
+
declare public invalidOptionText: string;
|
|
295
303
|
declare public noResultsText: string;
|
|
296
304
|
declare public optionIdField: string;
|
|
297
305
|
declare public optionTextField: string;
|
|
@@ -501,6 +509,22 @@ export class LookupField<TOption = any, TRecord = any> extends Field<
|
|
|
501
509
|
|
|
502
510
|
(instance as DropdownInstance).lastDropdown = context.lastDropdown;
|
|
503
511
|
|
|
512
|
+
if (
|
|
513
|
+
this.validateOptionExists &&
|
|
514
|
+
isArray(data.options) &&
|
|
515
|
+
!this.isEmpty(data)
|
|
516
|
+
) {
|
|
517
|
+
let invalid = this.multiple
|
|
518
|
+
? isArray(data.values) && data.records!.length < data.values.length
|
|
519
|
+
: !data.options.some(($option) =>
|
|
520
|
+
areKeysEqual(
|
|
521
|
+
getOptionKey(this.keyBindings!, { $option }),
|
|
522
|
+
data.selectedKeys[0],
|
|
523
|
+
),
|
|
524
|
+
);
|
|
525
|
+
if (invalid) data.error = this.invalidOptionText;
|
|
526
|
+
}
|
|
527
|
+
|
|
504
528
|
super.prepareData(context, instance);
|
|
505
529
|
}
|
|
506
530
|
|
|
@@ -604,6 +628,9 @@ LookupField.prototype.minOptionsForSearchField = 7;
|
|
|
604
628
|
LookupField.prototype.loadingText = "Loading...";
|
|
605
629
|
LookupField.prototype.queryErrorText =
|
|
606
630
|
"Error occurred while querying for lookup data.";
|
|
631
|
+
LookupField.prototype.validateOptionExists = false;
|
|
632
|
+
LookupField.prototype.invalidOptionText =
|
|
633
|
+
"The selected option is no longer available.";
|
|
607
634
|
LookupField.prototype.noResultsText = "No results found.";
|
|
608
635
|
LookupField.prototype.optionIdField = "id";
|
|
609
636
|
LookupField.prototype.optionTextField = "text";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Store } from "../../data/Store";
|
|
2
2
|
import { ValidationGroup } from "./ValidationGroup";
|
|
3
3
|
import { Validator } from "./Validator";
|
|
4
|
+
import { TextField } from "./TextField";
|
|
4
5
|
import { bind } from "../../ui/bind";
|
|
5
|
-
import { createTestRenderer } from "../../util/test/createTestRenderer";
|
|
6
|
+
import { createTestRenderer, act } from "../../util/test/createTestRenderer";
|
|
6
7
|
|
|
7
8
|
import assert from "assert";
|
|
8
9
|
|
|
@@ -107,6 +108,34 @@ describe("ValidationGroup", () => {
|
|
|
107
108
|
assert(visited);
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
it("visited flag changes are propagated to fields after being initialized to false", async () => {
|
|
112
|
+
let widget = (
|
|
113
|
+
<cx>
|
|
114
|
+
<ValidationGroup errors={bind("errors")} visited={bind("form.visited")}>
|
|
115
|
+
<TextField required value={bind("value1")} />
|
|
116
|
+
</ValidationGroup>
|
|
117
|
+
</cx>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
let store = new Store();
|
|
121
|
+
//initializing the bound value to false used to shadow later updates
|
|
122
|
+
store.set("form.visited", false);
|
|
123
|
+
|
|
124
|
+
const component = await createTestRenderer(store, widget);
|
|
125
|
+
|
|
126
|
+
let errors = store.get("errors");
|
|
127
|
+
assert.equal(errors.length, 1);
|
|
128
|
+
assert.equal(errors[0].visited, false);
|
|
129
|
+
|
|
130
|
+
await act(async () => {
|
|
131
|
+
store.set("form.visited", true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
errors = store.get("errors");
|
|
135
|
+
assert.equal(errors.length, 1);
|
|
136
|
+
assert.equal(errors[0].visited, true);
|
|
137
|
+
});
|
|
138
|
+
|
|
110
139
|
it("disabled flag can be overruled by the field props", async () => {
|
|
111
140
|
let widget = (
|
|
112
141
|
<cx>
|
|
@@ -65,6 +65,10 @@ Wheel.prototype.baseClass = "wheel";
|
|
|
65
65
|
Wheel.prototype.size = 3;
|
|
66
66
|
Wheel.prototype.styled = true;
|
|
67
67
|
|
|
68
|
+
/** A cyclic wheel renders its option list this many times, leaving scroll
|
|
69
|
+
* headroom on both sides of the centre copy. */
|
|
70
|
+
export const WHEEL_BUFFER_COPIES = 3;
|
|
71
|
+
|
|
68
72
|
export interface WheelComponentProps {
|
|
69
73
|
size: number;
|
|
70
74
|
children: React.ReactNode[];
|
|
@@ -74,6 +78,11 @@ export interface WheelComponentProps {
|
|
|
74
78
|
className?: string;
|
|
75
79
|
style?: React.CSSProperties;
|
|
76
80
|
index?: number;
|
|
81
|
+
/** Set to render the option list as an endlessly scrolling wheel. The list
|
|
82
|
+
* is rendered `WHEEL_BUFFER_COPIES` times and the wheel snaps to whichever
|
|
83
|
+
* copy of the selected value is nearest its current position, so a wrap
|
|
84
|
+
* scrolls seamlessly into the adjacent identical copy. */
|
|
85
|
+
cycle?: boolean;
|
|
77
86
|
onChange: (newIndex: number) => void;
|
|
78
87
|
onPipeKeyDown?: (fn: (e: React.KeyboardEvent) => void) => void;
|
|
79
88
|
onMouseDown?: () => void;
|
|
@@ -109,23 +118,30 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
|
|
|
109
118
|
scrollRef: (el: HTMLDivElement | null) => void;
|
|
110
119
|
|
|
111
120
|
render(): React.ReactNode {
|
|
112
|
-
let { size, children, CSS, baseClass, active, className, style, onMouseDown } = this.props;
|
|
121
|
+
let { size, children, CSS, baseClass, active, className, style, onMouseDown, cycle } = this.props;
|
|
113
122
|
let optionClass = CSS.element(baseClass, "option");
|
|
114
123
|
let dummyClass = CSS.element(baseClass, "option", { dummy: true });
|
|
115
124
|
|
|
125
|
+
// A cyclic wheel repeats the option list so it can scroll past either end.
|
|
126
|
+
let options = children;
|
|
127
|
+
if (cycle) {
|
|
128
|
+
options = [];
|
|
129
|
+
for (let i = 0; i < WHEEL_BUFFER_COPIES; i++) options.push(...children);
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
let tpad = [],
|
|
117
133
|
bpad = [],
|
|
118
134
|
padSize = 0;
|
|
119
135
|
|
|
120
136
|
for (let i = 0; i < (size - 1) / 2; i++) {
|
|
121
|
-
tpad.push({ key: -1 - i, child:
|
|
122
|
-
bpad.push({ key: -100 - i, child:
|
|
137
|
+
tpad.push({ key: -1 - i, child: options[0], cls: dummyClass });
|
|
138
|
+
bpad.push({ key: -100 - i, child: options[0], cls: dummyClass });
|
|
123
139
|
padSize++;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
let displayedOptions = [
|
|
127
143
|
...tpad,
|
|
128
|
-
...
|
|
144
|
+
...options.map((c, i) => ({
|
|
129
145
|
key: i,
|
|
130
146
|
child: c,
|
|
131
147
|
cls: optionClass,
|
|
@@ -228,7 +244,19 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
|
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
UNSAFE_componentWillReceiveProps(props: WheelComponentProps): void {
|
|
231
|
-
|
|
247
|
+
let newIndex = props.index || 0;
|
|
248
|
+
// A cyclic wheel renders the option list several times, so the same value
|
|
249
|
+
// appears in several copies. Snap to the copy nearest the wheel's current
|
|
250
|
+
// position rather than the one `props.index` names — a wrap then scrolls
|
|
251
|
+
// one item into the adjacent (identical) copy with no jump, instead of
|
|
252
|
+
// animating all the way back to the centre copy.
|
|
253
|
+
if (props.cycle) {
|
|
254
|
+
let period = props.children.length;
|
|
255
|
+
let value = ((newIndex % period) + period) % period;
|
|
256
|
+
newIndex = value + period * Math.round((this.index - value) / period);
|
|
257
|
+
newIndex = Math.max(0, Math.min(period * WHEEL_BUFFER_COPIES - 1, newIndex));
|
|
258
|
+
}
|
|
259
|
+
this.index = newIndex;
|
|
232
260
|
this.scrollTo();
|
|
233
261
|
}
|
|
234
262
|
|
|
@@ -272,8 +300,9 @@ export class WheelComponent extends VDOM.Component<WheelComponentProps, WheelCom
|
|
|
272
300
|
}
|
|
273
301
|
|
|
274
302
|
select(newIndex: number): void {
|
|
275
|
-
let { children } = this.props;
|
|
276
|
-
|
|
303
|
+
let { children, cycle } = this.props;
|
|
304
|
+
let length = children.length * (cycle ? WHEEL_BUFFER_COPIES : 1);
|
|
305
|
+
newIndex = Math.max(0, Math.min(length - 1, newIndex));
|
|
277
306
|
if (this.index !== newIndex) {
|
|
278
307
|
this.index = newIndex;
|
|
279
308
|
this.props.onChange(newIndex);
|