@truedat/df 8.13.5 → 8.13.7
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/package.json +4 -4
- package/src/components/DynamicForm.js +3 -0
- package/src/components/DynamicFormWithTranslations.js +3 -0
- package/src/components/FieldGroupSegment.js +3 -0
- package/src/components/FieldGroupWithTranslations.js +13 -0
- package/src/components/SelectDynamicFormWithTranslations.js +7 -1
- package/src/components/SelectableDynamicForm.js +7 -1
- package/src/components/__tests__/__snapshots__/SelectableDynamicForm.spec.js.snap +18 -1
- package/src/components/__tests__/originalContentFlow.spec.js +131 -0
- package/src/components/widgets/DateField.js +18 -9
- package/src/components/widgets/DateTimeField.js +26 -9
- package/src/components/widgets/DynamicField.js +54 -1
- package/src/components/widgets/DynamicTableField.js +41 -7
- package/src/components/widgets/FieldByWidget.js +36 -6
- package/src/components/widgets/__tests__/dateBoundsByWidget.spec.js +133 -0
- package/src/components/widgets/__tests__/dateBoundsEndToEnd.spec.js +83 -0
- package/src/components/widgets/__tests__/datePickerBoundaries.spec.js +124 -0
- package/src/components/widgets/__tests__/dateValueValidation.spec.js +215 -0
- package/src/components/widgets/__tests__/existingDateRestrictions.spec.js +354 -0
- package/src/templates/components/templateForm/DateRestrictionsForm.js +217 -0
- package/src/templates/components/templateForm/DefaultValue.js +79 -11
- package/src/templates/components/templateForm/FieldDefinition.js +15 -1
- package/src/templates/components/templateForm/TemplateForm.js +5 -0
- package/src/templates/components/templateForm/ValuesField.js +7 -0
- package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +48 -0
- package/src/templates/components/templateForm/__tests__/TemplateForm.spec.js +107 -0
- package/src/templates/components/templateForm/__tests__/dateRestrictionsForm.spec.js +429 -0
- package/src/templates/components/templateForm/__tests__/dateRestrictionsValidation.spec.js +166 -0
- package/src/templates/components/templateForm/__tests__/defaultValueBounds.spec.js +191 -0
- package/src/templates/components/templateForm/__tests__/defaultValueRestrictions.spec.js +175 -0
- package/src/templates/components/templateForm/contentValidation.js +184 -0
- package/src/templates/utils/__tests__/dateRestrictions.spec.js +578 -0
- package/src/templates/utils/__tests__/originalTableRows.spec.js +129 -0
- package/src/templates/utils/__tests__/validateContentDateRestrictions.spec.js +519 -0
- package/src/templates/utils/applyTemplate.js +27 -20
- package/src/templates/utils/dateRestrictions.js +497 -0
- package/src/templates/utils/dateValueUnchanged.js +12 -0
- package/src/templates/utils/originalTableRows.js +41 -0
- package/src/templates/utils/validateContent.js +75 -4
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { render, waitForLoad } from "@truedat/test/render";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { DynamicField } from "../DynamicField";
|
|
4
|
+
import DynamicTableField from "../DynamicTableField";
|
|
5
|
+
import { applyTemplate } from "../../../templates/utils/applyTemplate";
|
|
6
|
+
import { validateContent } from "../../../templates/utils/validateContent";
|
|
7
|
+
|
|
8
|
+
const restrictions = {
|
|
9
|
+
between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
|
|
10
|
+
resolved: { min: "2026-01-01", max: "2026-12-31" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const dateField = (type, value) => ({
|
|
14
|
+
name: "date",
|
|
15
|
+
label: "Date",
|
|
16
|
+
type,
|
|
17
|
+
widget: type,
|
|
18
|
+
restrictions,
|
|
19
|
+
value: { value, origin: "user" },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("DynamicField existing date restrictions", () => {
|
|
23
|
+
it("ignores origin-only differences in wrapped multiple date values", () => {
|
|
24
|
+
const { container } = render(
|
|
25
|
+
<DynamicField
|
|
26
|
+
field={dateField("date", [{ value: "2025-01-01", origin: "user" }])}
|
|
27
|
+
originalField={{ value: [{ value: "2025-01-01", origin: "default" }] }}
|
|
28
|
+
onChange={jest.fn()}
|
|
29
|
+
/>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(container.querySelector(".error")).toBeNull();
|
|
33
|
+
expect(container.querySelector("input")).not.toHaveAttribute("min");
|
|
34
|
+
});
|
|
35
|
+
it.each([
|
|
36
|
+
["date", "2025-01-01"],
|
|
37
|
+
["datetime", "2025-01-01T12:00"],
|
|
38
|
+
])("does not mark an unchanged existing %s as an error", (type, value) => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<DynamicField
|
|
41
|
+
field={dateField(type, value)}
|
|
42
|
+
originalField={{ value, origin: "default" }}
|
|
43
|
+
onChange={jest.fn()}
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(container.querySelector(".error")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
["date", "2025-01-01"],
|
|
52
|
+
["datetime", "2025-01-01T12:00"],
|
|
53
|
+
])("removes native bounds for an unchanged existing %s", (type, value) => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<DynamicField
|
|
56
|
+
field={dateField(type, value)}
|
|
57
|
+
originalField={{ value, origin: "default" }}
|
|
58
|
+
onChange={jest.fn()}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(container.querySelector("input")).not.toHaveAttribute("min");
|
|
63
|
+
expect(container.querySelector("input")).not.toHaveAttribute("max");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each([undefined, null, { value: "2024-01-01", origin: "user" }])(
|
|
67
|
+
"marks a new or changed out-of-range value as an error (baseline %j)",
|
|
68
|
+
(originalField) => {
|
|
69
|
+
const { getByTestId } = render(
|
|
70
|
+
<DynamicField
|
|
71
|
+
field={dateField("date", "2025-01-01")}
|
|
72
|
+
originalField={originalField}
|
|
73
|
+
onChange={jest.fn()}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(getByTestId("form-field")).toHaveClass("error");
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
it.each([undefined, { value: "2024-01-01", origin: "user" }])(
|
|
82
|
+
"retains native bounds for a new or changed value (baseline %j)",
|
|
83
|
+
(originalField) => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<DynamicField
|
|
86
|
+
field={dateField("date", "2025-01-01")}
|
|
87
|
+
originalField={originalField}
|
|
88
|
+
onChange={jest.fn()}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(container.querySelector("input")).toHaveAttribute(
|
|
93
|
+
"min",
|
|
94
|
+
"2026-01-01",
|
|
95
|
+
);
|
|
96
|
+
expect(container.querySelector("input")).toHaveAttribute(
|
|
97
|
+
"max",
|
|
98
|
+
"2026-12-31",
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it("restores bounds when the existing date changes", () => {
|
|
104
|
+
const originalField = { value: "2025-01-01", origin: "user" };
|
|
105
|
+
const { container, rerender } = render(
|
|
106
|
+
<DynamicField
|
|
107
|
+
field={dateField("date", "2025-01-01")}
|
|
108
|
+
originalField={originalField}
|
|
109
|
+
onChange={jest.fn()}
|
|
110
|
+
/>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
rerender(
|
|
114
|
+
<DynamicField
|
|
115
|
+
field={dateField("date", "2025-02-01")}
|
|
116
|
+
originalField={originalField}
|
|
117
|
+
onChange={jest.fn()}
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(container.querySelector("input")).toHaveAttribute(
|
|
122
|
+
"min",
|
|
123
|
+
"2026-01-01",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not suppress required errors on an unchanged empty field", () => {
|
|
128
|
+
const { getByText } = render(
|
|
129
|
+
<DynamicField
|
|
130
|
+
field={{ ...dateField("date", null), required: true }}
|
|
131
|
+
originalField={{ value: null, origin: "user" }}
|
|
132
|
+
onChange={jest.fn()}
|
|
133
|
+
/>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(
|
|
137
|
+
getByText("template.form.validation.empty_required"),
|
|
138
|
+
).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const tableField = (rows, columns = [dateField("date")]) => ({
|
|
143
|
+
name: "table",
|
|
144
|
+
type: "table",
|
|
145
|
+
widget: "dynamic_table",
|
|
146
|
+
value: rows,
|
|
147
|
+
values: { table_columns: columns },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("DynamicTableField existing date restrictions", () => {
|
|
151
|
+
const columns = [
|
|
152
|
+
dateField("date"),
|
|
153
|
+
{ name: "sibling", type: "string", widget: "string" },
|
|
154
|
+
];
|
|
155
|
+
const schema = {
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
name: "group",
|
|
159
|
+
fields: [{ ...tableField([], columns), type: "dynamic_table" }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
const row = (value) => ({
|
|
164
|
+
date: { value, origin: "user" },
|
|
165
|
+
sibling: { value: "old", origin: "user" },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const mountTable = async (originalRows) => {
|
|
169
|
+
const onChange = jest.fn();
|
|
170
|
+
const originalField = { value: originalRows, origin: "user" };
|
|
171
|
+
const view = (rows) => (
|
|
172
|
+
<DynamicTableField
|
|
173
|
+
field={tableField(rows, columns)}
|
|
174
|
+
originalField={originalField}
|
|
175
|
+
onChange={onChange}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
const initial = applyTemplate(schema)({ table: originalField }).table.value;
|
|
179
|
+
const rendered = render(view(initial));
|
|
180
|
+
await waitForLoad(rendered);
|
|
181
|
+
const update = () => {
|
|
182
|
+
const rows = onChange.mock.calls.at(-1)[1].value;
|
|
183
|
+
const normalized = applyTemplate(schema)({
|
|
184
|
+
table: { value: rows, origin: "user" },
|
|
185
|
+
});
|
|
186
|
+
rendered.rerender(view(normalized.table?.value || []));
|
|
187
|
+
return normalized;
|
|
188
|
+
};
|
|
189
|
+
return { ...rendered, update, originalField };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
it("keeps the survivor exempt after removing the first row and editing a sibling", async () => {
|
|
193
|
+
const rendered = await mountTable([row("2025-01-01"), row("2024-01-01")]);
|
|
194
|
+
fireEvent.click(rendered.container.querySelector("tbody .selectable"));
|
|
195
|
+
rendered.update();
|
|
196
|
+
fireEvent.change(
|
|
197
|
+
rendered.container.querySelector('input[name="sibling"]'),
|
|
198
|
+
{ target: { value: "edited" } },
|
|
199
|
+
);
|
|
200
|
+
const normalized = rendered.update();
|
|
201
|
+
|
|
202
|
+
expect(rendered.container.querySelector("td.error")).toBeNull();
|
|
203
|
+
expect(
|
|
204
|
+
rendered.container.querySelector('input[type="date"]'),
|
|
205
|
+
).not.toHaveAttribute("min");
|
|
206
|
+
expect(
|
|
207
|
+
validateContent(schema, { table: rendered.originalField })(normalized),
|
|
208
|
+
).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it.each([false, true])(
|
|
212
|
+
"rejects an added row matching a legacy date (delete first: %s)",
|
|
213
|
+
async (removeFirst) => {
|
|
214
|
+
const rendered = await mountTable([row("2025-01-01")]);
|
|
215
|
+
if (removeFirst) {
|
|
216
|
+
fireEvent.click(rendered.container.querySelector("tbody .selectable"));
|
|
217
|
+
rendered.update();
|
|
218
|
+
}
|
|
219
|
+
fireEvent.click(rendered.getByTestId("add-button"));
|
|
220
|
+
rendered.update();
|
|
221
|
+
const inputs = rendered.container.querySelectorAll('input[type="date"]');
|
|
222
|
+
fireEvent.change(inputs[inputs.length - 1], {
|
|
223
|
+
target: { value: "2025-01-01" },
|
|
224
|
+
});
|
|
225
|
+
const normalized = rendered.update();
|
|
226
|
+
|
|
227
|
+
expect(rendered.container.querySelector("td.error")).not.toBeNull();
|
|
228
|
+
expect(
|
|
229
|
+
validateContent(schema, { table: rendered.originalField })(normalized),
|
|
230
|
+
).toEqual([
|
|
231
|
+
{
|
|
232
|
+
error: "date out of range",
|
|
233
|
+
field: `table.${removeFirst ? 0 : 1}.date`,
|
|
234
|
+
values: restrictions.resolved,
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
it("does not mark unchanged existing cells as errors", async () => {
|
|
241
|
+
const value = [{ date: { value: "2025-01-01", origin: "user" } }];
|
|
242
|
+
const rendered = render(
|
|
243
|
+
<DynamicTableField
|
|
244
|
+
field={tableField(value)}
|
|
245
|
+
originalField={{ value, origin: "user" }}
|
|
246
|
+
onChange={jest.fn()}
|
|
247
|
+
/>,
|
|
248
|
+
);
|
|
249
|
+
await waitForLoad(rendered);
|
|
250
|
+
|
|
251
|
+
expect(rendered.container.querySelector(".error")).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("bounds changed cells independently of unchanged neighboring cells", async () => {
|
|
255
|
+
const originalRows = [
|
|
256
|
+
{ date: { value: "2025-01-01", origin: "user" } },
|
|
257
|
+
{ date: { value: "2025-02-01", origin: "user" } },
|
|
258
|
+
];
|
|
259
|
+
const rendered = render(
|
|
260
|
+
<DynamicTableField
|
|
261
|
+
field={tableField([
|
|
262
|
+
originalRows[0],
|
|
263
|
+
{ date: { value: "2025-03-01", origin: "user" } },
|
|
264
|
+
{ date: { value: "2025-04-01", origin: "user" } },
|
|
265
|
+
])}
|
|
266
|
+
originalField={{ value: originalRows, origin: "user" }}
|
|
267
|
+
onChange={jest.fn()}
|
|
268
|
+
/>,
|
|
269
|
+
);
|
|
270
|
+
await waitForLoad(rendered);
|
|
271
|
+
const inputs = rendered.container.querySelectorAll("input");
|
|
272
|
+
|
|
273
|
+
expect(inputs[0]).not.toHaveAttribute("min");
|
|
274
|
+
expect(inputs[1]).toHaveAttribute("min", "2026-01-01");
|
|
275
|
+
expect(inputs[2]).toHaveAttribute("min", "2026-01-01");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("retains cell errors when a table has no original content", async () => {
|
|
279
|
+
const rendered = render(
|
|
280
|
+
<DynamicTableField
|
|
281
|
+
field={tableField([{ date: { value: "2025-01-01", origin: "user" } }])}
|
|
282
|
+
onChange={jest.fn()}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
await waitForLoad(rendered);
|
|
286
|
+
|
|
287
|
+
expect(rendered.container.querySelector("td.error")).not.toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("passes original cell baselines recursively into nested tables", async () => {
|
|
291
|
+
const nestedRows = [{ date: { value: "2025-01-01", origin: "user" } }];
|
|
292
|
+
const rows = [{ table: { value: nestedRows, origin: "user" } }];
|
|
293
|
+
const rendered = render(
|
|
294
|
+
<DynamicTableField
|
|
295
|
+
field={tableField(rows, [tableField([])])}
|
|
296
|
+
originalField={{ value: rows, origin: "user" }}
|
|
297
|
+
onChange={jest.fn()}
|
|
298
|
+
/>,
|
|
299
|
+
);
|
|
300
|
+
await waitForLoad(rendered);
|
|
301
|
+
|
|
302
|
+
expect(rendered.container.querySelector("input")).not.toHaveAttribute(
|
|
303
|
+
"min",
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("preserves nested survivor baselines when a nested first row is removed", async () => {
|
|
308
|
+
const originalRows = [
|
|
309
|
+
{
|
|
310
|
+
table: {
|
|
311
|
+
value: [row("2025-01-01"), row("2024-01-01")],
|
|
312
|
+
origin: "user",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
const onChange = jest.fn();
|
|
317
|
+
const nestedColumn = { ...tableField([], columns), type: "dynamic_table" };
|
|
318
|
+
const view = (rows) => (
|
|
319
|
+
<DynamicTableField
|
|
320
|
+
field={tableField(rows, [nestedColumn])}
|
|
321
|
+
originalField={{ value: originalRows }}
|
|
322
|
+
onChange={onChange}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
const rendered = render(view(originalRows));
|
|
326
|
+
await waitForLoad(rendered);
|
|
327
|
+
const nestedTable = rendered.container
|
|
328
|
+
.querySelector('input[type="date"]')
|
|
329
|
+
.closest("table");
|
|
330
|
+
fireEvent.click(nestedTable.querySelector("tbody .selectable"));
|
|
331
|
+
rendered.rerender(view(onChange.mock.calls.at(-1)[1].value));
|
|
332
|
+
|
|
333
|
+
expect(rendered.container.querySelector("td.error")).toBeNull();
|
|
334
|
+
expect(
|
|
335
|
+
rendered.container.querySelector('input[type="date"]'),
|
|
336
|
+
).not.toHaveAttribute("min");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("uses unresolved static restrictions for table cell errors too", async () => {
|
|
340
|
+
const column = {
|
|
341
|
+
...dateField("date"),
|
|
342
|
+
restrictions: { between: restrictions.between },
|
|
343
|
+
};
|
|
344
|
+
const rendered = render(
|
|
345
|
+
<DynamicTableField
|
|
346
|
+
field={tableField([row("2025-01-01")], [column])}
|
|
347
|
+
onChange={jest.fn()}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
await waitForLoad(rendered);
|
|
351
|
+
|
|
352
|
+
expect(rendered.container.querySelector("td.error")).not.toBeNull();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
import { Form } from "semantic-ui-react";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
|
|
5
|
+
const TYPES = [
|
|
6
|
+
{ id: "template.field.restrictions.none", value: "none" },
|
|
7
|
+
{ id: "template.field.restrictions.max_date", value: "max_date" },
|
|
8
|
+
{ id: "template.field.restrictions.min_date", value: "min_date" },
|
|
9
|
+
{ id: "template.field.restrictions.between", value: "between" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const MODES = [
|
|
13
|
+
{ id: "template.field.restrictions.mode.static", value: "static" },
|
|
14
|
+
{ id: "template.field.restrictions.mode.dynamic", value: "dynamic" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const UNITS = [
|
|
18
|
+
{ id: "template.field.restrictions.units.days", value: "days" },
|
|
19
|
+
{ id: "template.field.restrictions.units.weeks", value: "weeks" },
|
|
20
|
+
{ id: "template.field.restrictions.units.months", value: "months" },
|
|
21
|
+
{ id: "template.field.restrictions.units.years", value: "years" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const detectType = (restrictions = {}) => {
|
|
25
|
+
if (restrictions.between || restrictions.between_start !== undefined)
|
|
26
|
+
return "between";
|
|
27
|
+
if (restrictions.max_date) return "max_date";
|
|
28
|
+
if (restrictions.min_date) return "min_date";
|
|
29
|
+
return "none";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const detectMode = (restrictions = {}, type) => {
|
|
33
|
+
const side =
|
|
34
|
+
type === "between"
|
|
35
|
+
? restrictions.between
|
|
36
|
+
: type === "none"
|
|
37
|
+
? undefined
|
|
38
|
+
: restrictions[type];
|
|
39
|
+
|
|
40
|
+
return side?.mode === "dynamic" ? "dynamic" : "static";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const emptyFor = (type, mode) => {
|
|
44
|
+
const side = () =>
|
|
45
|
+
mode === "static"
|
|
46
|
+
? { mode: "static", value: "" }
|
|
47
|
+
: { mode: "dynamic", amount: "", unit: "days" };
|
|
48
|
+
|
|
49
|
+
switch (type) {
|
|
50
|
+
case "max_date":
|
|
51
|
+
case "min_date":
|
|
52
|
+
return { [type]: side() };
|
|
53
|
+
case "between":
|
|
54
|
+
return mode === "static"
|
|
55
|
+
? { between: { mode: "static", start: "", end: "" } }
|
|
56
|
+
: {
|
|
57
|
+
between: {
|
|
58
|
+
mode: "dynamic",
|
|
59
|
+
from: { amount: "", unit: "days" },
|
|
60
|
+
to: { amount: "", unit: "days" },
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
default:
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const options = (formatMessage) => (items) =>
|
|
69
|
+
items.map(({ id, value }) => ({
|
|
70
|
+
key: value,
|
|
71
|
+
text: formatMessage({ id }),
|
|
72
|
+
value,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
export const DateRestrictionsForm = ({
|
|
76
|
+
field,
|
|
77
|
+
errors = {},
|
|
78
|
+
fieldNamePrefix,
|
|
79
|
+
onChange,
|
|
80
|
+
}) => {
|
|
81
|
+
const { formatMessage } = useIntl();
|
|
82
|
+
|
|
83
|
+
if (field?.type !== "date" && field?.type !== "datetime") return null;
|
|
84
|
+
|
|
85
|
+
const restrictions = field.restrictions || {};
|
|
86
|
+
const type = detectType(restrictions);
|
|
87
|
+
const mode = detectMode(restrictions, type);
|
|
88
|
+
const isDatetime = field.type === "datetime";
|
|
89
|
+
|
|
90
|
+
const emit = (path, value) =>
|
|
91
|
+
onChange(null, { name: `${fieldNamePrefix}.restrictions.${path}`, value });
|
|
92
|
+
|
|
93
|
+
const handleType = (_, { value }) =>
|
|
94
|
+
onChange(null, {
|
|
95
|
+
name: `${fieldNamePrefix}.restrictions`,
|
|
96
|
+
value: emptyFor(value, "static"),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const handleMode = (_, { value }) =>
|
|
100
|
+
onChange(null, {
|
|
101
|
+
name: `${fieldNamePrefix}.restrictions`,
|
|
102
|
+
value: emptyFor(type, value),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const typeSelect = (
|
|
106
|
+
<Form.Select
|
|
107
|
+
label={formatMessage({ id: "template.field.restrictions.label" })}
|
|
108
|
+
options={options(formatMessage)(TYPES)}
|
|
109
|
+
value={type}
|
|
110
|
+
onChange={handleType}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const modeSelect = (
|
|
115
|
+
<Form.Select
|
|
116
|
+
label={formatMessage({ id: "template.field.restrictions.mode" })}
|
|
117
|
+
options={options(formatMessage)(MODES)}
|
|
118
|
+
value={mode}
|
|
119
|
+
onChange={handleMode}
|
|
120
|
+
error={errors.mode && { content: formatMessage({ id: errors.mode }) }}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const valueInput = (path, value, labelId) => (
|
|
125
|
+
<Form.Input
|
|
126
|
+
key={path}
|
|
127
|
+
label={formatMessage({ id: labelId })}
|
|
128
|
+
name={`${fieldNamePrefix}.restrictions.${path}`}
|
|
129
|
+
value={value || ""}
|
|
130
|
+
onChange={handleFieldChange}
|
|
131
|
+
type={isDatetime ? "datetime-local" : "date"}
|
|
132
|
+
error={errors[path] && { content: formatMessage({ id: errors[path] }) }}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const handleFieldChange = (e, { name, value }) => {
|
|
137
|
+
e?.preventDefault?.();
|
|
138
|
+
emit(name.split("restrictions.").pop(), value);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const amountInput = (path, amount) => (
|
|
142
|
+
<Form.Input
|
|
143
|
+
key={path}
|
|
144
|
+
type="number"
|
|
145
|
+
step="1"
|
|
146
|
+
label={formatMessage({ id: "template.field.restrictions.amount" })}
|
|
147
|
+
name={`${fieldNamePrefix}.restrictions.${path}`}
|
|
148
|
+
value={amount ?? ""}
|
|
149
|
+
onChange={handleFieldChange}
|
|
150
|
+
error={errors[path] && { content: formatMessage({ id: errors[path] }) }}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const unitSelect = (path, unit) => (
|
|
155
|
+
<Form.Select
|
|
156
|
+
key={path}
|
|
157
|
+
label={formatMessage({ id: "template.field.restrictions.unit" })}
|
|
158
|
+
name={`${fieldNamePrefix}.restrictions.${path}`}
|
|
159
|
+
options={options(formatMessage)(UNITS)}
|
|
160
|
+
value={unit || ""}
|
|
161
|
+
onChange={handleFieldChange}
|
|
162
|
+
error={errors[path] && { content: formatMessage({ id: errors[path] }) }}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const fromLabel = "template.field.restrictions.between.from";
|
|
167
|
+
const toLabel = "template.field.restrictions.between.to";
|
|
168
|
+
const valueLabel = "template.field.restrictions.value";
|
|
169
|
+
|
|
170
|
+
const side = restrictions[type] || {};
|
|
171
|
+
const range = restrictions.between || {};
|
|
172
|
+
|
|
173
|
+
const bounds =
|
|
174
|
+
type === "between"
|
|
175
|
+
? mode === "static"
|
|
176
|
+
? [
|
|
177
|
+
valueInput("between.start", range.start, fromLabel),
|
|
178
|
+
valueInput("between.end", range.end, toLabel),
|
|
179
|
+
]
|
|
180
|
+
: [
|
|
181
|
+
amountInput("between.from.amount", range.from?.amount),
|
|
182
|
+
unitSelect("between.from.unit", range.from?.unit),
|
|
183
|
+
amountInput("between.to.amount", range.to?.amount),
|
|
184
|
+
unitSelect("between.to.unit", range.to?.unit),
|
|
185
|
+
]
|
|
186
|
+
: mode === "static"
|
|
187
|
+
? [valueInput(`${type}.value`, side.value, valueLabel)]
|
|
188
|
+
: [
|
|
189
|
+
amountInput(`${type}.amount`, side.amount),
|
|
190
|
+
unitSelect(`${type}.unit`, side.unit),
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (type === "none") {
|
|
194
|
+
return (
|
|
195
|
+
<Form.Group size="small" widths="equal">
|
|
196
|
+
{typeSelect}
|
|
197
|
+
</Form.Group>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Form.Group size="small" widths="equal">
|
|
203
|
+
{typeSelect}
|
|
204
|
+
{modeSelect}
|
|
205
|
+
{bounds}
|
|
206
|
+
</Form.Group>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
DateRestrictionsForm.propTypes = {
|
|
211
|
+
field: PropTypes.object,
|
|
212
|
+
errors: PropTypes.object,
|
|
213
|
+
fieldNamePrefix: PropTypes.string.isRequired,
|
|
214
|
+
onChange: PropTypes.func.isRequired,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export default DateRestrictionsForm;
|
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import PropTypes from "prop-types";
|
|
3
|
-
import { Form } from "semantic-ui-react";
|
|
3
|
+
import { Form, Label } from "semantic-ui-react";
|
|
4
4
|
import NumberField from "../../../components/widgets/NumberField";
|
|
5
5
|
import DateTimeField from "../../../components/widgets/DateTimeField";
|
|
6
6
|
import DateField from "../../../components/widgets/DateField";
|
|
7
|
+
import {
|
|
8
|
+
resolveRestrictions,
|
|
9
|
+
checkValuesAgainstBounds,
|
|
10
|
+
} from "../../utils/dateRestrictions";
|
|
11
|
+
|
|
12
|
+
const DateDefaultField = ({
|
|
13
|
+
Widget,
|
|
14
|
+
field,
|
|
15
|
+
name,
|
|
16
|
+
outOfRange,
|
|
17
|
+
onChange,
|
|
18
|
+
formatMessage,
|
|
19
|
+
}) => {
|
|
20
|
+
const { min, max } = resolveRestrictions(field.restrictions, field.type);
|
|
21
|
+
const defaultValue = _.get("default.value")(field);
|
|
22
|
+
const belowMin = checkValuesAgainstBounds(defaultValue, { min, max });
|
|
23
|
+
const invalidDate = belowMin === "invalid_date";
|
|
24
|
+
const tooEarly = belowMin === "too_early";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Form.Field error={outOfRange}>
|
|
28
|
+
<Widget
|
|
29
|
+
field={{ ...field, value: _.get("default.value")(field) }}
|
|
30
|
+
label={formatMessage({ id: "template.field.values.default_value" })}
|
|
31
|
+
min={min}
|
|
32
|
+
max={max}
|
|
33
|
+
onChange={(e, { value }) => onChange(e, { name: name, value: value })}
|
|
34
|
+
/>
|
|
35
|
+
{outOfRange && !invalidDate && (
|
|
36
|
+
<Label basic color="red" pointing="above">
|
|
37
|
+
{formatMessage(
|
|
38
|
+
{
|
|
39
|
+
id: tooEarly
|
|
40
|
+
? "template.field.values.default_after_min"
|
|
41
|
+
: "template.field.values.default_before_max",
|
|
42
|
+
},
|
|
43
|
+
{ limit: tooEarly ? min : max },
|
|
44
|
+
)}
|
|
45
|
+
</Label>
|
|
46
|
+
)}
|
|
47
|
+
</Form.Field>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
DateDefaultField.propTypes = {
|
|
52
|
+
Widget: PropTypes.elementType.isRequired,
|
|
53
|
+
|
|
54
|
+
field: PropTypes.object,
|
|
55
|
+
name: PropTypes.string,
|
|
56
|
+
outOfRange: PropTypes.bool,
|
|
57
|
+
onChange: PropTypes.func.isRequired,
|
|
58
|
+
formatMessage: PropTypes.func.isRequired,
|
|
59
|
+
};
|
|
7
60
|
|
|
8
61
|
const defaultValueFormat = (d) => (_.isArray(d) ? _.first(d) : d);
|
|
9
62
|
const multipleDefaultValueFormat = (d) => {
|
|
@@ -57,22 +110,37 @@ export const DefaultValue = ({
|
|
|
57
110
|
multipleDefalultsAvaliableWidget.includes(field.widget) &&
|
|
58
111
|
!multipleDefalultsDisableType.includes(type);
|
|
59
112
|
|
|
113
|
+
const dateFieldOutOfRange = (() => {
|
|
114
|
+
if (fieldType !== "date" && fieldType !== "datetime") return false;
|
|
115
|
+
if (_.isEmpty(field.restrictions)) return false;
|
|
116
|
+
|
|
117
|
+
const { min, max } = resolveRestrictions(field.restrictions, fieldType);
|
|
118
|
+
return (
|
|
119
|
+
checkValuesAgainstBounds(_.get("default.value")(field), { min, max }) !==
|
|
120
|
+
"ok"
|
|
121
|
+
);
|
|
122
|
+
})();
|
|
123
|
+
|
|
60
124
|
return (
|
|
61
125
|
<>
|
|
62
126
|
{!type && fieldType === "date" && (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
127
|
+
<DateDefaultField
|
|
128
|
+
Widget={DateField}
|
|
129
|
+
field={field}
|
|
130
|
+
name={name}
|
|
131
|
+
outOfRange={dateFieldOutOfRange}
|
|
132
|
+
onChange={onDefaultValueChange}
|
|
133
|
+
formatMessage={formatMessage}
|
|
68
134
|
/>
|
|
69
135
|
)}
|
|
70
136
|
{!type && fieldType === "datetime" && (
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
137
|
+
<DateDefaultField
|
|
138
|
+
Widget={DateTimeField}
|
|
139
|
+
field={field}
|
|
140
|
+
name={name}
|
|
141
|
+
outOfRange={dateFieldOutOfRange}
|
|
142
|
+
onChange={onDefaultValueChange}
|
|
143
|
+
formatMessage={formatMessage}
|
|
76
144
|
/>
|
|
77
145
|
)}
|
|
78
146
|
{!type && (fieldType === "integer" || fieldType === "float") && (
|