@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.
Files changed (39) hide show
  1. package/package.json +4 -4
  2. package/src/components/DynamicForm.js +3 -0
  3. package/src/components/DynamicFormWithTranslations.js +3 -0
  4. package/src/components/FieldGroupSegment.js +3 -0
  5. package/src/components/FieldGroupWithTranslations.js +13 -0
  6. package/src/components/SelectDynamicFormWithTranslations.js +7 -1
  7. package/src/components/SelectableDynamicForm.js +7 -1
  8. package/src/components/__tests__/__snapshots__/SelectableDynamicForm.spec.js.snap +18 -1
  9. package/src/components/__tests__/originalContentFlow.spec.js +131 -0
  10. package/src/components/widgets/DateField.js +18 -9
  11. package/src/components/widgets/DateTimeField.js +26 -9
  12. package/src/components/widgets/DynamicField.js +54 -1
  13. package/src/components/widgets/DynamicTableField.js +41 -7
  14. package/src/components/widgets/FieldByWidget.js +36 -6
  15. package/src/components/widgets/__tests__/dateBoundsByWidget.spec.js +133 -0
  16. package/src/components/widgets/__tests__/dateBoundsEndToEnd.spec.js +83 -0
  17. package/src/components/widgets/__tests__/datePickerBoundaries.spec.js +124 -0
  18. package/src/components/widgets/__tests__/dateValueValidation.spec.js +215 -0
  19. package/src/components/widgets/__tests__/existingDateRestrictions.spec.js +354 -0
  20. package/src/templates/components/templateForm/DateRestrictionsForm.js +217 -0
  21. package/src/templates/components/templateForm/DefaultValue.js +79 -11
  22. package/src/templates/components/templateForm/FieldDefinition.js +15 -1
  23. package/src/templates/components/templateForm/TemplateForm.js +5 -0
  24. package/src/templates/components/templateForm/ValuesField.js +7 -0
  25. package/src/templates/components/templateForm/__tests__/FieldDefinition.spec.js +48 -0
  26. package/src/templates/components/templateForm/__tests__/TemplateForm.spec.js +107 -0
  27. package/src/templates/components/templateForm/__tests__/dateRestrictionsForm.spec.js +429 -0
  28. package/src/templates/components/templateForm/__tests__/dateRestrictionsValidation.spec.js +166 -0
  29. package/src/templates/components/templateForm/__tests__/defaultValueBounds.spec.js +191 -0
  30. package/src/templates/components/templateForm/__tests__/defaultValueRestrictions.spec.js +175 -0
  31. package/src/templates/components/templateForm/contentValidation.js +184 -0
  32. package/src/templates/utils/__tests__/dateRestrictions.spec.js +578 -0
  33. package/src/templates/utils/__tests__/originalTableRows.spec.js +129 -0
  34. package/src/templates/utils/__tests__/validateContentDateRestrictions.spec.js +519 -0
  35. package/src/templates/utils/applyTemplate.js +27 -20
  36. package/src/templates/utils/dateRestrictions.js +497 -0
  37. package/src/templates/utils/dateValueUnchanged.js +12 -0
  38. package/src/templates/utils/originalTableRows.js +41 -0
  39. package/src/templates/utils/validateContent.js +75 -4
