@remoteoss/json-schema-form 0.5.0-dev.20230719162322 → 0.5.0-dev.20230901130231

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remoteoss/json-schema-form",
3
- "version": "0.5.0-dev.20230719162322",
3
+ "version": "0.5.0-dev.20230901130231",
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",
@@ -47,6 +47,7 @@
47
47
  ]
48
48
  },
49
49
  "dependencies": {
50
+ "json-logic-js": "^2.0.2",
50
51
  "lodash": "^4.17.21",
51
52
  "randexp": "^0.5.3",
52
53
  "yup": "^0.30.0"
@@ -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
+ });
@@ -0,0 +1,86 @@
1
+ import { createHeadlessForm } from '../createHeadlessForm';
2
+
3
+ describe('validations: const', () => {
4
+ it('Should work for number', () => {
5
+ const { handleValidation } = createHeadlessForm(
6
+ {
7
+ properties: {
8
+ ten_only: { type: 'number', const: 10 },
9
+ },
10
+ },
11
+ { strictInputType: false }
12
+ );
13
+ expect(handleValidation({}).formErrors).toEqual(undefined);
14
+ expect(handleValidation({ ten_only: 1 }).formErrors).toEqual({
15
+ ten_only: 'The only accepted value is 10.',
16
+ });
17
+ expect(handleValidation({ ten_only: 10 }).formErrors).toBeUndefined();
18
+ // null is also considered valid until we fix @BUG RMT-518
19
+ // Expectation: To fail with error "The only accepted value is 10."
20
+ expect(handleValidation({ ten_only: null }).formErrors).toBeUndefined();
21
+ });
22
+
23
+ it('Should work for text', () => {
24
+ const { handleValidation } = createHeadlessForm(
25
+ {
26
+ properties: {
27
+ hello_only: { type: 'string', const: 'hello' },
28
+ },
29
+ },
30
+ { strictInputType: false }
31
+ );
32
+ expect(handleValidation({}).formErrors).toEqual(undefined);
33
+ expect(handleValidation({ hello_only: 'what' }).formErrors).toEqual({
34
+ hello_only: 'The only accepted value is hello.',
35
+ });
36
+ expect(handleValidation({ hello_only: 'hello' }).formErrors).toEqual(undefined);
37
+ });
38
+
39
+ it('Should work for a conditionally applied const', () => {
40
+ const { handleValidation } = createHeadlessForm(
41
+ {
42
+ properties: {
43
+ answer: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
44
+ amount: {
45
+ description: 'If you select yes, this needs to be exactly 10.',
46
+ type: 'number',
47
+ },
48
+ },
49
+ allOf: [
50
+ {
51
+ if: { properties: { answer: { const: 'yes' } }, required: ['answer'] },
52
+ then: { properties: { amount: { const: 10 } }, required: ['amount'] },
53
+ },
54
+ ],
55
+ },
56
+ { strictInputType: false }
57
+ );
58
+ expect(handleValidation({}).formErrors).toEqual(undefined);
59
+ expect(handleValidation({ answer: 'no' }).formErrors).toEqual(undefined);
60
+ expect(handleValidation({ answer: 'yes' }).formErrors).toEqual({ amount: 'Required field' });
61
+ expect(handleValidation({ answer: 'yes', amount: 1 }).formErrors).toEqual({
62
+ amount: 'The only accepted value is 10.',
63
+ });
64
+ expect(handleValidation({ answer: 'yes', amount: 10 }).formErrors).toEqual(undefined);
65
+ });
66
+
67
+ it('Should show the custom error message', () => {
68
+ const { handleValidation } = createHeadlessForm(
69
+ {
70
+ properties: {
71
+ string: {
72
+ type: 'string',
73
+ const: 'hello',
74
+ 'x-jsf-errorMessage': { const: 'You must say hello!!!' },
75
+ },
76
+ },
77
+ },
78
+ { strictInputType: false }
79
+ );
80
+ expect(handleValidation({}).formErrors).toEqual(undefined);
81
+ expect(handleValidation({ string: 'hi' }).formErrors).toEqual({
82
+ string: 'You must say hello!!!',
83
+ });
84
+ expect(handleValidation({ string: 'hello' }).formErrors).toEqual(undefined);
85
+ });
86
+ });
@@ -18,6 +18,7 @@ import {
18
18
  schemaInputTypeSelectMultipleDeprecated,
19
19
  schemaInputTypeSelectMultiple,
20
20
  schemaInputTypeSelectMultipleOptional,
21
+ schemaInputTypeFieldset,
21
22
  schemaInputTypeNumber,
22
23
  schemaInputTypeNumberZeroMaximum,
23
24
  schemaInputTypeDate,
@@ -92,12 +93,15 @@ const getField = (fields, name, ...subNames) => {
92
93
  };
93
94
 
94
95
  beforeEach(() => {
96
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
95
97
  jest.spyOn(console, 'error').mockImplementation(() => {});
96
98
  });
97
99
 
98
100
  afterEach(() => {
99
101
  expect(console.error).not.toHaveBeenCalled();
100
102
  console.error.mockRestore();
103
+ expect(console.warn).not.toHaveBeenCalled();
104
+ console.warn.mockRestore();
101
105
  });
102
106
 
103
107
  describe('createHeadlessForm', () => {
@@ -1020,7 +1024,7 @@ describe('createHeadlessForm', () => {
1020
1024
  schema: expect.any(Object),
1021
1025
  type: 'date',
1022
1026
  minDate: '1922-03-01',
1023
- maxDate: '2022-03-01',
1027
+ maxDate: '2022-03-17',
1024
1028
  });
1025
1029
 
1026
1030
  const todayDateHint = new Date().toISOString().substring(0, 10);
@@ -1047,17 +1051,15 @@ describe('createHeadlessForm', () => {
1047
1051
  schema: expect.any(Object),
1048
1052
  type: 'date',
1049
1053
  minDate: '1922-03-01',
1050
- maxDate: '2022-03-01',
1054
+ maxDate: '2022-03-17',
1051
1055
  });
1052
1056
 
1053
1057
  expect(validateForm({})).toEqual({
1054
1058
  birthdate: 'Required field',
1055
1059
  });
1056
1060
 
1057
- const todayDateHint = new Date().toISOString().substring(0, 10);
1058
-
1059
1061
  expect(validateForm({ birthdate: '' })).toEqual({
1060
- birthdate: `Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`,
1062
+ birthdate: `Required field`,
1061
1063
  });
1062
1064
 
1063
1065
  expect(validateForm({ birthdate: '1922-02-01' })).toEqual({
@@ -1081,19 +1083,17 @@ describe('createHeadlessForm', () => {
1081
1083
  schema: expect.any(Object),
1082
1084
  type: 'date',
1083
1085
  minDate: '1922-03-01',
1084
- maxDate: '2022-03-01',
1086
+ maxDate: '2022-03-17',
1085
1087
  });
1086
1088
 
1087
- const todayDateHint = new Date().toISOString().substring(0, 10);
1088
-
1089
1089
  expect(validateForm({ birthdate: '' })).toEqual({
1090
- birthdate: `Must be a valid date in yyyy-mm-dd format. e.g. ${todayDateHint}`,
1090
+ birthdate: `Required field`,
1091
1091
  });
1092
1092
 
1093
1093
  expect(validateForm({ birthdate: '2022-02-01' })).toBeUndefined();
1094
1094
  expect(validateForm({ birthdate: '2022-03-01' })).toBeUndefined();
1095
1095
  expect(validateForm({ birthdate: '2022-04-01' })).toEqual({
1096
- birthdate: 'The date must be 2022-03-01 or before.',
1096
+ birthdate: 'The date must be 2022-03-17 or before.',
1097
1097
  });
1098
1098
  });
1099
1099
 
@@ -3450,6 +3450,27 @@ describe('createHeadlessForm', () => {
3450
3450
  ],
3451
3451
  });
3452
3452
  });
