@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,519 @@
1
+ import { validateContent, validateDateRestrictions } from "../validateContent";
2
+
3
+ const template = {
4
+ content: [
5
+ {
6
+ name: "dates",
7
+ fields: [
8
+ {
9
+ name: "fecha",
10
+ type: "date",
11
+ cardinality: "?",
12
+ restrictions: {
13
+ resolved: { min: "2026-01-01", max: "2026-12-31" },
14
+ },
15
+ },
16
+ ],
17
+ },
18
+ ],
19
+ };
20
+
21
+ const dateField = template.content[0].fields[0];
22
+ const fieldValue = (value, origin = "user") => ({ value, origin });
23
+ const dateContent = (value, origin) => ({ fecha: fieldValue(value, origin) });
24
+ const dateError = (
25
+ field = "fecha",
26
+ values = dateField.restrictions.resolved,
27
+ ) => ({
28
+ error: "date out of range",
29
+ field,
30
+ values,
31
+ });
32
+ const templateWith = (fields) => ({ content: [{ name: "fields", fields }] });
33
+ const tableField = {
34
+ name: "periodos",
35
+ type: "dynamic_table",
36
+ values: {
37
+ table_columns: [
38
+ { ...dateField, name: "fin" },
39
+ { name: "description", type: "string" },
40
+ ],
41
+ },
42
+ };
43
+ const tableTemplate = templateWith([tableField]);
44
+ const row = (value, description = "original") => ({
45
+ fin: fieldValue(value),
46
+ description: fieldValue(description),
47
+ });
48
+ const tableContent = (rows) => ({ periodos: fieldValue(rows) });
49
+
50
+ describe("validateContent date restrictions", () => {
51
+ it("blocks a complete value outside the published bounds", () => {
52
+ expect(
53
+ validateContent(template)({
54
+ fecha: { value: "2027-01-01", origin: "user" },
55
+ }),
56
+ ).toEqual([
57
+ {
58
+ error: "date out of range",
59
+ field: "fecha",
60
+ values: { min: "2026-01-01", max: "2026-12-31" },
61
+ },
62
+ ]);
63
+ });
64
+
65
+ it.each(["2026-01-01", "2026-12-31"])(
66
+ "accepts inclusive limit %s",
67
+ (value) => {
68
+ expect(
69
+ validateContent(template)({
70
+ fecha: { value, origin: "user" },
71
+ }),
72
+ ).toEqual([]);
73
+ },
74
+ );
75
+
76
+ it("validates creation when original content is explicitly undefined", () => {
77
+ expect(
78
+ validateContent(template, undefined)(dateContent("2027-01-01")),
79
+ ).toEqual([dateError()]);
80
+ });
81
+
82
+ it("exempts an unchanged out-of-range date", () => {
83
+ expect(
84
+ validateContent(
85
+ template,
86
+ dateContent("2027-01-01"),
87
+ )(dateContent("2027-01-01")),
88
+ ).toEqual([]);
89
+ });
90
+
91
+ it("validates a changed out-of-range date", () => {
92
+ expect(
93
+ validateContent(
94
+ template,
95
+ dateContent("2026-01-01"),
96
+ )(dateContent("2027-01-01")),
97
+ ).toEqual([dateError()]);
98
+ });
99
+
100
+ it("accepts a changed date within the bounds", () => {
101
+ expect(
102
+ validateContent(
103
+ template,
104
+ dateContent("2027-01-01"),
105
+ )(dateContent("2026-01-01")),
106
+ ).toEqual([]);
107
+ });
108
+
109
+ it("exempts an out-of-range date restored after an intermediate edit", () => {
110
+ const validate = validateContent(template, dateContent("2027-01-01"));
111
+ validate(dateContent("2028-01-01"));
112
+
113
+ expect(validate(dateContent("2027-01-01"))).toEqual([]);
114
+ });
115
+
116
+ it("ignores an origin-only change", () => {
117
+ expect(
118
+ validateContent(
119
+ template,
120
+ dateContent("2027-01-01", "system"),
121
+ )(dateContent("2027-01-01", "user")),
122
+ ).toEqual([]);
123
+ });
124
+
125
+ it("validates a date absent from the original content", () => {
126
+ expect(validateContent(template, {})(dateContent("2027-01-01"))).toEqual([
127
+ dateError(),
128
+ ]);
129
+ });
130
+
131
+ it.each([null, undefined, "", []])(
132
+ "preserves required validation for unchanged empty value %p",
133
+ (value) => {
134
+ const requiredTemplate = templateWith([
135
+ { ...dateField, cardinality: "1" },
136
+ ]);
137
+
138
+ expect(
139
+ validateContent(
140
+ requiredTemplate,
141
+ dateContent(value),
142
+ )(dateContent(value)),
143
+ ).toEqual([{ error: "missing required", field: "fecha" }]);
144
+ },
145
+ );
146
+
147
+ it("threads original content through the standalone date validator", () => {
148
+ const content = dateContent("2027-01-01");
149
+ const validations = [{ error: "existing", field: "other" }];
150
+
151
+ expect(
152
+ validateDateRestrictions(
153
+ [dateField],
154
+ dateContent("2027-01-01"),
155
+ )([validations, content]),
156
+ ).toEqual([validations, content]);
157
+ });
158
+
159
+ describe("multiple values", () => {
160
+ const multipleTemplate = templateWith([{ ...dateField, cardinality: "*" }]);
161
+
162
+ it("validates every value during creation", () => {
163
+ expect(
164
+ validateContent(multipleTemplate)(
165
+ dateContent(["2026-01-01", "2027-01-01"]),
166
+ ),
167
+ ).toEqual([dateError()]);
168
+ });
169
+
170
+ it("exempts a deeply equal array with a different reference", () => {
171
+ expect(
172
+ validateContent(
173
+ multipleTemplate,
174
+ dateContent([fieldValue("2027-01-01")]),
175
+ )(dateContent([fieldValue("2027-01-01")])),
176
+ ).toEqual([]);
177
+ });
178
+
179
+ it("ignores origin-only changes within array entries", () => {
180
+ expect(
181
+ validateContent(
182
+ multipleTemplate,
183
+ dateContent([fieldValue("2027-01-01", "system")]),
184
+ )(dateContent([fieldValue("2027-01-01", "user")])),
185
+ ).toEqual([]);
186
+ });
187
+
188
+ it.each([
189
+ ["2027-01-01", "2026-02-01"],
190
+ ["2027-01-01", "2026-01-01", "2026-02-01"],
191
+ ["2026-01-01", "2027-01-01"],
192
+ ["2027-01-01"],
193
+ ])("validates the entire modified array %p", (...values) => {
194
+ expect(
195
+ validateContent(
196
+ multipleTemplate,
197
+ dateContent(["2027-01-01", "2026-01-01"]),
198
+ )(dateContent(values)),
199
+ ).toEqual([dateError()]);
200
+ });
201
+
202
+ it("accepts removal of all values from an optional field", () => {
203
+ expect(
204
+ validateContent(
205
+ multipleTemplate,
206
+ dateContent(["2027-01-01"]),
207
+ )(dateContent([])),
208
+ ).toEqual([]);
209
+ });
210
+ });
211
+
212
+ describe("editing dynamic tables", () => {
213
+ it("exempts unchanged dates when an unrelated cell changes", () => {
214
+ expect(
215
+ validateContent(
216
+ tableTemplate,
217
+ tableContent([row("2027-01-01")]),
218
+ )(tableContent([row("2027-01-01", "edited")])),
219
+ ).toEqual([]);
220
+ });
221
+
222
+ it("validates only the changed date at its corresponding row", () => {
223
+ expect(
224
+ validateContent(
225
+ tableTemplate,
226
+ tableContent([row("2027-01-01"), row("2026-01-01")]),
227
+ )(tableContent([row("2027-01-01"), row("2028-01-01")])),
228
+ ).toEqual([dateError("periodos.1.fin")]);
229
+ });
230
+
231
+ it("validates a newly added row even when its date matches another row", () => {
232
+ expect(
233
+ validateContent(
234
+ tableTemplate,
235
+ tableContent([row("2027-01-01")]),
236
+ )(tableContent([row("2027-01-01"), row("2027-01-01")])),
237
+ ).toEqual([dateError("periodos.1.fin")]);
238
+ });
239
+
240
+ it("validates a newly added table", () => {
241
+ expect(
242
+ validateContent(tableTemplate, {})(tableContent([row("2027-01-01")])),
243
+ ).toEqual([dateError("periodos.0.fin")]);
244
+ });
245
+
246
+ it("does not reject dates from removed rows", () => {
247
+ expect(
248
+ validateContent(
249
+ tableTemplate,
250
+ tableContent([row("2027-01-01")]),
251
+ )(tableContent([])),
252
+ ).toEqual([]);
253
+ });
254
+
255
+ it("compares corresponding columns independently", () => {
256
+ const twoDates = templateWith([
257
+ {
258
+ ...tableField,
259
+ values: {
260
+ table_columns: [
261
+ { ...dateField, name: "fin" },
262
+ { ...dateField, name: "inicio" },
263
+ ],
264
+ },
265
+ },
266
+ ]);
267
+
268
+ expect(
269
+ validateContent(
270
+ twoDates,
271
+ tableContent([
272
+ {
273
+ fin: fieldValue("2027-01-01"),
274
+ inicio: fieldValue("2026-01-01"),
275
+ },
276
+ ]),
277
+ )(
278
+ tableContent([
279
+ {
280
+ fin: fieldValue("2027-01-01"),
281
+ inicio: fieldValue("2028-01-01"),
282
+ },
283
+ ]),
284
+ ),
285
+ ).toEqual([dateError("periodos.0.inicio")]);
286
+ });
287
+
288
+ it("recurses through nested dynamic-table cells", () => {
289
+ const nestedTemplate = templateWith([
290
+ {
291
+ ...tableField,
292
+ values: { table_columns: [{ ...tableField, name: "nested" }] },
293
+ },
294
+ ]);
295
+ const nestedContent = (date) =>
296
+ tableContent([
297
+ {
298
+ nested: fieldValue([row(date)]),
299
+ },
300
+ ]);
301
+
302
+ expect(
303
+ validateContent(
304
+ nestedTemplate,
305
+ nestedContent("2026-01-01"),
306
+ )(nestedContent("2027-01-01")),
307
+ ).toEqual([dateError("periodos.0.nested.0.fin")]);
308
+ });
309
+
310
+ it("preserves required validation for unchanged empty table cells", () => {
311
+ const requiredTable = templateWith([
312
+ {
313
+ ...tableField,
314
+ values: {
315
+ table_columns: [{ ...dateField, name: "fin", cardinality: "1" }],
316
+ },
317
+ },
318
+ ]);
319
+
320
+ expect(
321
+ validateContent(
322
+ requiredTable,
323
+ tableContent([row("")]),
324
+ )(tableContent([row("")])),
325
+ ).toEqual([{ error: "missing required", field: "periodos" }]);
326
+ });
327
+ });
328
+
329
+ describe("datetime", () => {
330
+ const bounds = { min: "2026-01-01T00:00", max: "2026-12-31T23:59" };
331
+ const datetimeTemplate = templateWith([
332
+ {
333
+ ...dateField,
334
+ type: "datetime",
335
+ restrictions: { resolved: bounds },
336
+ },
337
+ ]);
338
+
339
+ it("validates an out-of-range datetime during creation", () => {
340
+ expect(
341
+ validateContent(datetimeTemplate)(dateContent("2027-01-01T00:00")),
342
+ ).toEqual([dateError("fecha", bounds)]);
343
+ });
344
+
345
+ it("exempts an unchanged out-of-range datetime", () => {
346
+ expect(
347
+ validateContent(
348
+ datetimeTemplate,
349
+ dateContent("2027-01-01T00:00"),
350
+ )(dateContent("2027-01-01T00:00")),
351
+ ).toEqual([]);
352
+ });
353
+
354
+ it("validates a changed out-of-range datetime", () => {
355
+ expect(
356
+ validateContent(
357
+ datetimeTemplate,
358
+ dateContent("2027-01-01T00:00"),
359
+ )(dateContent("2027-01-01T00:01")),
360
+ ).toEqual([dateError("fecha", bounds)]);
361
+ });
362
+ });
363
+
364
+ describe("dynamic bounds", () => {
365
+ const dynamicTemplate = templateWith([
366
+ {
367
+ ...dateField,
368
+ restrictions: {
369
+ min_date: { mode: "dynamic", amount: 0, unit: "days" },
370
+ },
371
+ },
372
+ ]);
373
+
374
+ beforeEach(() => {
375
+ jest.useFakeTimers().setSystemTime(new Date("2026-10-21T12:00:00Z"));
376
+ });
377
+
378
+ afterEach(() => {
379
+ jest.useRealTimers();
380
+ });
381
+
382
+ it("validates creation against the current dynamic bounds", () => {
383
+ expect(
384
+ validateContent(dynamicTemplate)(dateContent("2026-10-20")),
385
+ ).toEqual([dateError("fecha", { min: "2026-10-21", max: undefined })]);
386
+ });
387
+
388
+ it("exempts an unchanged date that has aged outside dynamic bounds", () => {
389
+ expect(
390
+ validateContent(
391
+ dynamicTemplate,
392
+ dateContent("2026-10-20"),
393
+ )(dateContent("2026-10-20")),
394
+ ).toEqual([]);
395
+ });
396
+
397
+ it("validates a changed date against the current dynamic bounds", () => {
398
+ expect(
399
+ validateContent(
400
+ dynamicTemplate,
401
+ dateContent("2026-10-19"),
402
+ )(dateContent("2026-10-20")),
403
+ ).toEqual([dateError("fecha", { min: "2026-10-21", max: undefined })]);
404
+ });
405
+ });
406
+
407
+ it("does not reject partial keyboard input", () => {
408
+ expect(
409
+ validateContent(template)({
410
+ fecha: { value: "2026-0", origin: "user" },
411
+ }),
412
+ ).toEqual([]);
413
+ });
414
+
415
+ it.each(["2026-02-30", "2027-01-01T00:00:00Z", "2027-01-01T02:00:00+02:00"])(
416
+ "rejects complete invalid or out-of-range imported value %s",
417
+ (value) => {
418
+ expect(validateContent(template)({ fecha: fieldValue(value) })).toEqual([
419
+ value === "2026-02-30"
420
+ ? { error: "invalid date", field: "fecha" }
421
+ : dateError(
422
+ "fecha",
423
+ template.content[0].fields[0].restrictions.resolved,
424
+ ),
425
+ ]);
426
+ },
427
+ );
428
+
429
+ it("validates an invalid complete date in a dynamic-table cell", () => {
430
+ expect(
431
+ validateContent(tableTemplate)({
432
+ periodos: fieldValue([row("2026-02-30")]),
433
+ }),
434
+ ).toEqual([{ error: "invalid date", field: "periodos.0.fin" }]);
435
+ });
436
+
437
+ it("accepts a modified date whose seconds remain within its minute-level bound", () => {
438
+ const minuteTemplate = templateWith([
439
+ {
440
+ ...dateField,
441
+ type: "datetime",
442
+ restrictions: {
443
+ resolved: { min: null, max: "2026-12-31T23:59" },
444
+ },
445
+ },
446
+ ]);
447
+
448
+ expect(
449
+ validateContent(
450
+ minuteTemplate,
451
+ dateContent("2026-12-31T23:59"),
452
+ )(dateContent("2026-12-31T23:59:59Z")),
453
+ ).toEqual([]);
454
+ });
455
+
456
+ it("rejects a modified date-time with a sub-minute value past its maximum", () => {
457
+ const minuteTemplate = templateWith([
458
+ {
459
+ ...dateField,
460
+ type: "datetime",
461
+ restrictions: {
462
+ resolved: { min: null, max: "2026-12-31T23:59" },
463
+ },
464
+ },
465
+ ]);
466
+
467
+ expect(
468
+ validateContent(
469
+ minuteTemplate,
470
+ dateContent("2026-12-31T23:59"),
471
+ )(dateContent("2027-01-01T00:00:00Z")),
472
+ ).toEqual([dateError("fecha", { max: "2026-12-31T23:59" })]);
473
+ });
474
+
475
+ it("validates date columns in every dynamic-table row", () => {
476
+ const tableTemplate = {
477
+ content: [
478
+ {
479
+ name: "table",
480
+ fields: [
481
+ {
482
+ name: "periodos",
483
+ type: "dynamic_table",
484
+ values: {
485
+ table_columns: [
486
+ {
487
+ name: "fin",
488
+ type: "date",
489
+ restrictions: {
490
+ resolved: { min: null, max: "2026-12-31" },
491
+ },
492
+ },
493
+ ],
494
+ },
495
+ },
496
+ ],
497
+ },
498
+ ],
499
+ };
500
+
501
+ expect(
502
+ validateContent(tableTemplate)({
503
+ periodos: {
504
+ value: [
505
+ { fin: { value: "2026-12-31", origin: "user" } },
506
+ { fin: { value: "2027-01-01", origin: "user" } },
507
+ ],
508
+ origin: "user",
509
+ },
510
+ }),
511
+ ).toEqual([
512
+ {
513
+ error: "date out of range",
514
+ field: "periodos.1.fin",
515
+ values: { min: undefined, max: "2026-12-31" },
516
+ },
517
+ ]);
518
+ });
519
+ });
@@ -6,6 +6,7 @@ import { filterDomains } from "./filterDomains";
6
6
  import { filterFields } from "./filterFields";
