@remoteoss/json-schema-form 0.7.1-dev.20231026204916 → 0.7.2-dev.20231106093837

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.
@@ -1,8 +1,8 @@
1
1
 
2
2
  /*!
3
3
  Copyright (c) 2023 Remote Technology, Inc.
4
- NPM Package: @remoteoss/json-schema-form@0.7.1-dev.20231026204916
5
- Generated: Thu, 26 Oct 2023 20:49:34 GMT
4
+ NPM Package: @remoteoss/json-schema-form@0.7.2-dev.20231106093837
5
+ Generated: Mon, 06 Nov 2023 09:38:54 GMT
6
6
 
7
7
  MIT License
8
8
 
@@ -11779,38 +11779,22 @@ var validateMaxDate = (value, minDate) => {
11779
11779
  };
11780
11780
  var yupSchemas = {
11781
11781
  text: validateOnlyStrings,
11782
- radioOrSelect: (options) => StringSchema().nullable().transform((value) => {
11783
- if (value === "") {
11784
- return void 0;
11782
+ radioOrSelect: (options, isString2) => {
11783
+ if (isString2) {
11784
+ return StringSchema().nullable();
11785
11785
  }
11786
- if (options?.some((option) => option.value === null)) {
11787
- return value;
11788
- }
11789
- return value === null ? void 0 : value;
11790
- }).test(
11791
- /*
11792
- Custom test determines if the value either:
11793
- - Matches a specific option by value
11794
- - Matches a pattern
11795
- If the option is undefined do not test, to allow for optional fields.
11796
- */
11797
- "matchesOptionOrPattern",
11798
- ({ value }) => `The option ${JSON.stringify(value)} is not valid.`,
11799
- (value) => {
11800
- if (value === void 0) {
11801
- return true;
11802
- }
11803
- const exactMatch = options.some((option) => option.value === value);
11804
- if (exactMatch) {
11805
- return true;
11786
+ return StringSchema().nullable().transform((value) => {
11787
+ if (value === "") {
11788
+ return void 0;
11806
11789
  }
11807
- const patternMatch = options.some((option) => option.pattern?.test(value));
11808
- if (patternMatch) {
11809
- return true;
11790
+ if (options?.includes(null)) {
11791
+ return value;
11810
11792
  }
11811
- return false;
11812
- }
11813
- ),
11793
+ return value === null ? void 0 : value;
11794
+ }).oneOf(options, ({ value }) => {
11795
+ return `The option ${JSON.stringify(value)} is not valid.`;
11796
+ });
11797
+ },
11814
11798
  date: ({ minDate, maxDate }) => {
11815
11799
  let dateString = StringSchema().nullable().transform((value) => {
11816
11800
  if (value === "") {
@@ -11868,21 +11852,19 @@ function getRequiredErrorMessage(inputType, { inlineError, configError }) {
11868
11852
  }
11869
11853
  var getJsonTypeInArray = (jsonType) => Array.isArray(jsonType) ? jsonType.find((val) => val !== "null") : jsonType;
11870
11854
  var getOptions = (field) => {
11871
- const allValues = field.options?.map((option) => ({
11872
- value: option.value,
11873
- pattern: option.pattern ? new RegExp(option.pattern) : null
11874
- }));
11855
+ const allValues = field.options?.map((option) => option.value);
11875
11856
  const isOptionalWithNull = Array.isArray(field.jsonType) && // @TODO should also check the "oneOf" directly looking for "null"
11876
11857
  // option but we don't have direct access at this point.
11877
11858
  // Otherwise the JSON Schema validator will fail as explained in PR#18
11878
11859
  field.jsonType.includes("null");
11879
- return isOptionalWithNull ? [...allValues, { option: null }] : allValues;
11860
+ return isOptionalWithNull ? [...allValues, null] : allValues;
11880
11861
  };
11881
11862
  var getYupSchema = ({ inputType, ...field }) => {
11882
11863
  const jsonType = getJsonTypeInArray(field.jsonType);
11883
11864
  if (field.options?.length > 0) {
11865
+ const isString2 = field.options?.findIndex((option) => option.type === "string") > -1;
11884
11866
  const optionValues = getOptions(field);
11885
- return yupSchemas.radioOrSelect(optionValues);
11867
+ return yupSchemas.radioOrSelect(optionValues, isString2);
11886
11868
  }
11887
11869
  if (field.format === "date") {
11888
11870
  return yupSchemas.date({ minDate: field.minDate, maxDate: field.maxDate });
@@ -12373,6 +12355,33 @@ function processJSONLogicNode({
12373
12355
  }
12374
12356
 
12375
12357
  // src/helpers.js
12358
+ var dynamicInternalJsfAttrs = [
12359
+ "isVisible",
12360
+ // Driven from conditionals state
12361
+ "fields",
12362
+ // driven from group-array
12363
+ "getComputedAttributes",
12364
+ // From json-logic
12365
+ "computedAttributes",
12366
+ // From json-logic
12367
+ "calculateConditionalProperties",
12368
+ // driven from conditionals
12369
+ "calculateCustomValidationProperties",
12370
+ // To be deprecated in favor of json-logic
12371
+ "scopedJsonSchema"
12372
+ // The respective JSON Schema
12373
+ ];
12374
+ var dynamicInternalJsfAttrsObj = Object.fromEntries(
12375
+ dynamicInternalJsfAttrs.map((k) => [k, true])
12376
+ );
12377
+ function removeConditionalStaleAttributes(field, conditionalAttrs, rootAttrs) {
12378
+ Object.keys(field).forEach((key) => {
12379
+ if (conditionalAttrs[key] === void 0 && rootAttrs[key] === void 0 && // Don't remove attrs that were declared in the root field.
12380
+ dynamicInternalJsfAttrsObj[key] === void 0) {
12381
+ field[key] = void 0;
12382
+ }
12383
+ });
12384
+ }
12376
12385
  function hasType(type, typeName) {
12377
12386
  return Array.isArray(type) ? type.includes(typeName) : type === typeName;
12378
12387
  }
@@ -12465,19 +12474,21 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
12465
12474
  if (fieldIsRequired) {
12466
12475
  field.isVisible = true;
12467
12476
  }
12468
- const updateValues = (fieldValues) => Object.entries(fieldValues).forEach(([key, value]) => {
12469
- field[key] = typeof value === "function" ? value() : value;
12470
- if (key === "value") {
12471
- const readOnlyPropertyWasUpdated = typeof fieldValues.readOnly !== "undefined";
12472
- const isReadonlyByDefault = field.readOnly;
12473
- const isReadonly = readOnlyPropertyWasUpdated ? fieldValues.readOnly : isReadonlyByDefault;
12474
- if (!isReadonly && (value === null || field.inputType === "checkbox")) {
12475
- field.value = void 0;
12477
+ const updateAttributes = (fieldAttrs) => {
12478
+ Object.entries(fieldAttrs).forEach(([key, value]) => {
12479
+ field[key] = typeof value === "function" ? value() : value;
12480
+ if (key === "value") {
12481
+ const readOnlyPropertyWasUpdated = typeof fieldAttrs.readOnly !== "undefined";
12482
+ const isReadonlyByDefault = field.readOnly;
12483
+ const isReadonly = readOnlyPropertyWasUpdated ? fieldAttrs.readOnly : isReadonlyByDefault;
12484
+ if (!isReadonly && (value === null || field.inputType === "checkbox")) {
12485
+ field.value = void 0;
12486
+ }
12476
12487
  }
12477
- }
12478
- });
12488
+ });
12489
+ };
12479
12490
  if (field.getComputedAttributes) {
12480
- const computedFieldValues = field.getComputedAttributes({
12491
+ const newAttributes = field.getComputedAttributes({
12481
12492
  field,
12482
12493
  isRequired: fieldIsRequired,
12483
12494
  node,
@@ -12485,23 +12496,24 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
12485
12496
  config,
12486
12497
  logic
12487
12498
  });
12488
- updateValues(computedFieldValues);
12499
+ updateAttributes(newAttributes);
12489
12500
  }
12490
12501
  if (field.calculateConditionalProperties) {
12491
- const newFieldValues = field.calculateConditionalProperties({
12502
+ const { rootFieldAttrs, newAttributes } = field.calculateConditionalProperties({
12492
12503
  isRequired: fieldIsRequired,
12493
12504
  conditionBranch: node,
12494
12505
  formValues
12495
12506
  });
12496
- updateValues(newFieldValues);
12507
+ updateAttributes(newAttributes, rootFieldAttrs);
12508
+ removeConditionalStaleAttributes(field, newAttributes, rootFieldAttrs);
12497
12509
  }
12498
12510
  if (field.calculateCustomValidationProperties) {
12499
- const newFieldValues = field.calculateCustomValidationProperties(
12511
+ const newAttributes = field.calculateCustomValidationProperties(
12500
12512
  fieldIsRequired,
12501
12513
  node,
12502
12514
  formValues
12503
12515
  );
12504
- updateValues(newFieldValues);
12516
+ updateAttributes(newAttributes);
12505
12517
  }
12506
12518
  }
12507
12519
  function processNode({
@@ -12843,17 +12855,23 @@ function calculateConditionalProperties({ fieldParams, customProperties, logic,
12843
12855
  logic
12844
12856
  )
12845
12857
  };
12846
- return (0, import_omit2.default)((0, import_merge2.default)(base, presentation, newFieldParams), ["inputType"]);
12858
+ return {
12859
+ rootFieldAttrs: fieldParams,
12860
+ newAttributes: (0, import_omit2.default)((0, import_merge2.default)(base, presentation, newFieldParams), ["inputType"])
12861
+ };
12847
12862
  }
12848
12863
  const isVisible = isRequired;
12849
12864
  return {
12850
- isVisible,
12851
- required: isRequired,
12852
- schema: buildYupSchema({
12853
- ...fieldParams,
12854
- ...extractParametersFromNode(conditionBranch),
12855
- required: isRequired
12856
- })
12865
+ rootFieldAttrs: fieldParams,
12866
+ newAttributes: {
12867
+ isVisible,
12868
+ required: isRequired,
12869
+ schema: buildYupSchema({
12870
+ ...fieldParams,
12871
+ ...extractParametersFromNode(conditionBranch),
12872
+ required: isRequired
12873
+ })
12874
+ }
12857
12875
  };
12858
12876
  };
12859
12877
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remoteoss/json-schema-form",
3
- "version": "0.7.1-dev.20231026204916",
3
+ "version": "0.7.2-dev.20231106093837",
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",
@@ -27,12 +27,7 @@ it('Should allow check of a nested property in a conditional', () => {
27
27
  additionalProperties: false,
28
28
  properties: {
29
29
  child: {
30
- oneOf: [
31
- {
32
- const: 'yes',
33
- },
34
- { const: 'no' },
35
- ],
30
+ oneOf: [{ const: 'yes' }, { const: 'no' }],
36
31
  type: 'string',
37
32
  },
38
33
  },
@@ -56,3 +51,311 @@ it('Should allow check of a nested property in a conditional', () => {
56
51
  undefined
57
52
  );
58
53
  });
54
+
55
+ describe('Conditional attributes updated', () => {
56
+ it('Update basic case with const, default, maximum', () => {
57
+ const { fields, handleValidation } = createHeadlessForm(
58
+ {
59
+ properties: {
60
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
61
+ hours: { type: 'number' },
62
+ },
63
+ allOf: [
64
+ {
65
+ if: {
66
+ properties: { is_full_time: { const: 'yes' } },
67
+ required: ['is_full_time'],
68
+ },
69
+ then: {
70
+ properties: {
71
+ hours: {
72
+ const: 8,
73
+ default: 8,
74
+ },
75
+ },
76
+ },
77
+ else: {
78
+ properties: {
79
+ hours: {
80
+ maximum: 4,
81
+ },
82
+ },
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ { strictInputType: false }
88
+ );
89
+
90
+ // Given "Yes" it applies "const" and "default"
91
+ expect(handleValidation({ is_full_time: 'yes', hours: 4 }).formErrors).toEqual({
92
+ hours: 'The only accepted value is 8.',
93
+ });
94
+ expect(fields[1]).toMatchObject({ const: 8, default: 8 });
95
+ expect(fields[1].maximum).toBeUndefined();
96
+
97
+ // Changing to "No", applies the "maximum" and cleans "const" and "default"
98
+ expect(handleValidation({ is_full_time: 'no', hours: 4 }).formErrors).toBeUndefined();
99
+ expect(fields[1]).toMatchObject({ maximum: 4 });
100
+ expect(fields[1].const).toBeUndefined();
101
+ expect(fields[1].default).toBeUndefined();
102
+
103
+ // Changing back to "Yes", it removes "maximum", and applies "const" and "default"
104
+ expect(handleValidation({}).formErrors).toBeUndefined();
105
+ expect(handleValidation({ is_full_time: 'yes', hours: 8 }).formErrors).toBeUndefined();
106
+ expect(fields[1].maximum).toBeUndefined();
107
+ expect(fields[1]).toMatchObject({ const: 8, default: 8 });
108
+ });
109
+
110
+ it('Update a new attribute (eg description)', () => {
111
+ const { fields, handleValidation } = createHeadlessForm(
112
+ {
113
+ properties: {
114
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
115
+ hours: {
116
+ type: 'number',
117
+ },
118
+ },
119
+ allOf: [
120
+ {
121
+ if: {
122
+ properties: { is_full_time: { const: 'yes' } },
123
+ required: ['is_full_time'],
124
+ },
125
+ then: {
126
+ properties: {
127
+ hours: {
128
+ description: 'We recommend 8 hours.',
129
+ },
130
+ },
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ { strictInputType: false }
136
+ );
137
+
138
+ // By default the attribute is not set.
139
+ expect(fields[1].description).toBeUndefined();
140
+
141
+ // Given "Yes" it applies the conditional attribute
142
+ expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined();
143
+ expect(fields[1].description).toBe('We recommend 8 hours.');
144
+
145
+ // Changing to "No", removes the description
146
+ expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined();
147
+ expect(fields[1].description).toBeUndefined();
148
+
149
+ // Changing back to "Yes", it sets the attribute again
150
+ expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined();
151
+ expect(fields[1].description).toBe('We recommend 8 hours.');
152
+ });
153
+
154
+ it('Update an existing attribute (eg description)', () => {
155
+ const { fields, handleValidation } = createHeadlessForm(
156
+ {
157
+ properties: {
158
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
159
+ hours: {
160
+ type: 'number',
161
+ description: 'Any value works.',
162
+ },
163
+ },
164
+ allOf: [
165
+ {
166
+ if: {
167
+ properties: { is_full_time: { const: 'yes' } },
168
+ required: ['is_full_time'],
169
+ },
170
+ then: {
171
+ properties: {
172
+ hours: {
173
+ description: 'We recommend 8 hours.',
174
+ },
175
+ },
176
+ },
177
+ },
178
+ ],
179
+ },
180
+ { strictInputType: false }
181
+ );
182
+
183
+ // By default the attribute is set the base value.
184
+ expect(fields[1].description).toBe('Any value works.');
185
+
186
+ // Given "Yes" it applies the conditional attribute
187
+ expect(handleValidation({ is_full_time: 'yes', hours: 4 }).formErrors).toBeUndefined();
188
+ expect(fields[1].description).toBe('We recommend 8 hours.');
189
+
190
+ // Changing to "No", it applies the base value again.
191
+ expect(handleValidation({ is_full_time: 'no', hours: 4 }).formErrors).toBeUndefined();
192
+ expect(fields[1].description).toBe('Any value works.');
193
+
194
+ // Changing back to "Yes", it sets the attribute again
195
+ expect(handleValidation({ is_full_time: 'yes', hours: 8 }).formErrors).toBeUndefined();
196
+ expect(fields[1].description).toBe('We recommend 8 hours.');
197
+ });
198
+
199
+ it('Update a nested attribute', () => {
200
+ const { fields, handleValidation } = createHeadlessForm(
201
+ {
202
+ properties: {
203
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
204
+ hours: {
205
+ type: 'number',
206
+ presentation: {
207
+ inputType: 'number',
208
+ anything: 'info',
209
+ },
210
+ },
211
+ },
212
+ allOf: [
213
+ {
214
+ if: {
215
+ properties: { is_full_time: { const: 'yes' } },
216
+ required: ['is_full_time'],
217
+ },
218
+ then: {
219
+ properties: {
220
+ hours: {
221
+ presentation: {
222
+ anything: 'danger',
223
+ },
224
+ },
225
+ },
226
+ },
227
+ },
228
+ ],
229
+ },
230
+ { strictInputType: false }
231
+ );
232
+
233
+ // By default the attribute is set the base value.
234
+ expect(fields[1].anything).toBe('info');
235
+
236
+ // Given "Yes" it applies the conditional attribute
237
+ expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined();
238
+ expect(fields[1].anything).toBe('danger');
239
+
240
+ // Changing to "No", it applies the base value again.
241
+ expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined();
242
+ expect(fields[1].anything).toBe('info');
243
+
244
+ // Changing back to "Yes", it sets the attribute again
245
+ expect(handleValidation({ is_full_time: 'yes' }).formErrors).toBeUndefined();
246
+ expect(fields[1].anything).toBe('danger');
247
+ });
248
+
249
+ it('Keeps existing attributes in matches that dont change the attr', () => {
250
+ const { fields, handleValidation } = createHeadlessForm(
251
+ {
252
+ properties: {
253
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
254
+ hours: {
255
+ type: 'number',
256
+ description: 'Any value works.',
257
+ },
258
+ },
259
+ allOf: [
260
+ {
261
+ if: {
262
+ properties: { is_full_time: { const: 'yes' } },
263
+ required: ['is_full_time'],
264
+ },
265
+ then: {
266
+ required: ['hours'],
267
+ },
268
+ else: {
269
+ properties: {
270
+ hours: false,
271
+ },
272
+ },
273
+ },
274
+ ],
275
+ },
276
+ { strictInputType: false }
277
+ );
278
+
279
+ // By default the attribute is set the base value, even though the field is invisible.
280
+ expect(fields[1].description).toBe('Any value works.');
281
+ expect(fields[1].isVisible).toBe(false);
282
+
283
+ // Given "Yes" it keeps the base value
284
+ expect(handleValidation({ is_full_time: 'yes' }).formErrors).toEqual({
285
+ hours: 'Required field',
286
+ });
287
+ expect(fields[1].description).toBe('Any value works.');
288
+ expect(fields[1].isVisible).toBe(true);
289
+
290
+ // Changing to "No" it keeps the base value
291
+ expect(handleValidation({ is_full_time: 'no' }).formErrors).toBeUndefined();
292
+ expect(fields[1].description).toBe('Any value works.');
293
+ expect(fields[1].isVisible).toBe(false);
294
+ });
295
+
296
+ it('Keeps internal attributes (fieldAttrsFromJsf)', () => {
297
+ // This is necessary while we keep supporting "type", even if deprecated
298
+ // otherwise our Remote app will break because it didn't migrate
299
+ // from "type" to "inputType" yet.
300
+ const { fields, handleValidation } = createHeadlessForm(
301
+ {
302
+ properties: {
303
+ is_full_time: { type: 'string', oneOf: [{ const: 'yes' }, { const: 'no' }] },
304
+ salary_period: {
305
+ type: 'string',
306
+ title: 'Salary period',
307
+ oneOf: [
308
+ { title: 'Weekly', const: 'weekly' },
309
+ { title: 'Monthly', const: 'monthly' },
310
+ ],
311
+ },
312
+ },
313
+ allOf: [
314
+ {
315
+ if: {
316
+ properties: { is_full_time: { const: 'yes' } },
317
+ required: ['is_full_time'],
318
+ },
319
+ then: {
320
+ properties: {
321
+ salary_period: {
322
+ description: 'We recommend montlhy.',
323
+ },
324
+ },
325
+ },
326
+ },
327
+ ],
328
+ },
329
+ { strictInputType: false }
330
+ );
331
+
332
+ // Given "Yes" it keeps the "type"
333
+ handleValidation({ is_full_time: 'yes' });
334
+
335
+ // All the following attrs are never removed
336
+ // during conditionals because they are core.
337
+ expect(fields[1]).toMatchObject({
338
+ name: 'salary_period',
339
+ label: 'Salary period',
340
+ required: false,
341
+ type: 'radio',
342
+ inputType: 'radio',
343
+ jsonType: 'string',
344
+ computedAttributes: {},
345
+ calculateConditionalProperties: expect.any(Function),
346
+ schema: expect.any(Object),
347
+ scopedJsonSchema: expect.any(Object),
348
+ isVisible: true,
349
+ options: [
350
+ {
351
+ label: 'Weekly',
352
+ value: 'weekly',
353
+ },
354
+ {
355
+ label: 'Monthly',
356
+ value: 'monthly',
357
+ },
358
+ ],
359
+ });
360
+ });
361
+ });