3453
+
3454
+ it('should ignore initial values that do not match the field type (eg string vs object)', () => {
3455
+ const result = createHeadlessForm(schemaInputTypeFieldset, {
3456
+ initialValues: {
3457
+ a_fieldset: 'foo', // should be an object instead of string
3458
+ },
3459
+ });
3460
+
3461
+ // It returns fields without errors
3462
+ expect(result.fields).toBeDefined();
3463
+ expect(result.fields[0].fields[0].name).toBe('id_number');
3464
+ expect(result.fields[0].fields[1].name).toBe('tabs');
3465
+
3466
+ // Warn about those missmatched values
3467
+ expect(console.warn).toHaveBeenCalledWith(
3468
+ `Field "a_fieldset"'s value is "foo", but should be type object.`
3469
+ );
3470
+ console.warn.mockClear();
3471
+
3472
+ expect(console.error).not.toHaveBeenCalled();
3473
+ });
3453
3474
  });
3454
3475
  });
3455
3476
 
@@ -207,7 +207,6 @@ export const schemaInputTypeHidden = {
207
207
  title: 'Select multi hidden',
208
208
  default: ['Albania, Algeria'],
209
209
  'x-jsf-presentation': { inputType: 'hidden' },
210
- const: ['Albania, Algeria'],
211
210
  type: 'array',
212
211
  },