7
7
  import { filterSwitches } from "./filterSwitches";
8
8
  import { filterValues } from "./filterValues";
9
+ import { copyTableRowLineage } from "./originalTableRows";
9
10
 
10
11
  const isInformed = _.flow(
11
12
  _.cond([
@@ -80,32 +81,38 @@ export const applyTemplate =
80
81
  (template) =>
81
82
  (content, domainId = null) => {
82
83
  const templateContent = flattenFields(template);
83
- return _.flow(
84
- filterFields(templateContent),
85
- filterValues(templateContent),
86
- filterDepends(templateContent),
87
- filterSwitches(templateContent),
88
- (content) => filterDomains(templateContent)(content, domainId),
89
- (content) => applyDefaults(templateContent)(content, domainId),
90
- _.pickBy((value) => isInformed(value.value)),
91
- (content) => applyTableFields(templateContent, content, domainId),
92
- )(content);
84
+ return copyTableRowLineage(
85
+ content,
86
+ _.flow(
87
+ filterFields(templateContent),
88
+ filterValues(templateContent),
89
+ filterDepends(templateContent),
90
+ filterSwitches(templateContent),
91
+ (content) => filterDomains(templateContent)(content, domainId),
92
+ (content) => applyDefaults(templateContent)(content, domainId),
93
+ _.pickBy((value) => isInformed(value.value)),
94
+ (content) => applyTableFields(templateContent, content, domainId),
95
+ )(content),
96
+ );
93
97
  };
94
98
 
95
99
  export const applyTemplateWithoutDefaults =
96
100
  (template) =>
97
101
  (content, domainId = null) => {
98
102
  const templateContent = flattenFields(template);
99
- return _.flow(
100
- filterFields(templateContent),
101
- filterValues(templateContent),
102
- filterDepends(templateContent),
103
- filterSwitches(templateContent),
104
- (content) => filterDomains(templateContent)(content, domainId),
105
- _.pickBy((value) => isInformed(value.value)),
106
- (content) =>
107
- applyTableFieldsWithoutDefaults(templateContent, content, domainId),
108
- )(content);
103
+ return copyTableRowLineage(
104
+ content,
105
+ _.flow(
106
+ filterFields(templateContent),
107
+ filterValues(templateContent),
108
+ filterDepends(templateContent),
109
+ filterSwitches(templateContent),
110
+ (content) => filterDomains(templateContent)(content, domainId),
111
+ _.pickBy((value) => isInformed(value.value)),
112
+ (content) =>
113
+ applyTableFieldsWithoutDefaults(templateContent, content, domainId),
114
+ )(content),
115
+ );
109
116
  };
110
117
 
111
118
  export default applyTemplate;