@remoteoss/json-schema-form 0.4.1-dev.20230703203242 → 0.4.3-beta.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ #### 0.4.3-beta.0 (2023-08-09)
2
+
3
+ ##### Bug fixes
4
+
5
+ * **conditions:** Validate a deeply nested if (e.g. checking an object with a number property) in an if property now doesn't break the form. ([#33](https://github.com/remoteoss/json-schema-form/pull/33)) ([e34cfcc](https://github.com/remoteoss/json-schema-form/commit/e34cfccaf45f1460b346f3cff0c797b3d11259e3))
6
+
7
+ #### 0.4.2-beta.0 (2023-07-20)
8
+
9
+ ##### Bug Fixes
10
+
11
+ * **date:** Validate based on minDate and maxDate ([#30](https://github.com/remoteoss/json-schema-form/pull/30)) ([01c0143e](https://github.com/remoteoss/json-schema-form/commit/01c0143ea4a3775f9489ae6cb8fd99a90b3f1394))
12
+
13
+ #### 0.4.1-beta.0 (2023-07-03)
14
+
15
+ ##### Bug Fixes
16
+
17
+ * **fieldset:** support root conditionals for fieldsets ([#23](https://github.com/remoteoss/json-schema-form/pull/23)) ([65d87b3a](https://github.com/remoteoss/json-schema-form/commit/65d87b3a93018f0729aed565000eb2a2ce1f2ce7))
18
+ * **select/radio:** Accept just the values in options (plus `''` and `null` for backward-compatibility) ([#18](https://github.com/remoteoss/json-schema-form/pull/18)) ([37501d2d](https://github.com/remoteoss/json-schema-form/commit/37501d2ddafdd5e207b34d2ca3f6b7b7a1006e9d))
19
+
1
20
  #### 0.4.0-beta.0 (2023-06-22)
2
21
 
3
22
  ##### New Features
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2023 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.4.1-dev.20230703203242
5
- Generated: Mon, 03 Jul 2023 20:32:50 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.4.3-beta.0
5
+ Generated: Wed, 09 Aug 2023 08:39:30 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -84,6 +84,69 @@ var import_omitBy = __toESM(require("lodash/omitBy"));
84
84
  var import_set = __toESM(require("lodash/set"));
85
85
  var import_yup2 = require("yup");
86
86
 
87
+ // src/utils.js
88
+ function convertDiskSizeFromTo(from, to) {
89
+ const units = ["bytes", "kb", "mb"];
90
+ return function convert(value) {
91
+ return value * Math.pow(1024, units.indexOf(from.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
92
+ };
93
+ }
94
+ function containsHTML(str = "") {
95
+ return /<[a-z][\s\S]*>/i.test(str);
96
+ }
97
+ function wrapWithSpan(html, properties = {}) {
98
+ const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
99
+ return `<span ${attributes}>${html}</span>`;
100
+ }
101
+ function hasProperty(object2, propertyName) {
102
+ return Object.prototype.hasOwnProperty.call(object2, propertyName);
103
+ }
104
+
105
+ // src/checkIfConditionMatches.js
106
+ function checkIfConditionMatches(node, formValues, formFields) {
107
+ return Object.keys(node.if.properties).every((name) => {
108
+ const currentProperty = node.if.properties[name];
109
+ const value = formValues[name];
110
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
111
+ value === null;
112
+ const hasIfExplicit = node.if.required?.includes(name);
113
+ if (hasEmptyValue && !hasIfExplicit) {
114
+ return true;
115
+ }
116
+ if (hasProperty(currentProperty, "const")) {
117
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
118
+ }
119
+ if (currentProperty.contains?.pattern) {
120
+ const formValue = value || [];
121
+ if (Array.isArray(formValue)) {
122
+ const pattern = new RegExp(currentProperty.contains.pattern);
123
+ return (value || []).some((item) => pattern.test(item));
124
+ }
125
+ }
126
+ if (currentProperty.enum) {
127
+ return currentProperty.enum.includes(value);
128
+ }
129
+ if (currentProperty.properties) {
130
+ return checkIfConditionMatches(
131
+ { if: currentProperty },
132
+ formValues[name],
133
+ getField(name, formFields).fields
134
+ );
135
+ }
136
+ const field = getField(name, formFields);
137
+ return validateFieldSchema(
138
+ {
139
+ options: field.options,
140
+ // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
141
+ ...currentProperty,
142
+ inputType: field.inputType,
143
+ required: true
144
+ },
145
+ value
146
+ );
147
+ });
148
+ }
149
+
87
150
  // src/internals/helpers.js
88
151
  var import_merge = __toESM(require("lodash/fp/merge"));
89
152
  var import_get = __toESM(require("lodash/get"));
@@ -343,24 +406,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
343
406
  };
344
407
  }
345
408
 
346
- // src/utils.js
347
- function convertDiskSizeFromTo(from, to) {
348
- const units = ["bytes", "kb", "mb"];
349
- return function convert(value) {
350
- return value * Math.pow(1024, units.indexOf(from.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
351
- };
352
- }
353
- function containsHTML(str = "") {
354
- return /<[a-z][\s\S]*>/i.test(str);
355
- }
356
- function wrapWithSpan(html, properties = {}) {
357
- const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
358
- return `<span ${attributes}>${html}</span>`;
359
- }
360
- function hasProperty(object2, propertyName) {
361
- return Object.prototype.hasOwnProperty.call(object2, propertyName);
362
- }
363
-
364
409
  // src/yupSchema.js
365
410
  var import_flow = __toESM(require("lodash/flow"));
366
411
  var import_noop = __toESM(require("lodash/noop"));
@@ -381,6 +426,25 @@ var validateOnlyStrings = (0, import_yup.string)().trim().nullable().test(
381
426
  return true;
382
427
  }
383
428
  );
429
+ var compareDates = (d1, d2) => {
430
+ let date1 = new Date(d1).getTime();
431
+ let date2 = new Date(d2).getTime();
432
+ if (date1 < date2) {
433
+ return "LESSER";
434
+ } else if (date1 > date2) {
435
+ return "GREATER";
436
+ } else {
437
+ return "EQUAL";
438
+ }
439
+ };
440
+ var validateMinDate = (value, minDate) => {
441
+ const compare = compareDates(value, minDate);
442
+ return compare === "GREATER" || compare === "EQUAL" ? true : false;
443
+ };
444
+ var validateMaxDate = (value, minDate) => {
445
+ const compare = compareDates(value, minDate);
446
+ return compare === "LESSER" || compare === "EQUAL" ? true : false;
447
+ };
384
448
  var yupSchemas = {
385
449
  text: validateOnlyStrings,
386
450
  radioOrSelect: (options) => (0, import_yup.string)().nullable().transform((value) => {
@@ -394,10 +458,32 @@ var yupSchemas = {
394
458
  }).oneOf(options, ({ value }) => {
395
459
  return `The option ${JSON.stringify(value)} is not valid.`;
396
460
  }),
397
- date: (0, import_yup.string)().nullable().trim().matches(
398
- /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
399
- `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
400
- ),
461
+ date: ({ minDate, maxDate }) => {
462
+ let dateString = (0, import_yup.string)().nullable().transform((value) => {
463
+ if (value === "") {
464
+ return void 0;
465
+ }
466
+ return value === null ? void 0 : value;
467
+ }).trim().matches(
468
+ /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
469
+ `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
470
+ );
471
+ if (minDate) {
472
+ dateString = dateString.test(
473
+ "minDate",
474
+ `The date must be ${minDate} or after.`,
475
+ (value) => validateMinDate(value, minDate)
476
+ );
477
+ }
478
+ if (maxDate) {
479
+ dateString = dateString.test(
480
+ "maxDate",
481
+ `The date must be ${maxDate} or before.`,
482
+ (value) => validateMaxDate(value, maxDate)
483
+ );
484
+ }
485
+ return dateString;
486
+ },
401
487
  number: (0, import_yup.number)().typeError("The value must be a number").nullable(),
402
488
  file: (0, import_yup.array)().nullable(),
403
489
  email: (0, import_yup.string)().trim().email("Please enter a valid email address").nullable(),
@@ -442,6 +528,9 @@ var getYupSchema = ({ inputType, ...field }) => {
442
528
  const optionValues = getOptions(field);
443
529
  return yupSchemas.radioOrSelect(optionValues);
444
530
  }
531
+ if (field.format === "date") {
532
+ return yupSchemas.date({ minDate: field.minDate, maxDate: field.maxDate });
533
+ }
445
534
  return yupSchemas[inputType] || yupSchemasToJsonTypes[jsonType];
446
535
  };
447
536
  function buildYupSchema(field, config) {
@@ -631,42 +720,6 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) {
631
720
  const currentPropertyValue = typeof schemaValue === "number" ? schemaValue : schemaValue || void 0;
632
721
  return String(formValue) === String(currentPropertyValue);
633
722
  }
634
- function checkIfConditionMatches(node, formValues, formFields) {
635
- return Object.keys(node.if.properties).every((name) => {
636
- const currentProperty = node.if.properties[name];
637
- const value = formValues[name];
638
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
639
- value === null;
640
- const hasIfExplicit = node.if.required?.includes(name);
641
- if (hasEmptyValue && !hasIfExplicit) {
642
- return true;
643
- }
644
- if (hasProperty(currentProperty, "const")) {
645
- return compareFormValueWithSchemaValue(value, currentProperty.const);
646
- }
647
- if (currentProperty.contains?.pattern) {
648
- const formValue = value || [];
649
- if (Array.isArray(formValue)) {
650
- const pattern = new RegExp(currentProperty.contains.pattern);
651
- return (value || []).some((item) => pattern.test(item));
652
- }
653
- }
654
- if (currentProperty.enum) {
655
- return currentProperty.enum.includes(value);
656
- }
657
- const field = getField(name, formFields);
658
- return validateFieldSchema(
659
- {
660
- options: field.options,
661
- // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
662
- ...currentProperty,
663
- inputType: field.inputType,
664
- required: true
665
- },
666
- value
667
- );
668
- });
669
- }
670
723
  function isFieldFilled(fieldValue) {
671
724
  return Array.isArray(fieldValue) ? fieldValue.length > 0 : !!fieldValue;
672
725
  }
@@ -901,6 +954,7 @@ function extractParametersFromNode(schemaNode) {
901
954
  maxFileSize: node.maxFileSize,
902
955
  // @deprecated in favor of presentation.maxFileSize
903
956
  default: node.default,
957
+ format: node.format,
904
958
  // Checkboxes conditions
905
959
  // — For checkboxes that only accept one value (string)
906
960
  ...presentation?.inputType === "checkbox" && { checkboxValue: node.const },
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2023 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.4.1-dev.20230703203242
5
- Generated: Mon, 03 Jul 2023 20:32:50 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.4.3-beta.0
5
+ Generated: Wed, 09 Aug 2023 08:39:30 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -48,6 +48,69 @@ import omitBy from "lodash/omitBy";
48
48
  import set from "lodash/set";
49
49
  import { lazy } from "yup";
50
50
 
51
+ // src/utils.js
52
+ function convertDiskSizeFromTo(from, to) {
53
+ const units = ["bytes", "kb", "mb"];
54
+ return function convert(value) {
55
+ return value * Math.pow(1024, units.indexOf(from.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
56
+ };
57
+ }
58
+ function containsHTML(str = "") {
59
+ return /<[a-z][\s\S]*>/i.test(str);
60
+ }
61
+ function wrapWithSpan(html, properties = {}) {
62
+ const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
63
+ return `<span ${attributes}>${html}</span>`;
64
+ }
65
+ function hasProperty(object2, propertyName) {
66
+ return Object.prototype.hasOwnProperty.call(object2, propertyName);
67
+ }
68
+
69
+ // src/checkIfConditionMatches.js
70
+ function checkIfConditionMatches(node, formValues, formFields) {
71
+ return Object.keys(node.if.properties).every((name) => {
72
+ const currentProperty = node.if.properties[name];
73
+ const value = formValues[name];
74
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
75
+ value === null;
76
+ const hasIfExplicit = node.if.required?.includes(name);
77
+ if (hasEmptyValue && !hasIfExplicit) {
78
+ return true;
79
+ }
80
+ if (hasProperty(currentProperty, "const")) {
81
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
82
+ }
83
+ if (currentProperty.contains?.pattern) {
84
+ const formValue = value || [];
85
+ if (Array.isArray(formValue)) {
86
+ const pattern = new RegExp(currentProperty.contains.pattern);
87
+ return (value || []).some((item) => pattern.test(item));
88
+ }
89
+ }
90
+ if (currentProperty.enum) {
91
+ return currentProperty.enum.includes(value);
92
+ }
93
+ if (currentProperty.properties) {
94
+ return checkIfConditionMatches(
95
+ { if: currentProperty },
96
+ formValues[name],
97
+ getField(name, formFields).fields
98
+ );
99
+ }
100
+ const field = getField(name, formFields);
101
+ return validateFieldSchema(
102
+ {
103
+ options: field.options,
104
+ // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
105
+ ...currentProperty,
106
+ inputType: field.inputType,
107
+ required: true
108
+ },
109
+ value
110
+ );
111
+ });
112
+ }
113
+
51
114
  // src/internals/helpers.js
52
115
  import merge from "lodash/fp/merge";
53
116
  import get from "lodash/get";
@@ -307,24 +370,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
307
370
  };
308
371
  }
309
372
 
310
- // src/utils.js
311
- function convertDiskSizeFromTo(from, to) {
312
- const units = ["bytes", "kb", "mb"];
313
- return function convert(value) {
314
- return value * Math.pow(1024, units.indexOf(from.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
315
- };
316
- }
317
- function containsHTML(str = "") {
318
- return /<[a-z][\s\S]*>/i.test(str);
319
- }
320
- function wrapWithSpan(html, properties = {}) {
321
- const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
322
- return `<span ${attributes}>${html}</span>`;
323
- }
324
- function hasProperty(object2, propertyName) {
325
- return Object.prototype.hasOwnProperty.call(object2, propertyName);
326
- }
327
-
328
373
  // src/yupSchema.js
329
374
  import flow from "lodash/flow";
330
375
  import noop from "lodash/noop";
@@ -345,6 +390,25 @@ var validateOnlyStrings = string().trim().nullable().test(
345
390
  return true;
346
391
  }
347
392
  );
393
+ var compareDates = (d1, d2) => {
394
+ let date1 = new Date(d1).getTime();
395
+ let date2 = new Date(d2).getTime();
396
+ if (date1 < date2) {
397
+ return "LESSER";
398
+ } else if (date1 > date2) {
399
+ return "GREATER";
400
+ } else {
401
+ return "EQUAL";
402
+ }
403
+ };
404
+ var validateMinDate = (value, minDate) => {
405
+ const compare = compareDates(value, minDate);
406
+ return compare === "GREATER" || compare === "EQUAL" ? true : false;
407
+ };
408
+ var validateMaxDate = (value, minDate) => {
409
+ const compare = compareDates(value, minDate);
410
+ return compare === "LESSER" || compare === "EQUAL" ? true : false;
411
+ };
348
412
  var yupSchemas = {
349
413
  text: validateOnlyStrings,
350
414
  radioOrSelect: (options) => string().nullable().transform((value) => {
@@ -358,10 +422,32 @@ var yupSchemas = {
358
422
  }).oneOf(options, ({ value }) => {
359
423
  return `The option ${JSON.stringify(value)} is not valid.`;
360
424
  }),
361
- date: string().nullable().trim().matches(
362
- /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
363
- `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
364
- ),
425
+ date: ({ minDate, maxDate }) => {
426
+ let dateString = string().nullable().transform((value) => {
427
+ if (value === "") {
428
+ return void 0;
429
+ }
430
+ return value === null ? void 0 : value;
431
+ }).trim().matches(
432
+ /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
433
+ `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
434
+ );
435
+ if (minDate) {
436
+ dateString = dateString.test(
437
+ "minDate",
438
+ `The date must be ${minDate} or after.`,
439
+ (value) => validateMinDate(value, minDate)
440
+ );
441
+ }
442
+ if (maxDate) {
443
+ dateString = dateString.test(
444
+ "maxDate",
445
+ `The date must be ${maxDate} or before.`,
446
+ (value) => validateMaxDate(value, maxDate)
447
+ );
448
+ }
449
+ return dateString;
450
+ },
365
451
  number: number().typeError("The value must be a number").nullable(),
366
452
  file: array().nullable(),
367
453
  email: string().trim().email("Please enter a valid email address").nullable(),
@@ -406,6 +492,9 @@ var getYupSchema = ({ inputType, ...field }) => {
406
492
  const optionValues = getOptions(field);
407
493
  return yupSchemas.radioOrSelect(optionValues);
408
494
  }
495
+ if (field.format === "date") {
496
+ return yupSchemas.date({ minDate: field.minDate, maxDate: field.maxDate });
497
+ }
409
498
  return yupSchemas[inputType] || yupSchemasToJsonTypes[jsonType];
410
499
  };
411
500
  function buildYupSchema(field, config) {
@@ -595,42 +684,6 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) {
595
684
  const currentPropertyValue = typeof schemaValue === "number" ? schemaValue : schemaValue || void 0;
596
685
  return String(formValue) === String(currentPropertyValue);
597
686
  }
598
- function checkIfConditionMatches(node, formValues, formFields) {
599
- return Object.keys(node.if.properties).every((name) => {
600
- const currentProperty = node.if.properties[name];
601
- const value = formValues[name];
602
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
603
- value === null;
604
- const hasIfExplicit = node.if.required?.includes(name);
605
- if (hasEmptyValue && !hasIfExplicit) {
606
- return true;
607
- }
608
- if (hasProperty(currentProperty, "const")) {
609
- return compareFormValueWithSchemaValue(value, currentProperty.const);
610
- }
611
- if (currentProperty.contains?.pattern) {
612
- const formValue = value || [];
613
- if (Array.isArray(formValue)) {
614
- const pattern = new RegExp(currentProperty.contains.pattern);
615
- return (value || []).some((item) => pattern.test(item));
616
- }
617
- }
618
- if (currentProperty.enum) {
619
- return currentProperty.enum.includes(value);
620
- }
621
- const field = getField(name, formFields);
622
- return validateFieldSchema(
623
- {
624
- options: field.options,
625
- // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
626
- ...currentProperty,
627
- inputType: field.inputType,
628
- required: true
629
- },
630
- value
631
- );
632
- });
633
- }
634
687
  function isFieldFilled(fieldValue) {
635
688
  return Array.isArray(fieldValue) ? fieldValue.length > 0 : !!fieldValue;
636
689
  }
@@ -865,6 +918,7 @@ function extractParametersFromNode(schemaNode) {
865
918
  maxFileSize: node.maxFileSize,
866
919
  // @deprecated in favor of presentation.maxFileSize
867
920
  default: node.default,
921
+ format: node.format,
868
922
  // Checkboxes conditions
869
923
  // — For checkboxes that only accept one value (string)
870
924
  ...presentation?.inputType === "checkbox" && { checkboxValue: node.const },
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2023 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.4.1-dev.20230703203242
5
- Generated: Mon, 03 Jul 2023 20:32:50 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.4.3-beta.0
5
+ Generated: Wed, 09 Aug 2023 08:39:30 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -11025,6 +11025,69 @@ var lazy = function lazy2(fn) {
11025
11025
  return new Lazy_default(fn);
11026
11026
  };
11027
11027
 
11028
+ // src/utils.js
11029
+ function convertDiskSizeFromTo(from2, to) {
11030
+ const units = ["bytes", "kb", "mb"];
11031
+ return function convert(value) {
11032
+ return value * Math.pow(1024, units.indexOf(from2.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
11033
+ };
11034
+ }
11035
+ function containsHTML(str = "") {
11036
+ return /<[a-z][\s\S]*>/i.test(str);
11037
+ }
11038
+ function wrapWithSpan(html, properties = {}) {
11039
+ const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
11040
+ return `<span ${attributes}>${html}</span>`;
11041
+ }
11042
+ function hasProperty(object2, propertyName) {
11043
+ return Object.prototype.hasOwnProperty.call(object2, propertyName);
11044
+ }
11045
+
11046
+ // src/checkIfConditionMatches.js
11047
+ function checkIfConditionMatches(node, formValues, formFields) {
11048
+ return Object.keys(node.if.properties).every((name) => {
11049
+ const currentProperty = node.if.properties[name];
11050
+ const value = formValues[name];
11051
+ const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
11052
+ value === null;
11053
+ const hasIfExplicit = node.if.required?.includes(name);
11054
+ if (hasEmptyValue && !hasIfExplicit) {
11055
+ return true;
11056
+ }
11057
+ if (hasProperty(currentProperty, "const")) {
11058
+ return compareFormValueWithSchemaValue(value, currentProperty.const);
11059
+ }
11060
+ if (currentProperty.contains?.pattern) {
11061
+ const formValue = value || [];
11062
+ if (Array.isArray(formValue)) {
11063
+ const pattern = new RegExp(currentProperty.contains.pattern);
11064
+ return (value || []).some((item) => pattern.test(item));
11065
+ }
11066
+ }
11067
+ if (currentProperty.enum) {
11068
+ return currentProperty.enum.includes(value);
11069
+ }
11070
+ if (currentProperty.properties) {
11071
+ return checkIfConditionMatches(
11072
+ { if: currentProperty },
11073
+ formValues[name],
11074
+ getField(name, formFields).fields
11075
+ );
11076
+ }
11077
+ const field = getField(name, formFields);
11078
+ return validateFieldSchema(
11079
+ {
11080
+ options: field.options,
11081
+ // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
11082
+ ...currentProperty,
11083
+ inputType: field.inputType,
11084
+ required: true
11085
+ },
11086
+ value
11087
+ );
11088
+ });
11089
+ }
11090
+
11028
11091
  // src/internals/helpers.js
11029
11092
  var import_merge = __toESM(require_merge2());
11030
11093
  var import_get2 = __toESM(require_get());
@@ -11284,24 +11347,6 @@ function _composeFieldCustomClosure(defaultComposeFn) {
11284
11347
  };
11285
11348
  }
11286
11349
 
11287
- // src/utils.js
11288
- function convertDiskSizeFromTo(from2, to) {
11289
- const units = ["bytes", "kb", "mb"];
11290
- return function convert(value) {
11291
- return value * Math.pow(1024, units.indexOf(from2.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
11292
- };
11293
- }
11294
- function containsHTML(str = "") {
11295
- return /<[a-z][\s\S]*>/i.test(str);
11296
- }
11297
- function wrapWithSpan(html, properties = {}) {
11298
- const attributes = Object.entries(properties).reduce((acc, [key, value]) => `${acc}${key}="${value}" `, "").trim();
11299
- return `<span ${attributes}>${html}</span>`;
11300
- }
11301
- function hasProperty(object2, propertyName) {
11302
- return Object.prototype.hasOwnProperty.call(object2, propertyName);
11303
- }
11304
-
11305
11350
  // src/yupSchema.js
11306
11351
  var import_flow = __toESM(require_flow());
11307
11352
  var import_noop = __toESM(require_noop());
@@ -11321,6 +11366,25 @@ var validateOnlyStrings = StringSchema().trim().nullable().test(
11321
11366
  return true;
11322
11367
  }
11323
11368
  );
11369
+ var compareDates = (d1, d2) => {
11370
+ let date1 = new Date(d1).getTime();
11371
+ let date2 = new Date(d2).getTime();
11372
+ if (date1 < date2) {
11373
+ return "LESSER";
11374
+ } else if (date1 > date2) {
11375
+ return "GREATER";
11376
+ } else {
11377
+ return "EQUAL";
11378
+ }
11379
+ };
11380
+ var validateMinDate = (value, minDate) => {
11381
+ const compare = compareDates(value, minDate);
11382
+ return compare === "GREATER" || compare === "EQUAL" ? true : false;
11383
+ };
11384
+ var validateMaxDate = (value, minDate) => {
11385
+ const compare = compareDates(value, minDate);
11386
+ return compare === "LESSER" || compare === "EQUAL" ? true : false;
11387
+ };
11324
11388
  var yupSchemas = {
11325
11389
  text: validateOnlyStrings,
11326
11390
  radioOrSelect: (options) => StringSchema().nullable().transform((value) => {
@@ -11334,10 +11398,32 @@ var yupSchemas = {
11334
11398
  }).oneOf(options, ({ value }) => {
11335
11399
  return `The option ${JSON.stringify(value)} is not valid.`;
11336
11400
  }),
11337
- date: StringSchema().nullable().trim().matches(
11338
- /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
11339
- `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
11340
- ),
11401
+ date: ({ minDate, maxDate }) => {
11402
+ let dateString = StringSchema().nullable().transform((value) => {
11403
+ if (value === "") {
11404
+ return void 0;
11405
+ }
11406
+ return value === null ? void 0 : value;
11407
+ }).trim().matches(
11408
+ /(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
11409
+ `Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
11410
+ );
11411
+ if (minDate) {
11412
+ dateString = dateString.test(
11413
+ "minDate",
11414
+ `The date must be ${minDate} or after.`,
11415
+ (value) => validateMinDate(value, minDate)
11416
+ );
11417
+ }
11418
+ if (maxDate) {
11419
+ dateString = dateString.test(
11420
+ "maxDate",
11421
+ `The date must be ${maxDate} or before.`,
11422
+ (value) => validateMaxDate(value, maxDate)
11423
+ );
11424
+ }
11425
+ return dateString;
11426
+ },
11341
11427
  number: NumberSchema().typeError("The value must be a number").nullable(),
11342
11428
  file: array_default().nullable(),
11343
11429
  email: StringSchema().trim().email("Please enter a valid email address").nullable(),
@@ -11382,6 +11468,9 @@ var getYupSchema = ({ inputType, ...field }) => {
11382
11468
  const optionValues = getOptions(field);
11383
11469
  return yupSchemas.radioOrSelect(optionValues);
11384
11470
  }
11471
+ if (field.format === "date") {
11472
+ return yupSchemas.date({ minDate: field.minDate, maxDate: field.maxDate });
11473
+ }
11385
11474
  return yupSchemas[inputType] || yupSchemasToJsonTypes[jsonType];
11386
11475
  };
11387
11476
  function buildYupSchema(field, config) {
@@ -11571,42 +11660,6 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) {
11571
11660
  const currentPropertyValue = typeof schemaValue === "number" ? schemaValue : schemaValue || void 0;
11572
11661
  return String(formValue) === String(currentPropertyValue);
11573
11662
  }
11574
- function checkIfConditionMatches(node, formValues, formFields) {
11575
- return Object.keys(node.if.properties).every((name) => {
11576
- const currentProperty = node.if.properties[name];
11577
- const value = formValues[name];
11578
- const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
11579
- value === null;
11580
- const hasIfExplicit = node.if.required?.includes(name);
11581
- if (hasEmptyValue && !hasIfExplicit) {
11582
- return true;
11583
- }
11584
- if (hasProperty(currentProperty, "const")) {
11585
- return compareFormValueWithSchemaValue(value, currentProperty.const);
11586
- }
11587
- if (currentProperty.contains?.pattern) {
11588
- const formValue = value || [];
11589
- if (Array.isArray(formValue)) {
11590
- const pattern = new RegExp(currentProperty.contains.pattern);
11591
- return (value || []).some((item) => pattern.test(item));
11592
- }
11593
- }
11594
- if (currentProperty.enum) {
11595
- return currentProperty.enum.includes(value);
11596
- }
11597
- const field = getField(name, formFields);
11598
- return validateFieldSchema(
11599
- {
11600
- options: field.options,
11601
- // @TODO/CODE SMELL. We are passing the property (raw field), but buildYupSchema() expected the output field.
11602
- ...currentProperty,
11603
- inputType: field.inputType,
11604
- required: true
11605
- },
11606
- value
11607
- );
11608
- });
11609
- }
11610
11663
  function isFieldFilled(fieldValue) {
11611
11664
  return Array.isArray(fieldValue) ? fieldValue.length > 0 : !!fieldValue;
11612
11665
  }
@@ -11841,6 +11894,7 @@ function extractParametersFromNode(schemaNode) {
11841
11894
  maxFileSize: node.maxFileSize,
11842
11895
  // @deprecated in favor of presentation.maxFileSize
11843
11896
  default: node.default,
11897
+ format: node.format,
11844
11898
  // Checkboxes conditions
11845
11899
  // — For checkboxes that only accept one value (string)
11846
11900
  ...presentation?.inputType === "checkbox" && { checkboxValue: node.const },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remoteoss/json-schema-form",
3
- "version": "0.4.1-dev.20230703203242",
3
+ "version": "0.4.3-beta.0",
4
4
  "description": "Headless UI form powered by JSON Schemas",
5
5
  "author": "Remote.com <engineering@remote.com> (https://remote.com/)",
6
6
  "license": "MIT",
@@ -0,0 +1,51 @@
1
+ import { checkIfConditionMatches } from '../checkIfConditionMatches';
2
+
3
+ it('Empty if is always going to be true', () => {
4
+ expect(checkIfConditionMatches({ if: { properties: {} } })).toBe(true);
5
+ });
6
+
7
+ it('Basic if check passes with correct value', () => {
8
+ expect(
9
+ checkIfConditionMatches(
10
+ { if: { properties: { a: { const: 'hello' } } } },
11
+ {
12
+ a: 'hello',
13
+ }
14
+ )
15
+ ).toBe(true);
16
+ });
17
+
18
+ it('Basic if check fails with incorrect value', () => {
19
+ expect(
20
+ checkIfConditionMatches(
21
+ { if: { properties: { a: { const: 'hello' } } } },
22
+ {
23
+ a: 'goodbye',
24
+ }
25
+ )
26
+ ).toBe(false);
27
+ });
28
+
29
+ it('Nested properties check passes with correct value', () => {
30
+ expect(
31
+ checkIfConditionMatches(
32
+ { if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } },
33
+ {
34
+ parent: { child: 'hello from child' },
35
+ },
36
+ [{ name: 'parent', fields: [] }]
37
+ )
38
+ ).toBe(true);
39
+ });
40
+
41
+ it('Nested properties check passes with correct value', () => {
42
+ expect(
43
+ checkIfConditionMatches(
44
+ { if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } },
45
+ {
46
+ parent: { child: 'goodbye from child' },
47
+ },
48
+ [{ name: 'parent', fields: [] }]
49
+ )
50
+ ).toBe(false);
51
+ });
@@ -0,0 +1,58 @@
1
+ import { createHeadlessForm } from '../createHeadlessForm';
2
+
3
+ it('Should allow check of a nested property in a conditional', () => {
4
+ const { handleValidation } = createHeadlessForm(
5
+ {
6
+ additionalProperties: false,
7
+ allOf: [
8
+ {
9
+ if: {
10
+ properties: {
11
+ parent: {
12
+ properties: {
13
+ child: {
14
+ const: 'yes',
15
+ },
16
+ },
17
+ required: ['child'],
18
+ },
19
+ },
20
+ required: ['parent'],
21
+ },
22
+ then: { required: ['parent_sibling'] },
23
+ },
24
+ ],
25
+ properties: {
26
+ parent: {
27
+ additionalProperties: false,
28
+ properties: {
29
+ child: {
30
+ oneOf: [
31
+ {
32
+ const: 'yes',
33
+ },
34
+ { const: 'no' },
35
+ ],
36
+ type: 'string',
37
+ },
38
+ },
39
+ required: ['child'],
40
+ type: 'object',
41
+ },
42
+ parent_sibling: {
43
+ type: 'integer',
44
+ },
45
+ },
46
+ required: ['parent'],
47
+ type: 'object',
48
+ },
49
+ { strictInputType: false }
50
+ );
51
+ expect(handleValidation({ parent: { child: 'no' } }).formErrors).toEqual(undefined);
52
+ expect(handleValidation({ parent: { child: 'yes' } }).formErrors).toEqual({
53
+ parent_sibling: 'Required field',
54
+ });
55
+ expect(handleValidation({ parent: { child: 'yes' }, parent_sibling: 1 }).formErrors).toEqual(
56
+ undefined
57
+ );
58
+ });
@@ -439,23 +439,15 @@ describe('createHeadlessForm', () => {
439
439
 
440
440
  // All allowed options are valid
441
441
  validOptions.forEach((value) => {
442
- expect(
443
- validateForm({
444
- [fieldName]: value,
445
- })
446
- ).toBeUndefined();
442
+ expect(validateForm({ [fieldName]: value })).toBeUndefined();
447
443
  });
448
444
 
449
445
  // Any other arbitrary value is not valid.
450
- expect(
451
- validateForm({
452
- [fieldName]: 'blah-blah',
453
- })
454
- ).toEqual({
446
+ expect(validateForm({ [fieldName]: 'blah-blah' })).toEqual({
455
447
  [fieldName]: 'The option "blah-blah" is not valid.',
456
448
  });
457
449
 
458
- // This is a required field:
450
+ // Given undefined, it says it's a required field.
459
451
  expect(validateForm({})).toEqual({
460
452
  [fieldName]: 'Required field',
461
453
  });
@@ -831,6 +823,9 @@ describe('createHeadlessForm', () => {
831
823
 
832
824
  describe('support "radio" optional field - more examples @BUG RMT-518', () => {
833
825
  function assertCommonBehavior(validateForm) {
826
+ // Note: Very similar to assertOptionsAllowed()
827
+ // We could reuse it in a next iteration.
828
+
834
829
  // Happy path
835
830
  expect(validateForm({ has_car: 'yes' })).toBeUndefined();
836
831
 
@@ -1014,30 +1009,112 @@ describe('createHeadlessForm', () => {
1014
1009
  });
1015
1010
 
1016
1011
  it('support "date" field type', () => {
1017
- const result = createHeadlessForm(schemaInputTypeDate);
1012
+ const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1018
1013
 
1019
- expect(result).toMatchObject({
1020
- fields: [
1021
- {
1022
- label: 'Birthdate',
1023
- name: 'birthdate',
1024
- required: true,
1025
- schema: expect.any(Object),
1026
- type: 'date',
1027
- maxLength: 10,
1028
- minDate: '1922-03-01',
1029
- maxDate: '2022-03-01',
1030
- },
1031
- ],
1014
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
1015
+
1016
+ expect(fields[0]).toMatchObject({
1017
+ label: 'Birthdate',
1018
+ name: 'birthdate',
1019
+ required: true,
1020
+ schema: expect.any(Object),
1021
+ type: 'date',
1022
+ minDate: '1922-03-01',
1023
+ maxDate: '2022-03-17',
1032
1024
  });
1033
1025
 
1034
- const fieldValidator = result.fields[0].schema;
1035
1026
  const todayDateHint = new Date().toISOString().substring(0, 10);
1036
- expect(fieldValidator.isValidSync('2020-10-10')).toBe(true);
1037
- expect(fieldValidator.isValidSync('2020-13-10')).toBe(false);
1038
- expect(() => fieldValidator.validateSync('')).toThrowError(
1039
- `Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`
1040
- );
1027
+
1028
+ expect(validateForm({})).toEqual({
1029
+ birthdate: 'Required field',
1030
+ });
1031
+
1032
+ expect(validateForm({ birthdate: '2020-10-10' })).toBeUndefined();
1033
+ expect(validateForm({ birthdate: '2020-13-10' })).toEqual({
1034
+ birthdate: `Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`,
1035
+ });
1036
+ });
1037
+
1038
+ it('support "date" field type with a minDate', () => {
1039
+ const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1040
+
1041
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
1042
+
1043
+ expect(fields[0]).toMatchObject({
1044
+ label: 'Birthdate',
1045
+ name: 'birthdate',
1046
+ required: true,
1047
+ schema: expect.any(Object),
1048
+ type: 'date',
1049
+ minDate: '1922-03-01',
1050
+ maxDate: '2022-03-17',
1051
+ });
1052
+
1053
+ expect(validateForm({})).toEqual({
1054
+ birthdate: 'Required field',
1055
+ });
1056
+
1057
+ expect(validateForm({ birthdate: '' })).toEqual({
1058
+ birthdate: `Required field`,
1059
+ });
1060
+
1061
+ expect(validateForm({ birthdate: '1922-02-01' })).toEqual({
1062
+ birthdate: 'The date must be 1922-03-01 or after.',
1063
+ });
1064
+
1065
+ expect(validateForm({ birthdate: '1922-03-01' })).toBeUndefined();
1066
+
1067
+ expect(validateForm({ birthdate: '2021-03-01' })).toBeUndefined();
1068
+ });
1069
+
1070
+ it('support "date" field type with a maxDate', () => {
1071
+ const { fields, handleValidation } = createHeadlessForm(schemaInputTypeDate);
1072
+
1073
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
1074
+
1075
+ expect(fields[0]).toMatchObject({
1076
+ label: 'Birthdate',
1077
+ name: 'birthdate',
1078
+ required: true,
1079
+ schema: expect.any(Object),
1080
+ type: 'date',
1081
+ minDate: '1922-03-01',
1082
+ maxDate: '2022-03-17',
1083
+ });
1084
+
1085
+ expect(validateForm({ birthdate: '' })).toEqual({
1086
+ birthdate: `Required field`,
1087
+ });
1088
+
1089
+ expect(validateForm({ birthdate: '2022-02-01' })).toBeUndefined();
1090
+ expect(validateForm({ birthdate: '2022-03-01' })).toBeUndefined();
1091
+ expect(validateForm({ birthdate: '2022-04-01' })).toEqual({
1092
+ birthdate: 'The date must be 2022-03-17 or before.',
1093
+ });
1094
+ });
1095
+
1096
+ it('support format date with minDate and maxDate', () => {
1097
+ const schemaFormatDate = {
1098
+ properties: {
1099
+ birthdate: {
1100
+ title: 'Birthdate',
1101
+ type: 'string',
1102
+ format: 'date',
1103
+ 'x-jsf-presentation': {
1104
+ inputType: 'myDateType',
1105
+ maxDate: '2022-03-01',
1106
+ minDate: '1922-03-01',
1107
+ },
1108
+ },
1109
+ },
1110
+ };
1111
+
1112
+ const { handleValidation } = createHeadlessForm(schemaFormatDate);
1113
+ const validateForm = (vals) => friendlyError(handleValidation(vals));
1114
+
1115
+ expect(validateForm({ birthdate: '1922-02-01' })).toEqual({
1116
+ birthdate: 'The date must be 1922-03-01 or after.',
1117
+ });
1041
1118
  });
1042
1119
 
1043
1120
  it('supports "file" field type', () => {
@@ -272,18 +272,6 @@ export const mockSelectInputMultipleOptional = {
272
272
  type: ['array', 'null'],
273
273
  };
274
274
 
275
- export const mockDateInput = {
276
- 'x-jsf-presentation': {
277
- inputType: 'date',
278
- maxDate: '2022-03-01',
279
- minDate: '1922-03-01',
280
- },
281
- title: 'Birthdate',
282
- type: 'string',
283
- format: 'date',
284
- maxLength: 10,
285
- };
286
-
287
275
  export const mockFileInput = {
288
276
  description: 'File Input Description',
289
277
  'x-jsf-presentation': {
@@ -998,12 +986,19 @@ export const schemaInputTypeNumberWithPercentage = JSONSchemaBuilder()
998
986
  .setRequiredFields(['shares'])
999
987
  .build();
1000
988
 
1001
- export const schemaInputTypeDate = JSONSchemaBuilder()
1002
- .addInput({
1003
- birthdate: mockDateInput,
1004
- })
1005
- .setRequiredFields(['birthdate'])
1006
- .build();
989
+ export const schemaInputTypeDate = {
990
+ type: 'object',
991
+ additionalProperties: false,
992
+ properties: {
993
+ birthdate: {
994
+ 'x-jsf-presentation': { inputType: 'date', maxDate: '2022-03-17', minDate: '1922-03-01' },
995
+ title: 'Birthdate',
996
+ type: 'string',
997
+ format: 'date',
998
+ },
999
+ },
1000
+ required: ['birthdate'],
1001
+ };
1007
1002
 
1008
1003
  export const schemaInputTypeEmail = JSONSchemaBuilder()
1009
1004
  .addInput({