@truedat/df 8.13.4 → 8.13.6
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,497 @@
|
|
|
1
|
+
const UNITS = ["days", "weeks", "months", "years"];
|
|
2
|
+
const DATE_TYPES = ["date", "datetime"];
|
|
3
|
+
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
export const resolveRestrictions = (
|
|
6
|
+
restrictions,
|
|
7
|
+
fieldType,
|
|
8
|
+
now = new Date(),
|
|
9
|
+
) => {
|
|
10
|
+
if (!DATE_TYPES.includes(fieldType)) return open();
|
|
11
|
+
|
|
12
|
+
const r = restrictions || {};
|
|
13
|
+
const range = effectiveBetween(r, now, fieldType);
|
|
14
|
+
const lows = [limitOf(r.min_date, now, fieldType, "min"), range.low].filter(
|
|
15
|
+
defined,
|
|
16
|
+
);
|
|
17
|
+
const highs = [limitOf(r.max_date, now, fieldType, "max"), range.high].filter(
|
|
18
|
+
defined,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
min: format(tightest(lows, "min"), fieldType),
|
|
23
|
+
max: format(tightest(highs, "max"), fieldType),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const publishedBounds = (restrictions) => {
|
|
28
|
+
const resolved = restrictions && restrictions.resolved;
|
|
29
|
+
if (!resolved) return undefined;
|
|
30
|
+
return { min: resolved.min ?? undefined, max: resolved.max ?? undefined };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const open = () => ({ min: undefined, max: undefined });
|
|
34
|
+
|
|
35
|
+
const defined = (value) => value !== null && value !== undefined;
|
|
36
|
+
|
|
37
|
+
const limitOf = (side, now, fieldType, which) => {
|
|
38
|
+
if (!side || typeof side !== "object") return null;
|
|
39
|
+
if (side.mode === "static") return parseBound(side.value, which, fieldType);
|
|
40
|
+
if (side.mode === "dynamic") return shift(side, now, fieldType, which);
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const effectiveBetween = (r, now, fieldType) => {
|
|
45
|
+
const between = r.between;
|
|
46
|
+
|
|
47
|
+
if (between && between.mode === "static") {
|
|
48
|
+
return {
|
|
49
|
+
low: parseBound(between.start, "min", fieldType),
|
|
50
|
+
high: parseBound(between.end, "max", fieldType),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (between && between.mode === "dynamic") {
|
|
55
|
+
return {
|
|
56
|
+
low: shift(between.from, now, fieldType, "min", false),
|
|
57
|
+
high: shift(between.to, now, fieldType, "max", false),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hasLegacy =
|
|
62
|
+
r.between_start !== undefined || r.between_end !== undefined;
|
|
63
|
+
if (!hasLegacy) return { low: null, high: null };
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
low: parseBound(r.between_start, "min", fieldType),
|
|
67
|
+
high: parseBound(r.between_end, "max", fieldType),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const shift = (side, now, fieldType, which, invertPositiveMinimum = true) => {
|
|
72
|
+
const amount = toInteger(side.amount);
|
|
73
|
+
if (amount === null || !UNITS.includes(side.unit)) return null;
|
|
74
|
+
const anchor = fieldType === "datetime" ? localCalendarDate(now) : now;
|
|
75
|
+
const offset =
|
|
76
|
+
invertPositiveMinimum && which === "min" && amount > 0 ? -amount : amount;
|
|
77
|
+
const shifted = shiftFromNow(anchor, offset, side.unit, fieldType);
|
|
78
|
+
|
|
79
|
+
return fieldType === "datetime" && shifted instanceof Date
|
|
80
|
+
? atDayBoundary(shifted, which)
|
|
81
|
+
: shifted;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const shiftFromNow = (now, amount, unit, fieldType) => {
|
|
85
|
+
switch (unit) {
|
|
86
|
+
case "days":
|
|
87
|
+
return addDays(now, amount, fieldType);
|
|
88
|
+
case "weeks":
|
|
89
|
+
return addDays(now, amount * 7, fieldType);
|
|
90
|
+
case "months":
|
|
91
|
+
return addMonths(now, amount, fieldType);
|
|
92
|
+
case "years":
|
|
93
|
+
return addMonths(now, amount * 12, fieldType);
|
|
94
|
+
default:
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const addDays = (now, amount, fieldType) =>
|
|
100
|
+
fieldType === "date"
|
|
101
|
+
? sameKind(new Date(now.getTime() + amount * MS_IN_DAY), "date")
|
|
102
|
+
: sameKind(new Date(now.getTime() + amount * MS_IN_DAY), "datetime");
|
|
103
|
+
|
|
104
|
+
const addMonths = (now, amount, fieldType) => {
|
|
105
|
+
const year = now.getUTCFullYear();
|
|
106
|
+
const month = now.getUTCMonth();
|
|
107
|
+
const day = now.getUTCDate();
|
|
108
|
+
|
|
109
|
+
const zeroBased = month + amount;
|
|
110
|
+
const targetYear = year + Math.floor(zeroBased / 12);
|
|
111
|
+
const targetMonth = ((zeroBased % 12) + 12) % 12;
|
|
112
|
+
const lastDay = daysInMonth(targetYear, targetMonth);
|
|
113
|
+
|
|
114
|
+
return sameKind(
|
|
115
|
+
new Date(
|
|
116
|
+
Date.UTC(
|
|
117
|
+
targetYear,
|
|
118
|
+
targetMonth,
|
|
119
|
+
Math.min(day, lastDay),
|
|
120
|
+
now.getUTCHours(),
|
|
121
|
+
now.getUTCMinutes(),
|
|
122
|
+
now.getUTCSeconds(),
|
|
123
|
+
0,
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
fieldType,
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const daysInMonth = (year, month) =>
|
|
131
|
+
new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
|
132
|
+
|
|
133
|
+
const toInteger = (value) => {
|
|
134
|
+
if (Number.isInteger(value)) return value;
|
|
135
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
136
|
+
return parseInt(value, 10);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const parseBound = (value, which, fieldType) => {
|
|
142
|
+
if (typeof value !== "string" || !value) return null;
|
|
143
|
+
if (fieldType !== "datetime" || value.includes("T")) {
|
|
144
|
+
return parse(value, fieldType);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return parseLocalDateTime(`${value}T${which === "max" ? "23:59" : "00:00"}`);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const parse = (value, fieldType) => {
|
|
151
|
+
if (typeof value !== "string" || !value) return null;
|
|
152
|
+
|
|
153
|
+
if (fieldType === "datetime") {
|
|
154
|
+
if (!value.includes("T")) return parseLocalDateTime(`${value}T00:00`);
|
|
155
|
+
return parseDateTime(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const ms = Date.parse(
|
|
159
|
+
value.includes("T") ? normalizeIso(value) : `${value}T00:00:00Z`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (Number.isNaN(ms)) return null;
|
|
163
|
+
return sameKind(new Date(ms), fieldType);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const parseDateTime = (value) => {
|
|
167
|
+
const zoned = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(value);
|
|
168
|
+
if (zoned) {
|
|
169
|
+
const ms = Date.parse(normalizeIso(value));
|
|
170
|
+
return Number.isNaN(ms) ? null : utcCalendarDate(new Date(ms));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return parseLocalDateTime(value);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const parseLocalDateTime = (value) => {
|
|
177
|
+
const match =
|
|
178
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/.exec(
|
|
179
|
+
value,
|
|
180
|
+
);
|
|
181
|
+
if (!match) return null;
|
|
182
|
+
|
|
183
|
+
const [, year, month, day, hours, minutes] = match.map(Number);
|
|
184
|
+
if (
|
|
185
|
+
month < 1 ||
|
|
186
|
+
month > 12 ||
|
|
187
|
+
day < 1 ||
|
|
188
|
+
day > daysInMonth(year, month - 1) ||
|
|
189
|
+
hours > 23 ||
|
|
190
|
+
minutes > 59
|
|
191
|
+
) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Date(Date.UTC(year, month - 1, day, hours, minutes));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const localCalendarDate = (date) =>
|
|
199
|
+
new Date(
|
|
200
|
+
Date.UTC(
|
|
201
|
+
date.getFullYear(),
|
|
202
|
+
date.getMonth(),
|
|
203
|
+
date.getDate(),
|
|
204
|
+
date.getHours(),
|
|
205
|
+
date.getMinutes(),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const utcCalendarDate = (date) =>
|
|
210
|
+
new Date(
|
|
211
|
+
Date.UTC(
|
|
212
|
+
date.getUTCFullYear(),
|
|
213
|
+
date.getUTCMonth(),
|
|
214
|
+
date.getUTCDate(),
|
|
215
|
+
date.getUTCHours(),
|
|
216
|
+
date.getUTCMinutes(),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const atDayBoundary = (date, which) =>
|
|
221
|
+
new Date(
|
|
222
|
+
Date.UTC(
|
|
223
|
+
date.getUTCFullYear(),
|
|
224
|
+
date.getUTCMonth(),
|
|
225
|
+
date.getUTCDate(),
|
|
226
|
+
which === "max" ? 23 : 0,
|
|
227
|
+
which === "max" ? 59 : 0,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const normalizeIso = (iso) => {
|
|
232
|
+
const match = /^(.*T\d{2}:\d{2})(.*)$/.exec(iso);
|
|
233
|
+
if (!match) return iso;
|
|
234
|
+
|
|
235
|
+
const [, dateAndMinute, suffix] = match;
|
|
236
|
+
const remainder = suffix.replace(/(Z|[+-]\d{2}:?\d{2})$/i, "");
|
|
237
|
+
return /:\d{2}/.test(remainder) ? iso : `${dateAndMinute}:00${suffix}`;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const sameKind = (date, fieldType) =>
|
|
241
|
+
fieldType === "date"
|
|
242
|
+
? new Date(
|
|
243
|
+
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
|
|
244
|
+
)
|
|
245
|
+
: date;
|
|
246
|
+
|
|
247
|
+
const tightest = (candidates, direction) => {
|
|
248
|
+
const usable = candidates.filter((c) => c instanceof Date);
|
|
249
|
+
if (usable.length === 0) return null;
|
|
250
|
+
|
|
251
|
+
return usable.reduce((acc, candidate) => {
|
|
252
|
+
if (acc === null) return candidate;
|
|
253
|
+
const cmp = candidate.getTime() - acc.getTime();
|
|
254
|
+
return direction === "min"
|
|
255
|
+
? cmp > 0
|
|
256
|
+
? candidate
|
|
257
|
+
: acc
|
|
258
|
+
: cmp < 0
|
|
259
|
+
? candidate
|
|
260
|
+
: acc;
|
|
261
|
+
}, null);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
265
|
+
|
|
266
|
+
const format = (date, fieldType) => {
|
|
267
|
+
if (!(date instanceof Date)) return undefined;
|
|
268
|
+
|
|
269
|
+
const day = `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
270
|
+
if (fieldType === "date") return day;
|
|
271
|
+
|
|
272
|
+
return `${day}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const toInputBound = (value, side) => {
|
|
276
|
+
if (typeof value !== "string" || !value) return undefined;
|
|
277
|
+
if (value.includes("T")) return value.slice(0, 16);
|
|
278
|
+
return `${value}T${side === "max" ? "23:59" : "00:00"}`;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const compareToLimits = (value, min, max) => {
|
|
282
|
+
if (typeof value !== "string" || !value) return "ok";
|
|
283
|
+
if (!min && !max) return "ok";
|
|
284
|
+
|
|
285
|
+
const parsedValue = parseIsoDateValue(value);
|
|
286
|
+
if (!parsedValue) {
|
|
287
|
+
return isPartialIsoDateInput(value) ? "ok" : "invalid_date";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const parsedMin = min ? parseIsoDateValue(min) : null;
|
|
291
|
+
const parsedMax = max ? parseIsoDateValue(max) : null;
|
|
292
|
+
const normalizedFor = (bound) =>
|
|
293
|
+
valueForPrecision(parsedValue, granularityOf(bound || min || max));
|
|
294
|
+
const minPrecision = granularityOf(min || max);
|
|
295
|
+
const maxPrecision = granularityOf(max || min);
|
|
296
|
+
const minValue = parsedMin && valueForPrecision(parsedMin, minPrecision);
|
|
297
|
+
const maxValue = parsedMax && valueForPrecision(parsedMax, maxPrecision);
|
|
298
|
+
const normalizedMin = normalizedFor(min);
|
|
299
|
+
const normalizedMax = normalizedFor(max);
|
|
300
|
+
|
|
301
|
+
if (parsedMin && normalizedMin < minValue) {
|
|
302
|
+
return "too_early";
|
|
303
|
+
}
|
|
304
|
+
if (parsedMax && normalizedMax > maxValue) {
|
|
305
|
+
return "too_late";
|
|
306
|
+
}
|
|
307
|
+
return "ok";
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const isWithinLimits = (value, min, max) =>
|
|
311
|
+
compareToLimits(value, min, max) === "ok";
|
|
312
|
+
|
|
313
|
+
const granularityOf = (bound) =>
|
|
314
|
+
typeof bound === "string" && bound.includes("T") ? "minute" : "day";
|
|
315
|
+
|
|
316
|
+
const ISO_DATE_VALUE =
|
|
317
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/i;
|
|
318
|
+
|
|
319
|
+
const isPartialIsoDateInput = (value) => {
|
|
320
|
+
const partialDate = /^(\d{0,4})(?:-(\d{0,2})(?:-(\d{0,2}))?)?$/.exec(value);
|
|
321
|
+
if (partialDate) {
|
|
322
|
+
const [, yearString, monthString, dayString] = partialDate;
|
|
323
|
+
if (!monthString || monthString.length === 0) return true;
|
|
324
|
+
if (
|
|
325
|
+
(monthString.length === 1 && !/^[01]$/.test(monthString)) ||
|
|
326
|
+
(monthString.length === 2 &&
|
|
327
|
+
(Number(monthString) < 1 || Number(monthString) > 12))
|
|
328
|
+
) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (dayString === undefined || dayString.length === 0) return true;
|
|
332
|
+
if (monthString.length !== 2 || yearString.length !== 4) return false;
|
|
333
|
+
|
|
334
|
+
const year = Number(yearString);
|
|
335
|
+
const month = Number(monthString);
|
|
336
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
337
|
+
const daysInMonth = [
|
|
338
|
+
31,
|
|
339
|
+
leap ? 29 : 28,
|
|
340
|
+
31,
|
|
341
|
+
30,
|
|
342
|
+
31,
|
|
343
|
+
30,
|
|
344
|
+
31,
|
|
345
|
+
31,
|
|
346
|
+
30,
|
|
347
|
+
31,
|
|
348
|
+
30,
|
|
349
|
+
31,
|
|
350
|
+
];
|
|
351
|
+
const maxDay = daysInMonth[month - 1];
|
|
352
|
+
return dayString.length === 1
|
|
353
|
+
? Number(dayString) <= 2 ||
|
|
354
|
+
(dayString === "3" && maxDay >= 30) ||
|
|
355
|
+
dayString === "0"
|
|
356
|
+
: Number(dayString) >= 1 && Number(dayString) <= maxDay;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const partialDateTime = /^(\d{4}-\d{2}-\d{2})T(.*)$/.exec(value);
|
|
360
|
+
if (!partialDateTime || !parseIsoDateValue(partialDateTime[1])) return false;
|
|
361
|
+
|
|
362
|
+
const partialTime =
|
|
363
|
+
/^(\d{0,2})(?::(\d{0,2})(?::(\d{0,2})(?:\.(\d*))?)?)?$/.exec(
|
|
364
|
+
partialDateTime[2],
|
|
365
|
+
);
|
|
366
|
+
if (!partialTime) return false;
|
|
367
|
+
|
|
368
|
+
const [, hour = "", minute, second] = partialTime;
|
|
369
|
+
const validComponent = (component, max, firstDigitMax) => {
|
|
370
|
+
if (component === undefined || component === "") return true;
|
|
371
|
+
if (component.length === 1) return Number(component) <= firstDigitMax;
|
|
372
|
+
return Number(component) <= max;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
validComponent(hour, 23, 2) &&
|
|
377
|
+
validComponent(minute, 59, 5) &&
|
|
378
|
+
validComponent(second, 59, 5)
|
|
379
|
+
);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const parseIsoDateValue = (value) => {
|
|
383
|
+
if (typeof value !== "string") return null;
|
|
384
|
+
const match = ISO_DATE_VALUE.exec(value);
|
|
385
|
+
if (!match) return null;
|
|
386
|
+
|
|
387
|
+
const [
|
|
388
|
+
,
|
|
389
|
+
rawYear,
|
|
390
|
+
rawMonth,
|
|
391
|
+
rawDay,
|
|
392
|
+
rawHour,
|
|
393
|
+
rawMinute,
|
|
394
|
+
rawSecond,
|
|
395
|
+
fractionalSecond,
|
|
396
|
+
zone,
|
|
397
|
+
] = match;
|
|
398
|
+
const year = Number(rawYear);
|
|
399
|
+
const month = Number(rawMonth);
|
|
400
|
+
const day = Number(rawDay);
|
|
401
|
+
const hour = rawHour === undefined ? 0 : Number(rawHour);
|
|
402
|
+
const minute = rawMinute === undefined ? 0 : Number(rawMinute);
|
|
403
|
+
const second = rawSecond === undefined ? 0 : Number(rawSecond);
|
|
404
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
405
|
+
const daysInMonth = [
|
|
406
|
+
31,
|
|
407
|
+
leap ? 29 : 28,
|
|
408
|
+
31,
|
|
409
|
+
30,
|
|
410
|
+
31,
|
|
411
|
+
30,
|
|
412
|
+
31,
|
|
413
|
+
31,
|
|
414
|
+
30,
|
|
415
|
+
31,
|
|
416
|
+
30,
|
|
417
|
+
31,
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
month < 1 ||
|
|
422
|
+
month > 12 ||
|
|
423
|
+
day < 1 ||
|
|
424
|
+
day > daysInMonth[month - 1] ||
|
|
425
|
+
hour > 23 ||
|
|
426
|
+
minute > 59 ||
|
|
427
|
+
second > 59
|
|
428
|
+
) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const offsetMatch = zone && /^[+-](\d{2}):?(\d{2})$/.exec(zone);
|
|
433
|
+
if (
|
|
434
|
+
offsetMatch &&
|
|
435
|
+
(Number(offsetMatch[1]) > 14 ||
|
|
436
|
+
Number(offsetMatch[2]) > 59 ||
|
|
437
|
+
(Number(offsetMatch[1]) === 14 && Number(offsetMatch[2]) !== 0))
|
|
438
|
+
) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const millisecond = fractionalSecond
|
|
443
|
+
? Math.floor(Number(`0.${fractionalSecond}`) * 1000)
|
|
444
|
+
: 0;
|
|
445
|
+
const validDate = createUtcDate(
|
|
446
|
+
year,
|
|
447
|
+
month - 1,
|
|
448
|
+
day,
|
|
449
|
+
hour,
|
|
450
|
+
minute,
|
|
451
|
+
second,
|
|
452
|
+
millisecond,
|
|
453
|
+
);
|
|
454
|
+
if (offsetMatch) {
|
|
455
|
+
const offsetMinutes =
|
|
456
|
+
(Number(offsetMatch[1]) * 60 + Number(offsetMatch[2])) *
|
|
457
|
+
(zone[0] === "+" ? 1 : -1);
|
|
458
|
+
validDate.setTime(validDate.getTime() - offsetMinutes * 60 * 1000);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return validDate.getUTCFullYear() < 0 || validDate.getUTCFullYear() > 9999
|
|
462
|
+
? null
|
|
463
|
+
: { date: validDate };
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const createUtcDate = (year, month, day, hour, minute, second, millisecond) => {
|
|
467
|
+
const date = new Date(0);
|
|
468
|
+
date.setUTCFullYear(year, month, day);
|
|
469
|
+
date.setUTCHours(hour, minute, second, millisecond);
|
|
470
|
+
return date;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const valueForPrecision = ({ date }, precision) =>
|
|
474
|
+
precision === "minute"
|
|
475
|
+
? Math.floor(date.getTime() / (60 * 1000))
|
|
476
|
+
: date.getUTCFullYear() * 10000 +
|
|
477
|
+
(date.getUTCMonth() + 1) * 100 +
|
|
478
|
+
date.getUTCDate();
|
|
479
|
+
|
|
480
|
+
export const checkValuesAgainstBounds = (value, { min, max }) => {
|
|
481
|
+
const candidates =
|
|
482
|
+
value === null || value === undefined
|
|
483
|
+
? []
|
|
484
|
+
: (Array.isArray(value) ? value : [value]).map(unwrap);
|
|
485
|
+
|
|
486
|
+
const verdicts = candidates.map((candidate) =>
|
|
487
|
+
compareToLimits(candidate, min, max),
|
|
488
|
+
);
|
|
489
|
+
return (
|
|
490
|
+
verdicts.find((verdict) => verdict === "invalid_date") ||
|
|
491
|
+
verdicts.find((verdict) => verdict !== "ok") ||
|
|
492
|
+
"ok"
|
|
493
|
+
);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const unwrap = (item) =>
|
|
497
|
+
item && typeof item === "object" && !Array.isArray(item) ? item.value : item;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import _ from "lodash/fp";
|
|
2
|
+
|
|
3
|
+
const withoutOrigin = (value) =>
|
|
4
|
+
_.isArray(value)
|
|
5
|
+
? _.map(withoutOrigin)(value)
|
|
6
|
+
: _.isPlainObject(value)
|
|
7
|
+
? _.flow(_.omit("origin"), _.mapValues(withoutOrigin))(value)
|
|
8
|
+
: value;
|
|
9
|
+
|
|
10
|
+
export const dateValueUnchanged = (value, originalField) =>
|
|
11
|
+
!_.isNil(originalField) &&
|
|
12
|
+
_.isEqual(withoutOrigin(value), withoutOrigin(originalField.value));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const rowBaselines = new WeakMap();
|
|
2
|
+
const newRow = Symbol("new table row");
|
|
3
|
+
|
|
4
|
+
export const markNewRow = (row) => {
|
|
5
|
+
rowBaselines.set(row, new Map([[newRow, true]]));
|
|
6
|
+
return row;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const getOriginalRow = (row, index, originalRows) => {
|
|
10
|
+
if (row === null || typeof row !== "object") return undefined;
|
|
11
|
+
const baselines = rowBaselines.get(row);
|
|
12
|
+
const original = baselines?.has(newRow)
|
|
13
|
+
? undefined
|
|
14
|
+
: baselines?.has(originalRows)
|
|
15
|
+
? baselines.get(originalRows)
|
|
16
|
+
: originalRows?.includes(row)
|
|
17
|
+
? row
|
|
18
|
+
: originalRows?.[index];
|
|
19
|
+
originalRows &&
|
|
20
|
+
rowBaselines.set(
|
|
21
|
+
row,
|
|
22
|
+
new Map([...(baselines || []), [originalRows, original]]),
|
|
23
|
+
);
|
|
24
|
+
return original;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const copyRow = (source, output) => {
|
|
28
|
+
const baselines = rowBaselines.get(source);
|
|
29
|
+
baselines && rowBaselines.set(output, new Map(baselines));
|
|
30
|
+
return output;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const copyTableRowLineage = (source, output) => {
|
|
34
|
+
const objects = source && output && typeof output === "object";
|
|
35
|
+
objects && copyRow(source, output);
|
|
36
|
+
objects &&
|
|
37
|
+
Object.keys(output).forEach((key) =>
|
|
38
|
+
copyTableRowLineage(source[key], output[key]),
|
|
39
|
+
);
|
|
40
|
+
return output;
|
|
41
|
+
};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import _ from "lodash/fp";
|
|
2
2
|
import { flattenFields } from "./flattenFields";
|
|
3
|
+
import { getOriginalRow } from "./originalTableRows";
|
|
4
|
+
import { dateValueUnchanged } from "./dateValueUnchanged";
|
|
5
|
+
import {
|
|
6
|
+
publishedBounds,
|
|
7
|
+
checkValuesAgainstBounds,
|
|
8
|
+
resolveRestrictions,
|
|
9
|
+
} from "./dateRestrictions";
|
|
3
10
|
|
|
4
11
|
const fieldIsHiddenVerifier =
|
|
5
12
|
(content) =>
|
|
@@ -27,15 +34,19 @@ const isTableRequired = (field, content) => {
|
|
|
27
34
|
return false;
|
|
28
35
|
};
|
|
29
36
|
|
|
30
|
-
const validateTemplateContent = (schema, content) => {
|
|
37
|
+
const validateTemplateContent = (schema, content, originalContent) => {
|
|
31
38
|
const fieldIsHidden = fieldIsHiddenVerifier(content);
|
|
32
39
|
const templateContent = _.reject(fieldIsHidden)(schema);
|
|
33
|
-
return _.flow(
|
|
40
|
+
return _.flow(
|
|
41
|
+
validateRequired(templateContent),
|
|
42
|
+
validateDateRestrictions(templateContent, originalContent),
|
|
43
|
+
_.head,
|
|
44
|
+
)([[], content]);
|
|
34
45
|
};
|
|
35
46
|
|
|
36
|
-
export const validateContent = (template) => (content) => {
|
|
47
|
+
export const validateContent = (template, originalContent) => (content) => {
|
|
37
48
|
const schema = flattenFields(template);
|
|
38
|
-
return validateTemplateContent(schema, content);
|
|
49
|
+
return validateTemplateContent(schema, content, originalContent);
|
|
39
50
|
};
|
|
40
51
|
|
|
41
52
|
const isEmptyValue = (value) =>
|
|
@@ -69,6 +80,66 @@ export const validateRequired =
|
|
|
69
80
|
return [[...validations, ...newValidations], content];
|
|
70
81
|
};
|
|
71
82
|
|
|
83
|
+
export const validateDateRestrictions =
|
|
84
|
+
(templateContent, originalContent) =>
|
|
85
|
+
([validations, content]) => {
|
|
86
|
+
const validateDate = (field, fieldContent, originalFieldContent, path) => {
|
|
87
|
+
const unchanged = dateValueUnchanged(
|
|
88
|
+
fieldContent?.value,
|
|
89
|
+
originalFieldContent,
|
|
90
|
+
);
|
|
91
|
+
const bounds =
|
|
92
|
+
publishedBounds(field?.restrictions) ||
|
|
93
|
+
resolveRestrictions(field?.restrictions, field?.type);
|
|
94
|
+
if (
|
|
95
|
+
unchanged ||
|
|
96
|
+
!bounds ||
|
|
97
|
+
!["date", "datetime"].includes(field?.type) ||
|
|
98
|
+
(!bounds.min && !bounds.max)
|
|
99
|
+
) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const verdict = checkValuesAgainstBounds(fieldContent?.value, bounds);
|
|
103
|
+
if (verdict === "invalid_date") {
|
|
104
|
+
return [{ error: "invalid date", field: path }];
|
|
105
|
+
}
|
|
106
|
+
return verdict === "ok"
|
|
107
|
+
? []
|
|
108
|
+
: [{ error: "date out of range", field: path, values: bounds }];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const validateField = (
|
|
112
|
+
field,
|
|
113
|
+
fieldContent,
|
|
114
|
+
originalFieldContent,
|
|
115
|
+
path = field?.name,
|
|
116
|
+
) =>
|
|
117
|
+
field?.type === "dynamic_table"
|
|
118
|
+
? (fieldContent?.value || []).flatMap((row, rowIndex) =>
|
|
119
|
+
(field?.values?.table_columns || []).flatMap((column) =>
|
|
120
|
+
validateField(
|
|
121
|
+
column,
|
|
122
|
+
row?.[column.name],
|
|
123
|
+
getOriginalRow(row, rowIndex, originalFieldContent?.value)?.[
|
|
124
|
+
column.name
|
|
125
|
+
],
|
|
126
|
+
`${path}.${rowIndex}.${column.name}`,
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
: validateDate(field, fieldContent, originalFieldContent, path);
|
|
131
|
+
|
|
132
|
+
const newValidations = templateContent.flatMap((field) =>
|
|
133
|
+
validateField(
|
|
134
|
+
field,
|
|
135
|
+
content?.[field?.name],
|
|
136
|
+
originalContent?.[field?.name],
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return [[...validations, ...newValidations], content];
|
|
141
|
+
};
|
|
142
|
+
|
|
72
143
|
export const dependentRequired = (field, content) => {
|
|
73
144
|
if (_.has("mandatory")(field)) {
|
|
74
145
|
const on = field?.mandatory?.on;
|