@@ -0,0 +1,166 @@
1
+ import {
2
+ parseContentValidation,
3
+ validateDateRestrictions,
4
+ } from "../contentValidation";
5
+
6
+ const dateField = (restrictions, type = "date") => ({
7
+ name: "fecha",
8
+ label: "Fecha",
9
+ type,
10
+ widget: type,
11
+ cardinality: "?",
12
+ restrictions,
13
+ });
14
+
15
+ const parseField = (field) =>
16
+ parseContentValidation([{ name: "group", fields: [field] }])[0].fields[0];
17
+
18
+ describe("validateDateRestrictions", () => {
19
+ it.each([
20
+ [
21
+ "an empty static minimum",
22
+ { min_date: { mode: "static", value: "" } },
23
+ "min_date.value",
24
+ "required",
25
+ ],
26
+ [
27
+ "an impossible static maximum",
28
+ { max_date: { mode: "static", value: "2026-02-30" } },
29
+ "max_date.value",
30
+ "invalid_date",
31
+ ],
32
+ [
33
+ "an empty static range start",
34
+ { between: { mode: "static", start: "", end: "2026-12-31" } },
35
+ "between.start",
36
+ "required",
37
+ ],
38
+ [
39
+ "an empty static range end",
40
+ { between: { mode: "static", start: "2026-01-01", end: "" } },
41
+ "between.end",
42
+ "required",
43
+ ],
44
+ [
45
+ "a reversed static range",
46
+ { between: { mode: "static", start: "2026-12-31", end: "2026-01-01" } },
47
+ "between.end",
48
+ "invalid_order",
49
+ ],
50
+ [
51
+ "an incomplete relative minimum",
52
+ { min_date: { mode: "dynamic", amount: "", unit: "days" } },
53
+ "min_date.amount",
54
+ "invalid_amount",
55
+ ],
56
+ [
57
+ "a fractional relative amount",
58
+ { max_date: { mode: "dynamic", amount: "1.5", unit: "days" } },
59
+ "max_date.amount",
60
+ "invalid_amount",
61
+ ],
62
+ [
63
+ "an unsupported relative unit",
64
+ { max_date: { mode: "dynamic", amount: 1, unit: "hours" } },
65
+ "max_date.unit",
66
+ "invalid_unit",
67
+ ],
68
+ [
69
+ "a relative range with its start after its end",
70
+ {
71
+ between: {
72
+ mode: "dynamic",
73
+ from: { amount: 5, unit: "days" },
74
+ to: { amount: -2, unit: "days" },
75
+ },
76
+ },
77
+ "between.to.amount",
78
+ "invalid_order",
79
+ ],
80
+ ])(
81
+ "reports %s at the corresponding field",
82
+ (_name, restrictions, path, error) => {
83
+ expect(validateDateRestrictions(dateField(restrictions))[path]).toBe(
84
+ `template.field.restrictions.validation.${error}`,
85
+ );
86
+ },
87
+ );
88
+
89
+ it("accepts a valid static datetime range", () => {
90
+ expect(
91
+ validateDateRestrictions(
92
+ dateField(
93
+ {
94
+ between: {
95
+ mode: "static",
96
+ start: "2026-10-25T01:30",
97
+ end: "2026-10-25T02:30",
98
+ },
99
+ },
100
+ "datetime",
101
+ ),
102
+ ),
103
+ ).toEqual({});
104
+ });
105
+
106
+ it("rejects an invalid time and an invalid timezone offset", () => {
107
+ expect(
108
+ validateDateRestrictions(
109
+ dateField(
110
+ {
111
+ max_date: { mode: "static", value: "2026-10-25T25:30" },
112
+ },
113
+ "datetime",
114
+ ),
115
+ )["max_date.value"],
116
+ ).toBe("template.field.restrictions.validation.invalid_date");
117
+ expect(
118
+ validateDateRestrictions(
119
+ dateField(
120
+ {
121
+ max_date: { mode: "static", value: "2026-10-25T02:30+25:00" },
122
+ },
123
+ "datetime",
124
+ ),
125
+ )["max_date.value"],
126
+ ).toBe("template.field.restrictions.validation.invalid_date");
127
+ });
128
+
129
+ it("does not report errors when no restriction is selected", () => {
130
+ expect(validateDateRestrictions(dateField({}))).toEqual({});
131
+ });
132
+
133
+ it("marks the field and its group invalid when a restriction is incomplete", () => {
134
+ const validated = parseField(
135
+ dateField({ between: { mode: "static", start: "", end: "2026-12-31" } }),
136
+ );
137
+
138
+ expect(validated.errors).toEqual(
139
+ expect.objectContaining({
140
+ hasErrors: true,
141
+ restrictions: {
142
+ "between.start": "template.field.restrictions.validation.required",
143
+ },
144
+ }),
145
+ );
146
+ });
147
+
148
+ it("reports errors for dynamic table date columns", () => {
149
+ const validated = parseField({
150
+ ...dateField(undefined),
151
+ type: "dynamic_table",
152
+ values: {
153
+ table_columns: [
154
+ dateField({
155
+ min_date: { mode: "dynamic", amount: "", unit: "days" },
156
+ }),
157
+ ],
158
+ },
159
+ });
160
+
161
+ expect(validated.errors.hasErrors).toBe(true);
162
+ expect(
163
+ validated.values.table_columns[0].errors.restrictions["min_date.amount"],
164
+ ).toBe("template.field.restrictions.validation.invalid_amount");
165
+ });
166
+ });
@@ -0,0 +1,191 @@
1
+ import { screen } from "@testing-library/react";
2
+ import { renderWithIntl as render } from "@truedat/test/renderWithIntl";
3
+ import { DefaultValue } from "../DefaultValue";
4
+
5
+ const formatMessage = jest.fn(({ id }) => id);
6
+
7
+ beforeEach(() => formatMessage.mockClear());
8
+
9
+ const field = (restrictions, defaultValue, type = "date") => ({
10
+ name: "fecha",
11
+ label: "Fecha",
12
+ type,
13
+ widget: type,
14
+ cardinality: "?",
15
+ values: null,
16
+ restrictions,
17
+ default: { value: defaultValue, origin: "default" },
18
+ });
19
+
20
+ const mount = (f) =>
21
+ render(
22
+ <DefaultValue
23
+ defaultField="content[0].fields[0].default"
24
+ field={f}
25
+ fieldType={f.type}
26
+ formatMessage={formatMessage}
27
+ onChange={jest.fn()}
28
+ />,
29
+ );
30
+
31
+ const range = {
32
+ between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
33
+ };
34
+
35
+ describe("a default inside its own restriction", () => {
36
+ it("is not marked as an error", () => {
37
+ const { container } = mount(field(range, "2026-06-01"));
38
+
39
+ expect(container.querySelector(".field.error")).toBeNull();
40
+ expect(
41
+ screen.queryByText(/default_after_min|default_before_max/),
42
+ ).toBeNull();
43
+ });
44
+
45
+ it("still bounds the calendar to the restriction", () => {
46
+ mount(field(range, "2026-06-01"));
47
+
48
+ const input = screen.getByPlaceholderText("Date");
49
+ expect(input).toHaveAttribute("min", "2026-01-01");
50
+ expect(input).toHaveAttribute("max", "2026-12-31");
51
+ });
52
+ });
53
+
54
+ describe("a default past a maximum", () => {
55
+ it("is marked as an error", () => {
56
+ const { container } = mount(field(range, "2027-01-01"));
57
+
58
+ expect(container.querySelector(".field.error")).not.toBeNull();
59
+ });
60
+
61
+ it("names the bound it broke, and the bound is the maximum", () => {
62
+ mount(field(range, "2027-01-01"));
63
+
64
+ expect(
65
+ screen.getByText("template.field.values.default_before_max"),
66
+ ).toBeInTheDocument();
67
+ expect(screen.queryByText(/default_after_min/)).toBeNull();
68
+ });
69
+
70
+ it("bounds the calendar so the bad date cannot be re-picked", () => {
71
+ mount(field(range, "2027-01-01"));
72
+
73
+ const input = screen.getByPlaceholderText("Date");
74
+ expect(input).toHaveAttribute("max", "2026-12-31");
75
+ });
76
+ });
77
+
78
+ describe("a default before a minimum", () => {
79
+ it("names the bound it broke, and the bound is the minimum", () => {
80
+ mount(field(range, "2025-12-31"));
81
+
82
+ expect(
83
+ screen.getByText("template.field.values.default_after_min"),
84
+ ).toBeInTheDocument();
85
+ expect(screen.queryByText(/default_before_max/)).toBeNull();
86
+ });
87
+ });
88
+
89
+ describe("a default that becomes invalid because the restriction changed", () => {
90
+ it("is caught as soon as the new restriction is applied", () => {
91
+ mount(
92
+ field(
93
+ {
94
+ between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
95
+ resolved: { min: null, max: "2027-12-31" },
96
+ },
97
+ "2027-01-01",
98
+ ),
99
+ );
100
+
101
+ expect(
102
+ screen.getByText("template.field.values.default_before_max"),
103
+ ).toBeInTheDocument();
104
+ expect(screen.queryByText(/default_after_min/)).toBeNull();
105
+ });
106
+ });
107
+
108
+ describe("a default with no restriction", () => {
109
+ it("is not bounded and not marked", () => {
110
+ const { container } = mount(field(undefined, "2027-01-01"));
111
+
112
+ const input = screen.getByPlaceholderText("Date");
113
+ expect(input).not.toHaveAttribute("min");
114
+ expect(input).not.toHaveAttribute("max");
115
+ expect(container.querySelector(".field.error")).toBeNull();
116
+ });
117
+ });
118
+
119
+ describe("a datetime default", () => {
120
+ const dtRange = {
121
+ max_date: { mode: "static", value: "2026-06-30T10:00" },
122
+ };
123
+
124
+ it("is bounded at minute precision", () => {
125
+ mount(field(dtRange, "2026-06-15T10:30", "datetime"));
126
+
127
+ const input = screen.getByPlaceholderText("Date Time");
128
+ expect(input).toHaveAttribute("max", "2026-06-30T10:00");
129
+ });
130
+
131
+ it("one minute past the bound is marked", () => {
132
+ const { container } = mount(field(dtRange, "2026-06-30T10:01", "datetime"));
133
+
134
+ expect(container.querySelector(".field.error")).not.toBeNull();
135
+ expect(
136
+ screen.getByText("template.field.values.default_before_max"),
137
+ ).toBeInTheDocument();
138
+ expect(screen.queryByText(/default_after_min/)).toBeNull();
139
+ });
140
+ });
141
+
142
+ describe("changing the bound keeps reporting the right side", () => {
143
+ it("a value above the maximum is not reported as below the minimum", () => {
144
+ mount(
145
+ field(
146
+ {
147
+ min_date: { mode: "static", value: "2026-01-01" },
148
+ max_date: { mode: "static", value: "2026-06-30" },
149
+ },
150
+ "2027-01-01",
151
+ ),
152
+ );
153
+
154
+ expect(
155
+ screen.getByText("template.field.values.default_before_max"),
156
+ ).toBeInTheDocument();
157
+ expect(screen.queryByText(/default_after_min/)).toBeNull();
158
+ });
159
+ });
160
+
161
+ describe("bound interpolation parameters", () => {
162
+ it.each([
163
+ ["2027-01-01", "default_before_max", "2026-12-31"],
164
+ ["2025-12-31", "default_after_min", "2026-01-01"],
165
+ ])(
166
+ "passes the breached limit for %s to the formatter",
167
+ (value, key, limit) => {
168
+ mount(field(range, value));
169
+
170
+ expect(formatMessage).toHaveBeenCalledWith(
171
+ { id: `template.field.values.${key}` },
172
+ { limit },
173
+ );
174
+ },
175
+ );
176
+
177
+ it("passes a datetime limit without losing its time", () => {
178
+ mount(
179
+ field(
180
+ { max_date: { mode: "static", value: "2026-06-30T10:00" } },
181
+ "2026-06-30T10:01",
182
+ "datetime",
183
+ ),
184
+ );
185
+
186
+ expect(formatMessage).toHaveBeenCalledWith(
187
+ { id: "template.field.values.default_before_max" },
188
+ { limit: "2026-06-30T10:00" },
189
+ );
190
+ });
191
+ });
@@ -0,0 +1,175 @@
1
+ import { defaultValueOutOfRange } from "../contentValidation";
2
+
3
+ const dateField = (restrictions, defaultValue) => ({
4
+ name: "fecha",
5
+ label: "Fecha",
6
+ type: "date",
7
+ widget: "date",
8
+ restrictions,
9
+ default: { value: defaultValue, origin: "default" },
10
+ });
11
+
12
+ describe("a date field default must satisfy its own restriction", () => {
13
+ const range = {
14
+ between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
15
+ };
16
+
17
+ describe("no violation", () => {
18
+ it("accepts a default inside the range", () => {
19
+ expect(defaultValueOutOfRange(dateField(range, "2026-06-01"))).toBe(
20
+ false,
21
+ );
22
+ });
23
+
24
+ it("accepts a default exactly on either bound", () => {
25
+ expect(defaultValueOutOfRange(dateField(range, "2026-01-01"))).toBe(
26
+ false,
27
+ );
28
+ expect(defaultValueOutOfRange(dateField(range, "2026-12-31"))).toBe(
29
+ false,
30
+ );
31
+ });
32
+
33
+ it("accepts an absent default", () => {
34
+ expect(defaultValueOutOfRange(dateField(range, ""))).toBe(false);
35
+ expect(defaultValueOutOfRange(dateField(range, null))).toBe(false);
36
+ expect(
37
+ defaultValueOutOfRange({ ...dateField(range, ""), default: null }),
38
+ ).toBe(false);
39
+ });
40
+
41
+ it("accepts a field with no restrictions at all", () => {
42
+ const field = dateField(undefined, "1999-01-01");
43
+ expect(defaultValueOutOfRange(field)).toBe(false);
44
+ expect(defaultValueOutOfRange(dateField({}, "1999-01-01"))).toBe(false);
45
+ });
46
+
47
+ it("does not apply to fields that are not dates", () => {
48
+ const field = {
49
+ name: "s",
50
+ label: "S",
51
+ type: "string",
52
+ restrictions: range,
53
+ default: { value: "1999-01-01", origin: "default" },
54
+ };
55
+ expect(defaultValueOutOfRange(field)).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe("violation", () => {
60
+ it("flags a default before the range", () => {
61
+ expect(defaultValueOutOfRange(dateField(range, "2025-12-31"))).toBe(true);
62
+ });
63
+
64
+ it("flags a default after the range", () => {
65
+ expect(defaultValueOutOfRange(dateField(range, "2027-01-01"))).toBe(true);
66
+ });
67
+
68
+ it("flags against a single-sided max_date", () => {
69
+ const maxOnly = { max_date: { mode: "static", value: "2026-12-31" } };
70
+ expect(defaultValueOutOfRange(dateField(maxOnly, "2027-01-01"))).toBe(
71
+ true,
72
+ );
73
+ expect(defaultValueOutOfRange(dateField(maxOnly, "2026-01-01"))).toBe(
74
+ false,
75
+ );
76
+ });
77
+
78
+ it("flags against a single-sided min_date", () => {
79
+ const minOnly = { min_date: { mode: "static", value: "2026-01-01" } };
80
+ expect(defaultValueOutOfRange(dateField(minOnly, "2025-12-31"))).toBe(
81
+ true,
82
+ );
83
+ expect(defaultValueOutOfRange(dateField(minOnly, "2026-06-01"))).toBe(
84
+ false,
85
+ );
86
+ });
87
+ });
88
+
89
+ describe("a datetime default is checked at minute precision", () => {
90
+ const maxAt = { max_date: { mode: "static", value: "2026-06-30T10:00" } };
91
+ const datetimeField = (restrictions, defaultValue) => ({
92
+ ...dateField(restrictions, defaultValue),
93
+ type: "datetime",
94
+ widget: "datetime",
95
+ });
96
+
97
+ it("accepts the bound's own minute", () => {
98
+ expect(
99
+ defaultValueOutOfRange(datetimeField(maxAt, "2026-06-30T10:00")),
100
+ ).toBe(false);
101
+ });
102
+
103
+ it("flags one minute past it", () => {
104
+ expect(
105
+ defaultValueOutOfRange(datetimeField(maxAt, "2026-06-30T10:01")),
106
+ ).toBe(true);
107
+ });
108
+
109
+ it("does not blank out the whole day of a midnight bound", () => {
110
+ const midnight = { max_date: { mode: "static", value: "2026-06-30" } };
111
+ expect(
112
+ defaultValueOutOfRange(datetimeField(midnight, "2026-06-30T23:59")),
113
+ ).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("multiple cardinality defaults are checked element by element", () => {
118
+ it("accepts a list where every value is inside the range", () => {
119
+ expect(
120
+ defaultValueOutOfRange(dateField(range, ["2026-03-01", "2026-07-01"])),
121
+ ).toBe(false);
122
+ });
123
+
124
+ it("flags a list where one value is outside", () => {
125
+ expect(
126
+ defaultValueOutOfRange(dateField(range, ["2026-03-01", "2027-07-01"])),
127
+ ).toBe(true);
128
+ });
129
+ });
130
+
131
+ describe("a relative restriction is resolved live", () => {
132
+ it("flags a default beyond a relative maximum", () => {
133
+ const relative = {
134
+ max_date: { mode: "dynamic", amount: 1, unit: "days" },
135
+ };
136
+
137
+ expect(defaultValueOutOfRange(dateField(relative, "2030-01-01"))).toBe(
138
+ true,
139
+ );
140
+ });
141
+
142
+ it("accepts a default inside a relative window", () => {
143
+ const relative = {
144
+ min_date: { mode: "dynamic", amount: -1, unit: "years" },
145
+ };
146
+ const recent = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
147
+ .toISOString()
148
+ .slice(0, 10);
149
+ expect(defaultValueOutOfRange(dateField(relative, recent))).toBe(false);
150
+ });
151
+
152
+ it("does not compare a past default against a relative maximum", () => {
153
+ const relative = {
154
+ max_date: { mode: "dynamic", amount: -1, unit: "days" },
155
+ };
156
+ expect(defaultValueOutOfRange(dateField(relative, "2020-01-01"))).toBe(
157
+ false,
158
+ );
159
+ });
160
+ });
161
+
162
+ describe("the resolved block that arrived with the template is not trusted", () => {
163
+ it("stale bounds do not mask an invalid default", () => {
164
+ const field = dateField(
165
+ {
166
+ between: { mode: "static", start: "2026-01-01", end: "2026-12-31" },
167
+ resolved: { min: null, max: "2027-12-31" },
168
+ },
169
+ "2027-06-01",
170
+ );
171
+
172
+ expect(defaultValueOutOfRange(field)).toBe(true);
173
+ });
174
+ });
175
+ });