213
212
  },
@@ -991,7 +991,7 @@ export const schemaInputTypeDate = {
991
991
  additionalProperties: false,
992
992
  properties: {
993
993
  birthdate: {
994
- 'x-jsf-presentation': { inputType: 'date', maxDate: '2022-03-01', minDate: '1922-03-01' },
994
+ 'x-jsf-presentation': { inputType: 'date', maxDate: '2022-03-17', minDate: '1922-03-01' },
995
995
  title: 'Birthdate',
996
996
  type: 'string',
997
997
  format: 'date',
@@ -1020,12 +1020,12 @@ export const schemaInputTypeFileWithSkippable = JSONSchemaBuilder()
1020
1020
  })
1021
1021
  .build();
1022
1022
 
1023
- export const schemaInputTypeFieldset = JSONSchemaBuilder()
1024
- .addInput({
1023
+ export const schemaInputTypeFieldset = {
1024
+ properties: {
1025
1025
  a_fieldset: mockFieldset,
1026
- })
1027
- .setRequiredFields(['a_fieldset'])
1028
- .build();
1026
+ },
1027
+ required: ['a_fieldset'],
1028
+ };
1029
1029
 
1030
1030
  export const schemaInputTypeFocusedFieldset = JSONSchemaBuilder()
1031
1031
  .addInput({
@@ -0,0 +1,162 @@
1
+ export function createSchemaWithRulesOnFieldA(rules) {
2
+ return {
3
+ properties: {
4
+ field_a: {
5
+ type: 'number',
6
+ 'x-jsf-logic-validations': Object.keys(rules),
7
+ },
8
+ field_b: {
9
+ type: 'number',
10
+ },
11
+ },
12
+ required: ['field_a', 'field_b'],
13
+ 'x-jsf-logic': { validations: rules },
14
+ };
15
+ }
16
+
17
+ export function createSchemaWithThreePropertiesWithRuleOnFieldA(rules) {
18
+ return {
19
+ properties: {
20
+ field_a: {
21
+ type: 'number',
22
+ 'x-jsf-logic-validations': Object.keys(rules),
23
+ },
24
+ field_b: {
25
+ type: 'number',
26
+ },
27
+ field_c: {
28
+ type: 'number',
29
+ },
30
+ },
31
+ 'x-jsf-logic': { validations: rules },
32
+ required: ['field_a', 'field_b', 'field_c'],
33
+ };
34
+ }
35
+
36
+ export const schemaWithNonRequiredField = {
37
+ properties: {
38
+ field_a: {
39
+ type: 'number',
40
+ },
41
+ field_b: {
42
+ type: 'number',
43
+ 'x-jsf-logic-validations': ['a_greater_than_field_b'],
44
+ },
45
+ },
46
+ 'x-jsf-logic': {
47
+ validations: {
48
+ a_greater_than_field_b: {
49
+ errorMessage: 'Must be greater than field_a',
50
+ rule: {
51
+ '>': [{ var: 'field_a' }, { var: 'field_b' }],
52
+ },
53
+ },
54
+ },
55
+ },
56
+ required: [],
57
+ };
58
+
59
+ export const schemaWithNativeAndJSONLogicChecks = {
60
+ properties: {
61
+ field_a: {
62
+ type: 'number',
63
+ minimum: 100,
64
+ 'x-jsf-logic-validations': ['a_multiple_of_ten'],
65
+ },
66
+ },
67
+ 'x-jsf-logic': {
68
+ validations: {
69
+ a_multiple_of_ten: {
70
+ errorMessage: 'Must be a multiple of 10',
71
+ rule: {
72
+ '===': [{ '%': [{ var: 'field_a' }, 10] }, 0],
73
+ },
74
+ },
75
+ },
76
+ },
77
+ required: ['field_a'],
78
+ };
79
+
80
+ export const multiRuleSchema = {
81
+ properties: {
82
+ field_a: {
83
+ type: 'number',
84
+ 'x-jsf-logic-validations': ['a_bigger_than_b', 'is_even_number'],
85
+ },
86
+ field_b: {
87
+ type: 'number',
88
+ },
89
+ },
90
+ required: ['field_a', 'field_b'],
91
+ 'x-jsf-logic': {
92
+ validations: {
93
+ a_bigger_than_b: {
94
+ errorMessage: 'A must be bigger than B',
95
+ rule: {
96
+ '>': [{ var: 'field_a' }, { var: 'field_b' }],
97
+ },
98
+ },
99
+ is_even_number: {
100
+ errorMessage: 'A must be even',
101
+ rule: {
102
+ '===': [{ '%': [{ var: 'field_a' }, 2] }, 0],
103
+ },
104
+ },
105
+ },
106
+ },
107
+ };
108
+
109
+ export const schemaWithTwoRules = {
110
+ properties: {
111
+ field_a: {
112
+ type: 'number',
113
+ 'x-jsf-logic-validations': ['a_bigger_than_b'],
114
+ },
115
+ field_b: {
116
+ type: 'number',
117
+ 'x-jsf-logic-validations': ['is_even_number'],
118
+ },
119
+ },
120
+ required: ['field_a', 'field_b'],
121
+ 'x-jsf-logic': {
122
+ validations: {
123
+ a_bigger_than_b: {
124
+ errorMessage: 'A must be bigger than B',
125
+ rule: {
126
+ '>': [{ var: 'field_a' }, { var: 'field_b' }],
127
+ },
128
+ },
129
+ is_even_number: {
130
+ errorMessage: 'B must be even',
131
+ rule: {
132
+ '===': [{ '%': [{ var: 'field_b' }, 2] }, 0],
133
+ },
134
+ },
135
+ },
136
+ },
137
+ };
138
+
139
+ export const schemaWithComputedAttributes = {
140
+ properties: {
141
+ field_a: {
142
+ type: 'number',
143
+ },
144
+ field_b: {
145
+ type: 'number',
146
+ 'x-jsf-logic-computedAttrs': {
147
+ const: 'a_times_two',
148
+ default: 'a_times_two',
149
+ },
150
+ },
151
+ },
152
+ required: ['field_a', 'field_b'],
153
+ 'x-jsf-logic': {
154
+ computedValues: {
155
+ a_times_two: {
156
+ rule: {
157
+ '*': [{ var: 'field_a' }, 2],
158
+ },
159
+ },
160
+ },
161
+ },
162
+ };