@oneblink/apps-react 2.11.0-beta.5 → 2.11.0-beta.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.
@@ -3,6 +3,8 @@ import { localisationService } from '@oneblink/apps';
3
3
  import { parseDateValue } from './generate-default-data';
4
4
  import generateCivicaNameRecordElements from './generateCivicaNameRecordElements';
5
5
  import generateFreshdeskDependentFieldElements from './generateFreshdeskDependentFieldElements';
6
+ import cleanFormSubmissionModel from './cleanFormSubmissionModel';
7
+ import getDateRangeConfiguration from './getDateRangeConfiguration';
6
8
  export const lookupValidationMessage = 'Lookup is required';
7
9
  // https://validatejs.org/#validators-datetime
8
10
  // Before using it we must add the parse and format functions
@@ -19,10 +21,12 @@ validate.extend(validate.validators.datetime, {
19
21
  return options.format(dateValue);
20
22
  },
21
23
  });
22
- validate.validators.entries = function (value, { setSchema, entrySchema, }) {
24
+ validate.validators.entries = function (value, { setSchema, entrySchema: { schema: entrySchema, formElementConditionallyShown }, }) {
23
25
  const entries = Array.isArray(value) ? value : [];
24
26
  const entryErrors = entries.reduce((errorsByIndex, entry, index) => {
25
- const entryValidation = validateSingleMessageError(entry, entrySchema);
27
+ const entryValidation = validateSubmission(entrySchema, entry, (formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.type) === 'repeatableSet'
28
+ ? formElementConditionallyShown.entries[index.toString()]
29
+ : undefined);
26
30
  if (entryValidation) {
27
31
  errorsByIndex[index] = entryValidation;
28
32
  }
@@ -38,8 +42,10 @@ validate.validators.entries = function (value, { setSchema, entrySchema, }) {
38
42
  entries: entryErrors,
39
43
  };
40
44
  };
41
- validate.validators.nestedElements = function (value, schema) {
42
- const errors = validateSingleMessageError(value || {}, schema);
45
+ validate.validators.nestedElements = function (value, { schema, formElementConditionallyShown }) {
46
+ const errors = validateSubmission(schema, value, (formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.type) === 'formElements'
47
+ ? formElementConditionallyShown.formElements
48
+ : undefined);
43
49
  if (!errors) {
44
50
  return;
45
51
  }
@@ -125,418 +131,378 @@ function getCustomRegexFormatConfig(formElement) {
125
131
  }
126
132
  : undefined;
127
133
  }
128
- export function validateSubmission(schema, submission, formElementsConditionallyShown) {
129
- const formElementsValidation = validateSingleMessageError(submission, schema);
130
- if (formElementsValidation) {
131
- clearValidationMessagesForHiddenElements(formElementsValidation, formElementsConditionallyShown);
132
- if (!validate.isEmpty(formElementsValidation)) {
133
- return formElementsValidation;
134
- }
135
- }
136
- }
137
- const clearValidationMessagesForHiddenElements = (formElementsValidation, formElementsConditionallyShown) => {
138
- // If there is no validation to check, there are no invalid elements
139
- // If there is no conditionally shown elements, all invalid elements should display validation messages,
140
- if (!formElementsValidation || !formElementsConditionallyShown) {
141
- return;
142
- }
143
- for (const key in formElementsValidation) {
144
- const formElementValidation = formElementsValidation[key];
145
- if (!formElementValidation) {
146
- continue;
147
- }
148
- const formElementConditionallyShown = formElementsConditionallyShown[key];
149
- // If the validation is for an element that is being hidden,
150
- // we can remove the validation message and move to the next validation
151
- if (formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.isHidden) {
152
- delete formElementsValidation[key];
153
- continue;
154
- }
155
- // If the validation is for a single element (not nested elements),
156
- // we will always show the validation message
157
- if (typeof formElementValidation === 'string') {
158
- continue;
159
- }
160
- // Here we will check to see if the nested elements that are
161
- // invalid are being shown, if not, remove validation messages
162
- switch (formElementValidation.type) {
163
- case 'repeatableSet': {
164
- for (const index in formElementValidation.entries) {
165
- clearValidationMessagesForHiddenElements(formElementValidation.entries[index], formElementConditionallyShown &&
166
- formElementConditionallyShown.type === 'repeatableSet'
167
- ? formElementConditionallyShown.entries[index]
168
- : undefined);
169
- if (validate.isEmpty(formElementValidation.entries[index])) {
170
- delete formElementValidation.entries[index];
171
- }
172
- }
173
- // Remove the validation if all entries are valid and the set is also valid
174
- if (validate.isEmpty(formElementValidation.entries) &&
175
- !formElementValidation.set) {
176
- delete formElementsValidation[key];
177
- }
178
- break;
179
- }
180
- case 'formElements': {
181
- clearValidationMessagesForHiddenElements(formElementValidation.formElements, (formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.type) === 'formElements'
182
- ? formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.formElements
183
- : undefined);
184
- if (validate.isEmpty(formElementValidation.formElements)) {
185
- delete formElementsValidation[key];
186
- }
187
- break;
188
- }
189
- }
190
- }
191
- };
192
134
  const presence = ({ required, requiredMessage }, message) => (required ? { message: requiredMessage || message } : false);
193
135
  const escapeElementName = (elementName) => {
194
136
  const escapedName = elementName.replace(/\./g, '\\.');
195
137
  return escapedName;
196
138
  };
139
+ function getCleanDateRangeConfiguration(options, elements, submission, formElementsConditionallyShown) {
140
+ if (options.referenceFormElementId && submission) {
141
+ const { model } = cleanFormSubmissionModel(submission, elements, formElementsConditionallyShown, true);
142
+ return getDateRangeConfiguration(options, elements, model);
143
+ }
144
+ return [options.date, options.daysOffset];
145
+ }
197
146
  export function generateValidationSchema(elements, elementIdsWithLookupsExecuted) {
198
147
  return elements.reduce((partialSchema, formElement) => {
199
- var _a;
200
148
  switch (formElement.type) {
149
+ // Elements that do not need to be validated
201
150
  case 'summary':
202
151
  case 'calculation':
203
152
  case 'image':
204
153
  case 'html':
205
154
  case 'infoPage':
206
155
  case 'heading': {
207
- break;
208
- }
209
- case 'section':
210
- case 'page': {
211
- const nestedSchema = generateValidationSchema(formElement.elements, elementIdsWithLookupsExecuted);
212
- Object.assign(partialSchema, nestedSchema);
213
- break;
214
- }
215
- case 'draw': {
216
- partialSchema[escapeElementName(formElement.name)] = {
217
- attachment: true,
218
- presence: presence(formElement, 'A saved signature is required'),
219
- };
220
- break;
221
- }
222
- case 'camera': {
223
- partialSchema[escapeElementName(formElement.name)] = {
224
- attachment: true,
225
- presence: presence(formElement, 'A photo is required'),
226
- };
227
- break;
228
- }
229
- case 'captcha': {
230
- partialSchema[escapeElementName(formElement.name)] = {
231
- presence: presence({ ...formElement, required: true }, 'Please complete the CAPTCHA successfully'),
232
- };
233
- break;
234
- }
235
- case 'location': {
236
- partialSchema[escapeElementName(formElement.name)] = {
237
- presence: presence(formElement, 'Please select a location'),
238
- lookups: {
239
- formElement,
240
- elementIdsWithLookupsExecuted,
241
- },
242
- };
243
- break;
244
- }
245
- case 'compliance': {
246
- partialSchema[escapeElementName(formElement.name)] = {
247
- presence: presence(formElement, 'Required'),
248
- lookups: {
249
- formElement,
250
- elementIdsWithLookupsExecuted,
251
- },
252
- attachments: true,
253
- };
254
- break;
255
- }
256
- case 'checkboxes': {
257
- const requiredAllDefaultMessage = 'All options are required';
258
- partialSchema[escapeElementName(formElement.name)] = {
259
- presence: presence({
260
- ...formElement,
261
- required: formElement.required || !!formElement.requiredAll,
262
- }, formElement.requiredAll ? requiredAllDefaultMessage : 'Required'),
263
- length: formElement.requiredAll
264
- ? {
265
- is: (_a = formElement.options) === null || _a === void 0 ? void 0 : _a.length,
266
- message: formElement.requiredMessage || requiredAllDefaultMessage,
267
- }
268
- : undefined,
269
- lookups: {
270
- formElement,
271
- elementIdsWithLookupsExecuted,
272
- },
273
- };
274
- break;
275
- }
276
- case 'abn':
277
- case 'geoscapeAddress':
278
- case 'pointAddress':
279
- case 'civicaStreetName':
280
- case 'autocomplete':
281
- case 'radio':
282
- case 'select': {
283
- partialSchema[escapeElementName(formElement.name)] = {
284
- presence: presence(formElement, 'Required'),
285
- lookups: {
286
- formElement,
287
- elementIdsWithLookupsExecuted,
288
- },
289
- };
290
- break;
291
- }
292
- case 'boolean': {
293
- partialSchema[escapeElementName(formElement.name)] = {
294
- isTrue: formElement.required && 'Required',
295
- lookups: {
296
- formElement,
297
- elementIdsWithLookupsExecuted,
298
- },
299
- };
300
- break;
156
+ return partialSchema;
301
157
  }
302
- case 'bsb': {
303
- partialSchema[escapeElementName(formElement.name)] = {
304
- presence: presence(formElement, 'Please enter a BSB number'),
305
- lookups: {
306
- formElement,
307
- elementIdsWithLookupsExecuted,
308
- },
309
- format: {
310
- pattern: /\d{3}-\d{3}/,
311
- message: 'Please enter a valid BSB number',
312
- },
313
- };
314
- break;
315
- }
316
- case 'barcodeScanner': {
317
- partialSchema[escapeElementName(formElement.name)] = {
318
- presence: presence(formElement, 'Please scan a barcode or enter a value'),
319
- lookups: {
320
- formElement,
321
- elementIdsWithLookupsExecuted,
322
- },
323
- format: getCustomRegexFormatConfig(formElement),
324
- };
325
- break;
326
- }
327
- case 'text':
328
- case 'textarea': {
329
- partialSchema[escapeElementName(formElement.name)] = {
330
- presence: presence(formElement, 'Please enter a value'),
331
- lookups: {
332
- formElement,
333
- elementIdsWithLookupsExecuted,
334
- },
335
- length: {
336
- minimum: formElement.minLength,
337
- tooShort: 'Please enter a value with at least %{count} character(s)',
338
- maximum: formElement.maxLength,
339
- tooLong: 'Please enter a value with %{count} character(s) or less',
340
- },
341
- format: getCustomRegexFormatConfig(formElement),
342
- };
343
- break;
344
- }
345
- case 'telephone': {
346
- partialSchema[escapeElementName(formElement.name)] = {
347
- presence: presence(formElement, 'Please enter a phone number'),
348
- lookups: {
349
- formElement,
350
- elementIdsWithLookupsExecuted,
351
- },
352
- format: getCustomRegexFormatConfig(formElement),
353
- };
354
- break;
355
- }
356
- case 'email': {
357
- partialSchema[escapeElementName(formElement.name)] = {
358
- presence: presence(formElement, 'Please enter an email address'),
359
- email: {
360
- message: 'Please enter a valid email for this field',
361
- },
362
- lookups: {
363
- formElement,
364
- elementIdsWithLookupsExecuted,
365
- },
366
- format: getCustomRegexFormatConfig(formElement),
367
- };
368
- break;
369
- }
370
- case 'time': {
371
- partialSchema[escapeElementName(formElement.name)] = {
372
- presence: presence(formElement, 'Please select a time'),
373
- lookups: {
374
- formElement,
375
- elementIdsWithLookupsExecuted,
376
- },
377
- };
378
- break;
379
- }
380
- case 'date': {
381
- partialSchema[escapeElementName(formElement.name)] = {
382
- presence: presence(formElement, 'Please select a date'),
383
- date: {
384
- format: (v) => localisationService.formatDate(v),
385
- earliest: parseDateValue({
386
- dateOnly: true,
387
- daysOffset: formElement.fromDateDaysOffset,
388
- value: formElement.fromDate,
389
- }),
390
- latest: parseDateValue({
391
- dateOnly: true,
392
- daysOffset: formElement.toDateDaysOffset,
393
- value: formElement.toDate,
394
- }),
395
- notValid: 'Please select a valid date',
396
- tooEarly: 'Date cannot be before %{date}',
397
- tooLate: 'Date cannot be after %{date}',
398
- },
399
- lookups: {
400
- formElement,
401
- elementIdsWithLookupsExecuted,
402
- },
403
- };
404
- break;
405
- }
406
- case 'datetime': {
407
- partialSchema[escapeElementName(formElement.name)] = {
408
- presence: presence(formElement, 'Please select a date and time'),
409
- datetime: {
410
- format: (v) => localisationService.formatDatetime(v),
411
- earliest: parseDateValue({
412
- dateOnly: false,
413
- daysOffset: formElement.fromDateDaysOffset,
414
- value: formElement.fromDate,
415
- }),
416
- latest: parseDateValue({
417
- dateOnly: false,
418
- daysOffset: formElement.toDateDaysOffset,
419
- value: formElement.toDate,
420
- }),
421
- notValid: 'Please select a valid date and time',
422
- tooEarly: 'Date and time cannot be before %{date}',
423
- tooLate: 'Date and time cannot be after %{date}',
424
- },
425
- lookups: {
426
- formElement,
427
- elementIdsWithLookupsExecuted,
428
- },
429
- };
430
- break;
158
+ }
159
+ const constraint = (value, submission, propertyName, { formElementsConditionallyShown }) => {
160
+ var _a;
161
+ // If the element is current hidden, we do not need to apply validation
162
+ const formElementConditionallyShown = formElementsConditionallyShown === null || formElementsConditionallyShown === void 0 ? void 0 : formElementsConditionallyShown[formElement.name];
163
+ if (formElementConditionallyShown === null || formElementConditionallyShown === void 0 ? void 0 : formElementConditionallyShown.isHidden) {
164
+ return;
431
165
  }
432
- case 'number': {
433
- let minErrorMessage = 'Please enter a number greater than or equal to %{count}';
434
- let maxErrorMessage = 'Please enter a number less than or equal to %{count}';
435
- if (typeof formElement.minNumber === 'number' &&
436
- typeof formElement.maxNumber === 'number') {
437
- minErrorMessage =
438
- maxErrorMessage = `Please enter a number between ${formElement.minNumber} and ${formElement.maxNumber}`;
166
+ switch (formElement.type) {
167
+ case 'draw': {
168
+ return {
169
+ attachment: true,
170
+ presence: presence(formElement, 'A saved signature is required'),
171
+ };
439
172
  }
440
- partialSchema[escapeElementName(formElement.name)] = {
441
- type: 'number',
442
- presence: presence(formElement, 'Please enter a number'),
443
- numericality: {
444
- greaterThanOrEqualTo: formElement.minNumber,
445
- notGreaterThanOrEqualTo: minErrorMessage,
446
- lessThanOrEqualTo: formElement.maxNumber,
447
- notLessThanOrEqualTo: maxErrorMessage,
448
- onlyInteger: formElement.isInteger,
449
- notInteger: 'Please enter a whole number',
450
- },
451
- lookups: {
452
- formElement,
453
- elementIdsWithLookupsExecuted,
454
- },
455
- numberRegex: getCustomRegexFormatConfig(formElement),
456
- };
457
- break;
458
- }
459
- case 'files': {
460
- partialSchema[escapeElementName(formElement.name)] = {
461
- presence: formElement.minEntries
462
- ? {
463
- message: `Please upload at least ${formElement.minEntries} file${formElement.minEntries === 1 ? '' : 's'}`,
464
- }
465
- : false,
466
- length: {
467
- minimum: formElement.minEntries,
468
- maximum: formElement.maxEntries,
469
- tooLong: 'Cannot upload more than %{count} file(s)',
470
- tooShort: 'Please upload at least %{count} file(s)',
471
- },
472
- type: {
473
- type: (files) => {
474
- return (!Array.isArray(files) ||
475
- files.every((file) => {
476
- return checkFileNameIsValid(formElement, file.fileName);
477
- }));
173
+ case 'camera': {
174
+ return {
175
+ attachment: true,
176
+ presence: presence(formElement, 'A photo is required'),
177
+ };
178
+ }
179
+ case 'captcha': {
180
+ return {
181
+ presence: presence({ ...formElement, required: true }, 'Please complete the CAPTCHA successfully'),
182
+ };
183
+ }
184
+ case 'location': {
185
+ return {
186
+ presence: presence(formElement, 'Please select a location'),
187
+ lookups: {
188
+ formElement,
189
+ elementIdsWithLookupsExecuted,
478
190
  },
479
- message: `Only the following file types are accepted: ${(formElement.restrictedFileTypes || []).join(', ')}`,
480
- },
481
- needsExtension: formElement,
482
- attachments: true,
483
- };
484
- break;
485
- }
486
- case 'repeatableSet': {
487
- partialSchema[escapeElementName(formElement.name)] = {
488
- entries: {
489
- setSchema: {
490
- presence: formElement.minSetEntries
491
- ? {
492
- message: `Must have at least ${formElement.minSetEntries} ${formElement.minSetEntries === 1 ? 'entry' : 'entries'}`,
493
- }
494
- : false,
495
- length: {
496
- minimum: formElement.minSetEntries,
497
- maximum: formElement.maxSetEntries,
498
- tooLong: 'Cannot have more than %{count} entry/entries',
499
- tooShort: 'Must have at least %{count} entry/entries',
191
+ };
192
+ }
193
+ case 'compliance': {
194
+ return {
195
+ presence: presence(formElement, 'Required'),
196
+ lookups: {
197
+ formElement,
198
+ elementIdsWithLookupsExecuted,
199
+ },
200
+ attachments: true,
201
+ };
202
+ }
203
+ case 'checkboxes': {
204
+ const requiredAllDefaultMessage = 'All options are required';
205
+ return {
206
+ presence: presence({
207
+ ...formElement,
208
+ required: formElement.required || !!formElement.requiredAll,
209
+ }, formElement.requiredAll ? requiredAllDefaultMessage : 'Required'),
210
+ length: formElement.requiredAll
211
+ ? {
212
+ is: (_a = formElement.options) === null || _a === void 0 ? void 0 : _a.length,
213
+ message: formElement.requiredMessage || requiredAllDefaultMessage,
214
+ }
215
+ : undefined,
216
+ lookups: {
217
+ formElement,
218
+ elementIdsWithLookupsExecuted,
219
+ },
220
+ };
221
+ }
222
+ case 'abn':
223
+ case 'geoscapeAddress':
224
+ case 'pointAddress':
225
+ case 'civicaStreetName':
226
+ case 'autocomplete':
227
+ case 'radio':
228
+ case 'select': {
229
+ return {
230
+ presence: presence(formElement, 'Required'),
231
+ lookups: {
232
+ formElement,
233
+ elementIdsWithLookupsExecuted,
234
+ },
235
+ };
236
+ }
237
+ case 'boolean': {
238
+ return {
239
+ isTrue: formElement.required && 'Required',
240
+ lookups: {
241
+ formElement,
242
+ elementIdsWithLookupsExecuted,
243
+ },
244
+ };
245
+ }
246
+ case 'bsb': {
247
+ return {
248
+ presence: presence(formElement, 'Please enter a BSB number'),
249
+ lookups: {
250
+ formElement,
251
+ elementIdsWithLookupsExecuted,
252
+ },
253
+ format: {
254
+ pattern: /\d{3}-\d{3}/,
255
+ message: 'Please enter a valid BSB number',
256
+ },
257
+ };
258
+ }
259
+ case 'barcodeScanner': {
260
+ return {
261
+ presence: presence(formElement, 'Please scan a barcode or enter a value'),
262
+ lookups: {
263
+ formElement,
264
+ elementIdsWithLookupsExecuted,
265
+ },
266
+ format: getCustomRegexFormatConfig(formElement),
267
+ };
268
+ }
269
+ case 'text':
270
+ case 'textarea': {
271
+ return {
272
+ presence: presence(formElement, 'Please enter a value'),
273
+ lookups: {
274
+ formElement,
275
+ elementIdsWithLookupsExecuted,
276
+ },
277
+ length: {
278
+ minimum: formElement.minLength,
279
+ tooShort: 'Please enter a value with at least %{count} character(s)',
280
+ maximum: formElement.maxLength,
281
+ tooLong: 'Please enter a value with %{count} character(s) or less',
282
+ },
283
+ format: getCustomRegexFormatConfig(formElement),
284
+ };
285
+ }
286
+ case 'telephone': {
287
+ return {
288
+ presence: presence(formElement, 'Please enter a phone number'),
289
+ lookups: {
290
+ formElement,
291
+ elementIdsWithLookupsExecuted,
292
+ },
293
+ format: getCustomRegexFormatConfig(formElement),
294
+ };
295
+ }
296
+ case 'email': {
297
+ return {
298
+ presence: presence(formElement, 'Please enter an email address'),
299
+ email: {
300
+ message: 'Please enter a valid email for this field',
301
+ },
302
+ lookups: {
303
+ formElement,
304
+ elementIdsWithLookupsExecuted,
305
+ },
306
+ format: getCustomRegexFormatConfig(formElement),
307
+ };
308
+ }
309
+ case 'time': {
310
+ return {
311
+ presence: presence(formElement, 'Please select a time'),
312
+ lookups: {
313
+ formElement,
314
+ elementIdsWithLookupsExecuted,
315
+ },
316
+ };
317
+ }
318
+ case 'date': {
319
+ const [fromDate, fromDateDaysOffset] = getCleanDateRangeConfiguration({
320
+ date: formElement.fromDate,
321
+ daysOffset: formElement.fromDateDaysOffset,
322
+ referenceFormElementId: formElement.fromDateElementId,
323
+ }, elements, submission, formElementsConditionallyShown);
324
+ const [toDate, toDateDaysOffset] = getCleanDateRangeConfiguration({
325
+ date: formElement.toDate,
326
+ daysOffset: formElement.toDateDaysOffset,
327
+ referenceFormElementId: formElement.toDateElementId,
328
+ }, elements, submission, formElementsConditionallyShown);
329
+ return {
330
+ presence: presence(formElement, 'Please select a date'),
331
+ date: {
332
+ format: (v) => localisationService.formatDate(v),
333
+ earliest: parseDateValue({
334
+ dateOnly: true,
335
+ daysOffset: fromDateDaysOffset,
336
+ value: fromDate,
337
+ }),
338
+ latest: parseDateValue({
339
+ dateOnly: true,
340
+ daysOffset: toDateDaysOffset,
341
+ value: toDate,
342
+ }),
343
+ notValid: 'Please select a valid date',
344
+ tooEarly: 'Date cannot be before %{date}',
345
+ tooLate: 'Date cannot be after %{date}',
346
+ },
347
+ lookups: {
348
+ formElement,
349
+ elementIdsWithLookupsExecuted,
350
+ },
351
+ };
352
+ }
353
+ case 'datetime': {
354
+ const [fromDate, fromDateDaysOffset] = getCleanDateRangeConfiguration({
355
+ date: formElement.fromDate,
356
+ daysOffset: formElement.fromDateDaysOffset,
357
+ referenceFormElementId: formElement.fromDateElementId,
358
+ }, elements, submission, formElementsConditionallyShown);
359
+ const [toDate, toDateDaysOffset] = getCleanDateRangeConfiguration({
360
+ date: formElement.toDate,
361
+ daysOffset: formElement.toDateDaysOffset,
362
+ referenceFormElementId: formElement.toDateElementId,
363
+ }, elements, submission, formElementsConditionallyShown);
364
+ return {
365
+ presence: presence(formElement, 'Please select a date and time'),
366
+ datetime: {
367
+ format: (v) => localisationService.formatDatetime(v),
368
+ earliest: parseDateValue({
369
+ dateOnly: false,
370
+ daysOffset: fromDateDaysOffset,
371
+ value: fromDate,
372
+ }),
373
+ latest: parseDateValue({
374
+ dateOnly: false,
375
+ daysOffset: toDateDaysOffset,
376
+ value: toDate,
377
+ }),
378
+ notValid: 'Please select a valid date and time',
379
+ tooEarly: 'Date and time cannot be before %{date}',
380
+ tooLate: 'Date and time cannot be after %{date}',
381
+ },
382
+ lookups: {
383
+ formElement,
384
+ elementIdsWithLookupsExecuted,
385
+ },
386
+ };
387
+ }
388
+ case 'number': {
389
+ let minErrorMessage = 'Please enter a number greater than or equal to %{count}';
390
+ let maxErrorMessage = 'Please enter a number less than or equal to %{count}';
391
+ if (typeof formElement.minNumber === 'number' &&
392
+ typeof formElement.maxNumber === 'number') {
393
+ minErrorMessage =
394
+ maxErrorMessage = `Please enter a number between ${formElement.minNumber} and ${formElement.maxNumber}`;
395
+ }
396
+ return {
397
+ type: 'number',
398
+ presence: presence(formElement, 'Please enter a number'),
399
+ numericality: {
400
+ greaterThanOrEqualTo: formElement.minNumber,
401
+ notGreaterThanOrEqualTo: minErrorMessage,
402
+ lessThanOrEqualTo: formElement.maxNumber,
403
+ notLessThanOrEqualTo: maxErrorMessage,
404
+ onlyInteger: formElement.isInteger,
405
+ notInteger: 'Please enter a whole number',
406
+ },
407
+ lookups: {
408
+ formElement,
409
+ elementIdsWithLookupsExecuted,
410
+ },
411
+ numberRegex: getCustomRegexFormatConfig(formElement),
412
+ };
413
+ }
414
+ case 'files': {
415
+ return {
416
+ presence: formElement.minEntries
417
+ ? {
418
+ message: `Please upload at least ${formElement.minEntries} file${formElement.minEntries === 1 ? '' : 's'}`,
419
+ }
420
+ : false,
421
+ length: {
422
+ minimum: formElement.minEntries,
423
+ maximum: formElement.maxEntries,
424
+ tooLong: 'Cannot upload more than %{count} file(s)',
425
+ tooShort: 'Please upload at least %{count} file(s)',
426
+ },
427
+ type: {
428
+ type: (files) => {
429
+ return (!Array.isArray(files) ||
430
+ files.every((file) => {
431
+ return checkFileNameIsValid(formElement, file.fileName);
432
+ }));
500
433
  },
434
+ message: `Only the following file types are accepted: ${(formElement.restrictedFileTypes || []).join(', ')}`,
501
435
  },
502
- entrySchema: generateValidationSchema(formElement.elements, elementIdsWithLookupsExecuted),
503
- },
504
- };
505
- break;
506
- }
507
- case 'civicaNameRecord': {
508
- const nestedElements = generateCivicaNameRecordElements(formElement, []);
509
- partialSchema[escapeElementName(formElement.name)] = {
510
- nestedElements: generateValidationSchema(nestedElements, elementIdsWithLookupsExecuted),
511
- };
512
- break;
513
- }
514
- case 'form': {
515
- if (formElement.elements) {
516
- partialSchema[escapeElementName(formElement.name)] = {
517
- nestedElements: generateValidationSchema(formElement.elements, elementIdsWithLookupsExecuted),
436
+ needsExtension: formElement,
437
+ attachments: true,
518
438
  };
519
439
  }
520
- break;
521
- }
522
- case 'freshdeskDependentField': {
523
- const nestedElements = generateFreshdeskDependentFieldElements(formElement, undefined);
524
- partialSchema[escapeElementName(formElement.name)] = {
525
- nestedElements: generateValidationSchema(nestedElements, elementIdsWithLookupsExecuted),
526
- };
527
- break;
528
- }
529
- default: {
530
- console.info('Unsupported form element with validation', formElement);
440
+ case 'repeatableSet': {
441
+ return {
442
+ entries: {
443
+ setSchema: {
444
+ presence: formElement.minSetEntries
445
+ ? {
446
+ message: `Must have at least ${formElement.minSetEntries} ${formElement.minSetEntries === 1 ? 'entry' : 'entries'}`,
447
+ }
448
+ : false,
449
+ length: {
450
+ minimum: formElement.minSetEntries,
451
+ maximum: formElement.maxSetEntries,
452
+ tooLong: 'Cannot have more than %{count} entry/entries',
453
+ tooShort: 'Must have at least %{count} entry/entries',
454
+ },
455
+ },
456
+ entrySchema: {
457
+ schema: generateValidationSchema(formElement.elements, elementIdsWithLookupsExecuted),
458
+ formElementConditionallyShown: formElementsConditionallyShown === null || formElementsConditionallyShown === void 0 ? void 0 : formElementsConditionallyShown[formElement.name],
459
+ },
460
+ },
461
+ };
462
+ }
463
+ case 'civicaNameRecord': {
464
+ const nestedElements = generateCivicaNameRecordElements(formElement, []);
465
+ return {
466
+ nestedElements: {
467
+ schema: generateValidationSchema(nestedElements, elementIdsWithLookupsExecuted),
468
+ formElementConditionallyShown: formElementsConditionallyShown === null || formElementsConditionallyShown === void 0 ? void 0 : formElementsConditionallyShown[formElement.name],
469
+ },
470
+ };
471
+ }
472
+ case 'form': {
473
+ if (formElement.elements) {
474
+ return {
475
+ nestedElements: {
476
+ schema: generateValidationSchema(formElement.elements, elementIdsWithLookupsExecuted),
477
+ formElementConditionallyShown: formElementsConditionallyShown === null || formElementsConditionallyShown === void 0 ? void 0 : formElementsConditionallyShown[formElement.name],
478
+ },
479
+ };
480
+ }
481
+ break;
482
+ }
483
+ case 'freshdeskDependentField': {
484
+ const nestedElements = generateFreshdeskDependentFieldElements(formElement, undefined);
485
+ return {
486
+ nestedElements: {
487
+ schema: generateValidationSchema(nestedElements, elementIdsWithLookupsExecuted),
488
+ formElementConditionallyShown: formElementsConditionallyShown === null || formElementsConditionallyShown === void 0 ? void 0 : formElementsConditionallyShown[formElement.name],
489
+ },
490
+ };
491
+ }
492
+ default: {
493
+ console.info('Unsupported form element with validation', formElement);
494
+ }
531
495
  }
532
- }
496
+ };
497
+ partialSchema[escapeElementName(formElement.name)] = constraint;
533
498
  return partialSchema;
534
499
  }, {});
535
500
  }
536
- const validateSingleMessageError = (submission, schema) => {
501
+ export function validateSubmission(schema, submission, formElementsConditionallyShown) {
537
502
  const errorsAsArray = validate(submission, schema, {
538
503
  format: 'grouped',
539
504
  fullMessages: false,
505
+ formElementsConditionallyShown,
540
506
  });
541
507
  if (!errorsAsArray || validate.isEmpty(errorsAsArray)) {
542
508
  return;
@@ -552,7 +518,7 @@ const validateSingleMessageError = (submission, schema) => {
552
518
  return;
553
519
  }
554
520
  return errors;
555
- };
521
+ }
556
522
  export function checkFileNameIsValid(formElement, fileName) {
557
523
  const extension = fileName.split('.').pop();
558
524
  return (!formElement.restrictedFileTypes ||