@remoteoss/json-schema-form 0.10.1-beta.0 → 0.11.0-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.
@@ -53,12 +53,15 @@ const schemaPet = {
53
53
  properties: {
54
54
  street: {
55
55
  title: 'Street',
56
+ 'x-jsf-presentation': {
57
+ inputType: 'text',
58
+ },
56
59
  },
57
60
  },
58
61
  },
59
62
  },
60
63
  required: ['has_pet'],
61
- 'x-jsf-order': ['has_pet', 'pet_name'],
64
+ 'x-jsf-order': ['has_pet', 'pet_name', 'pet_age', 'pet_fat', 'pet_address'],
62
65
  allOf: [
63
66
  {
64
67
  id: 'pet_conditional_id',
@@ -102,6 +105,36 @@ const schemaAddress = {
102
105
  },
103
106
  };
104
107
 
108
+ beforeAll(() => {
109
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
110
+ });
111
+
112
+ afterAll(() => {
113
+ console.warn.mockRestore();
114
+ });
115
+
116
+ describe('modify() - warnings', () => {
117
+ it('logs a warning by default', () => {
118
+ const result = modify(schemaPet, {});
119
+
120
+ expect(console.warn).toBeCalledWith(
121
+ 'json-schema-form modify(): We highly recommend you to handle/report the returned `warnings` as they highlight possible bugs in your modifications. To mute this log, pass `muteLogging: true` to the config.'
122
+ );
123
+
124
+ console.warn.mockClear();
125
+ expect(result.warnings).toEqual([]);
126
+ });
127
+
128
+ it('given muteLogging, it does not log the warning', () => {
129
+ const result = modify(schemaPet, {
130
+ muteLogging: true,
131
+ });
132
+
133
+ expect(console.warn).not.toBeCalled();
134
+ expect(result.warnings).toEqual([]);
135
+ });
136
+ });
137
+
105
138
  describe('modify() - basic mutations', () => {
106
139
  it('replace base field', () => {
107
140
  const result = modify(schemaPet, {
@@ -119,7 +152,7 @@ describe('modify() - basic mutations', () => {
119
152
  },
120
153
  });
121
154
 
122
- expect(result).toMatchObject({
155
+ expect(result.schema).toMatchObject({
123
156
  properties: {
124
157
  pet_name: {
125
158
  title: 'Your pet name',
@@ -135,24 +168,30 @@ describe('modify() - basic mutations', () => {
135
168
  it('replace nested field', () => {
136
169
  const result = modify(schemaAddress, {
137
170
  fields: {
171
+ // You can use dot notation
138
172
  'address.street': {
139
173
  title: 'Street name',
140
174
  },
141
175
  'address.city': () => ({
142
176
  title: 'City name',
143
177
  }),
144
- address: () => {
178
+ // Or pass the native object
179
+ address: (fieldAttrs) => {
145
180
  return {
146
181
  properties: {
147
- number: { title: 'Door number' },
182
+ number: (nestedAttrs) => {
183
+ return {
184
+ 'x-test-siblings': Object.keys(fieldAttrs.properties),
185
+ title: `Door ${nestedAttrs.title}`,
186
+ };
187
+ },
148
188
  },
149
189
  };
150
190
  },
151
- // TODO: write test to nested field
152
191
  },
153
192
  });
154
193
 
155
- expect(result).toMatchObject({
194
+ expect(result.schema).toMatchObject({
156
195
  properties: {
157
196
  address: {
158
197
  properties: {
@@ -160,7 +199,8 @@ describe('modify() - basic mutations', () => {
160
199
  title: 'Street name',
161
200
  },
162
201
  number: {
163
- title: 'Door number',
202
+ title: 'Door Number',
203
+ 'x-test-siblings': ['street', 'number', 'city'],
164
204
  },
165
205
  city: {
166
206
  title: 'City name',
@@ -171,6 +211,44 @@ describe('modify() - basic mutations', () => {
171
211
  });
172
212
  });
173
213
 
214
+ it('replace fields that dont exist gets ignored', () => {
215
+ // IMPORTANT NOTE on this behavior:
216
+ // Context: At Remote we have a lot of global customization that run equally across multiple different JSON Schemas.
217
+ // With this, we avoid applying customizations to non-existing fields. (aka create fields)
218
+ // That's why we have the "create" config, specific to create new fields.
219
+ const result = modify(schemaPet, {
220
+ fields: {
221
+ unknown_field: {
222
+ title: 'This field does not exist in the original schema',
223
+ },
224
+ 'nested.field': {
225
+ title: 'Nop',
226
+ },
227
+ pet_name: {
228
+ title: 'New pet title',
229
+ },
230
+ },
231
+ });
232
+
233
+ expect(result.schema.properties.unknown_field).toBeUndefined();
234
+ expect(result.schema.properties.nested).toBeUndefined();
235
+ expect(result.schema.properties.pet_name).toEqual({
236
+ ...schemaPet.properties.pet_name,
237
+ title: 'New pet title',
238
+ });
239
+
240
+ expect(result.warnings).toEqual([
241
+ {
242
+ type: 'FIELD_TO_CHANGE_NOT_FOUND',
243
+ message: 'Changing field "unknown_field" was ignored because it does not exist.',
244
+ },
245
+ {
246
+ type: 'FIELD_TO_CHANGE_NOT_FOUND',
247
+ message: 'Changing field "nested.field" was ignored because it does not exist.',
248
+ },
249
+ ]);
250
+ });
251
+
174
252
  it('replace all fields', () => {
175
253
  const result = modify(schemaPet, {
176
254
  allFields: (fieldName, fieldAttrs) => {
@@ -188,7 +266,7 @@ describe('modify() - basic mutations', () => {
188
266
  },
189
267
  });
190
268
 
191
- expect(result).toMatchObject({
269
+ expect(result.schema).toMatchObject({
192
270
  properties: {
193
271
  has_pet: {
194
272
  dataFoo: 'abc',
@@ -203,6 +281,7 @@ describe('modify() - basic mutations', () => {
203
281
  styleDecimals: 2,
204
282
  },
205
283
  pet_address: {
284
+ // assert recursivity
206
285
  properties: {
207
286
  street: {
208
287
  dataFoo: 'abc',
@@ -213,7 +292,7 @@ describe('modify() - basic mutations', () => {
213
292
  });
214
293
  });
215
294
 
216
- it('replace field options (radio/select)', () => {
295
+ it('replace field attrs that are arrays (partial)', () => {
217
296
  const result = modify(schemaPet, {
218
297
  fields: {
219
298
  has_pet: (fieldAttrs) => {
@@ -239,7 +318,7 @@ describe('modify() - basic mutations', () => {
239
318
  },
240
319
  });
241
320
 
242
- expect(result).toMatchObject({
321
+ expect(result.schema).toMatchObject({
243
322
  properties: {
244
323
  has_pet: {
245
324
  oneOf: [
@@ -257,7 +336,30 @@ describe('modify() - basic mutations', () => {
257
336
  });
258
337
  });
259
338
 
260
- it('error message', () => {
339
+ it('replace field attrs that are arrays (full)', () => {
340
+ const result = modify(schemaPet, {
341
+ fields: {
342
+ has_pet: {
343
+ oneOf: [{ const: 'yaaas', title: 'YAAS!' }],
344
+ },
345
+ },
346
+ });
347
+
348
+ expect(result.schema).toMatchObject({
349
+ properties: {
350
+ has_pet: {
351
+ oneOf: [
352
+ {
353
+ const: 'yaaas',
354
+ title: 'YAAS!',
355
+ },
356
+ ],
357
+ },
358
+ },
359
+ });
360
+ });
361
+
362
+ it('customize x-jsf-errorMessage (shorthand)', () => {
261
363
  const result = modify(schemaPet, {
262
364
  fields: {
263
365
  pet_age: (fieldAttrs) => {
@@ -277,7 +379,7 @@ describe('modify() - basic mutations', () => {
277
379
  // ...
278
380
  });
279
381
 
280
- expect(result).toMatchObject({
382
+ expect(result.schema).toMatchObject({
281
383
  properties: {
282
384
  pet_age: {
283
385
  'x-jsf-errorMessage': {
@@ -297,4 +399,418 @@ describe('modify() - basic mutations', () => {
297
399
  },
298
400
  });
299
401
  });
402
+
403
+ it('customize x-jsf-presentation (shorthand)', () => {
404
+ const result = modify(schemaPet, {
405
+ fields: {
406
+ pet_age: () => {
407
+ return {
408
+ presentation: {
409
+ 'data-foo': 123,
410
+ },
411
+ };
412
+ },
413
+ 'pet_address.street': {
414
+ presentation: {
415
+ 'data-foo': 456,
416
+ },
417
+ },
418
+ },
419
+ });
420
+
421
+ const originalPetAgePresentation = schemaPet.properties.pet_age['x-jsf-presentation'];
422
+ expect(originalPetAgePresentation).toBeDefined();
423
+
424
+ const originalPetStreetPresentation =
425
+ schemaPet.properties.pet_address.properties.street['x-jsf-presentation'];
426
+ expect(originalPetStreetPresentation).toBeDefined();
427
+
428
+ expect(result.schema).toMatchObject({
429
+ properties: {
430
+ pet_age: {
431
+ 'x-jsf-presentation': {
432
+ ...originalPetAgePresentation,
433
+ 'data-foo': 123,
434
+ },
435
+ },
436
+ pet_address: {
437
+ properties: {
438
+ street: {
439
+ 'x-jsf-presentation': {
440
+ ...originalPetStreetPresentation,
441
+ 'data-foo': 456,
442
+ },
443
+ },
444
+ },
445
+ },
446
+ },
447
+ });
448
+ });
449
+ });
450
+
451
+ const schemaTickets = {
452
+ properties: {
453
+ age: {
454
+ title: 'Age',
455
+ type: 'integer',
456
+ },
457
+ quantity: {
458
+ title: 'Quantity',
459
+ type: 'integer',
460
+ },
461
+ has_premium: {
462
+ title: 'Has premium',
463
+ type: 'string',
464
+ },
465
+ premium_id: {
466
+ title: 'Premium ID',
467
+ type: 'boolean',
468
+ },
469
+ reason: {
470
+ title: 'Why not premium?',
471
+ type: 'string',
472
+ },
473
+ },
474
+ 'x-jsf-order': ['age', 'quantity', 'has_premium', 'premium_id', 'reason'],
475
+ allOf: [
476
+ {
477
+ // Empty conditional to sanity test empty cases
478
+ if: {},
479
+ then: {},
480
+ else: {},
481
+ },
482
+ // Create two conditionals to test both get matched
483
+ {
484
+ if: {
485
+ has_premium: {
486
+ const: 'yes',
487
+ },
488
+ required: ['has_premium'],
489
+ },
490
+ then: {
491
+ required: ['premium_id'],
492
+ },
493
+ else: {},
494
+ },
495
+ {
496
+ if: {
497
+ properties: {
498
+ has_premium: {
499
+ const: 'no',
500
+ },
501
+ },
502
+ required: ['has_premium'],
503
+ },
504
+ then: {
505
+ properties: {
506
+ reason: false,
507
+ },
508
+ },
509
+ else: {},
510
+ },
511
+ ],
512
+ };
513
+
514
+ describe('modify() - reoder fields', () => {
515
+ it('reorder fields - basic usage', () => {
516
+ const baseExample = {
517
+ properties: {
518
+ /* does not matter */
519
+ },
520
+ 'x-jsf-order': ['field_a', 'field_b', 'field_c', 'field_d'],
521
+ };
522
+ const result = modify(baseExample, {
523
+ orderRoot: ['field_c', 'field_b'],
524
+ });
525
+
526
+ // 💡 Note how the missing field (field_d) was added to the end as safety measure.
527
+ expect(result.schema).toMatchObject({
528
+ 'x-jsf-order': ['field_c', 'field_b', 'field_a', 'field_d'],
529
+ });
530
+
531
+ expect(result.warnings).toMatchObject([
532
+ {
533
+ type: 'ORDER_MISSING_FIELDS',
534
+ message:
535
+ 'Some fields got forgotten in the new order. They were automatically appended: field_a, field_d',
536
+ },
537
+ ]);
538
+ });
539
+
540
+ it('reorder fields - basic usage fallback', () => {
541
+ const baseExample = {
542
+ properties: {
543
+ /* does not matter */
544
+ },
545
+ };
546
+ const result = modify(baseExample, {
547
+ orderRoot: ['field_c', 'field_b'],
548
+ });
549
+
550
+ // Does not explode if it doesn't have an original order.
551
+ expect(result.schema).toMatchObject({
552
+ 'x-jsf-order': ['field_c', 'field_b'],
553
+ });
554
+
555
+ expect(result.warnings).toEqual([]);
556
+ });
557
+
558
+ it('reorder fields - as callback based on original order', () => {
559
+ const baseExample = {
560
+ properties: {
561
+ /* does not matter */
562
+ },
563
+ 'x-jsf-order': ['field_a', 'field_b', 'field_c', 'field_d'],
564
+ };
565
+ const result = modify(baseExample, {
566
+ orderRoot: (original) => original.reverse(),
567
+ });
568
+
569
+ expect(result.schema).toMatchObject({
570
+ 'x-jsf-order': ['field_d', 'field_c', 'field_b', 'field_a'],
571
+ });
572
+ });
573
+
574
+ it('reorder fields in fieldsets (through config.fields)', () => {
575
+ // NOTE: A better API is needed but we decided to not implement it yet
576
+ // as we didn't agreed on the best DX. Check PR #78 for proposed APIs.
577
+ // Until then this is the workaround.
578
+ // Note the warning "ORDER_MISSING_FIELDS" won't be added.
579
+
580
+ const baseExample = {
581
+ properties: {
582
+ address: {
583
+ properties: {
584
+ /* does not matter */
585
+ },
586
+ 'x-jsf-order': ['first_line', 'zipcode', 'city'],
587
+ },
588
+ age: {
589
+ /* ... */
590
+ },
591
+ },
592
+ 'x-jsf-order': ['address', 'age'],
593
+ };
594
+
595
+ const result = modify(baseExample, {
596
+ fields: {
597
+ address: (attrs) => {
598
+ // eslint-disable-next-line no-unused-vars
599
+ const [_firstLine, ...restOrder] = attrs['x-jsf-order'];
600
+ return { 'x-jsf-order': restOrder.reverse() }; // ['city', 'zipcode']
601
+ },
602
+ },
603
+ });
604
+
605
+ expect(result.schema).toMatchObject({
606
+ properties: {
607
+ address: {
608
+ // Note how first_line was NOT appended
609
+ 'x-jsf-order': ['city', 'zipcode'],
610
+ },
611
+ },
612
+ });
613
+ });
614
+ });
615
+
616
+ describe('modify() - create fields', () => {
617
+ it('create base field', () => {
618
+ const result = modify(schemaAddress, {
619
+ create: {
620
+ new_field: {
621
+ title: 'New field',
622
+ type: 'string',
623
+ },
624
+ address: {
625
+ someAttr: 'foo',
626
+ },
627
+ },
628
+ });
629
+
630
+ expect(result.schema).toMatchObject({
631
+ properties: {
632
+ new_field: {
633
+ title: 'New field',
634
+ type: 'string',
635
+ },
636
+ address: schemaAddress.properties.address,
637
+ },
638
+ });
639
+
640
+ // this is ignored because the field already exists
641
+ expect(result.schema.properties.address.someAttr).toBe(undefined);
642
+
643
+ expect(result.warnings).toEqual([
644
+ {
645
+ type: 'FIELD_TO_CREATE_EXISTS',
646
+ message: 'Creating field "address" was ignored because it already exists.',
647
+ },
648
+ ]);
649
+ });
650
+
651
+ it('create nested field', () => {
652
+ const result = modify(schemaAddress, {
653
+ create: {
654
+ // Pointer as string
655
+ 'address.state': {
656
+ title: 'State',
657
+ },
658
+ // Pointer as object
659
+ address: {
660
+ someAttr: 'foo',
661
+ properties: {
662
+ district: {
663
+ title: 'District',
664
+ },
665
+ },
666
+ },
667
+ // Ignore field street because the field already exists [1]
668
+ 'address.street': {
669
+ title: 'Foo',
670
+ },
671
+ },
672
+ });
673
+
674
+ expect(result.schema.properties.address.properties).toMatchObject({
675
+ ...schemaAddress.properties.address.properties,
676
+ state: {
677
+ title: 'State',
678
+ },
679
+ district: {
680
+ title: 'District',
681
+ },
682
+ });
683
+
684
+ // Ignore address.someAttr because the address itself already exists.
685
+ expect(result.schema.properties.address.someAttr).toBeUndefined();
686
+
687
+ // Ignore field street because it already exists [1]
688
+ expect(result.schema.properties.address.properties.street.title).toBe('Street');
689
+
690
+ expect(result.warnings).toEqual([
691
+ {
692
+ type: 'FIELD_TO_CREATE_EXISTS',
693
+ message: 'Creating field "address" was ignored because it already exists.',
694
+ },
695
+ {
696
+ type: 'FIELD_TO_CREATE_EXISTS',
697
+ message: 'Creating field "address.street" was ignored because it already exists.',
698
+ },
699
+ ]);
700
+ });
701
+ });
702
+
703
+ describe('modify() - pick fields', () => {
704
+ it('basic usage', () => {
705
+ const { schema, warnings } = modify(schemaTickets, {
706
+ pick: ['quantity'],
707
+ });
708
+
709
+ // Note how the other fields got removed from
710
+ // from the root properties, the "order" and "allOf".
711
+ expect(schema.properties).toEqual({
712
+ quantity: {
713
+ title: 'Quantity',
714
+ type: 'integer',
715
+ },
716
+ });
717
+ expect(schema.properties.age).toBeUndefined();
718
+ expect(schema.properties.has_premium).toBeUndefined();
719
+ expect(schema.properties.premium_id).toBeUndefined();
720
+
721
+ expect(schema['x-jsf-order']).toEqual(['quantity']);
722
+ expect(schema.allOf).toEqual([]); // conditional got removed.
723
+
724
+ expect(warnings).toHaveLength(0);
725
+ });
726
+
727
+ it('related conditionals are kept - (else)', () => {
728
+ const { schema, warnings } = modify(schemaTickets, {
729
+ pick: ['has_premium'],
730
+ });
731
+
732
+ expect(schema).toMatchObject({
733
+ properties: {
734
+ has_premium: {
735
+ title: 'Has premium',
736
+ },
737
+ premium_id: {
738
+ title: 'Premium ID',
739
+ },
740
+ reason: {
741
+ title: 'Why not premium?',
742
+ },
743
+ },
744
+ allOf: [schemaTickets.allOf[1], schemaTickets.allOf[2]],
745
+ });
746
+
747
+ expect(schema.properties.quantity).toBeUndefined();
748
+ expect(schema.properties.age).toBeUndefined();
749
+ expect(warnings).toEqual([
750
+ {
751
+ type: 'PICK_MISSED_FIELD',
752
+ message:
753
+ 'The picked fields are in conditionals that refeer other fields. They added automatically: "premium_id", "reason". Check "meta" for more details.',
754
+ meta: { premium_id: { path: 'allOf[1].then' }, reason: { path: 'allOf[2].then' } },
755
+ },
756
+ ]);
757
+ });
758
+
759
+ it('related conditionals are kept - (if)', () => {
760
+ const { schema, warnings } = modify(schemaTickets, {
761
+ pick: ['premium_id'],
762
+ });
763
+
764
+ expect(schema).toMatchObject({
765
+ properties: {
766
+ has_premium: {
767
+ title: 'Has premium',
768
+ },
769
+ premium_id: {
770
+ title: 'Premium ID',
771
+ },
772
+ },
773
+ allOf: [schemaTickets.allOf[0]],
774
+ });
775
+
776
+ expect(schema.properties.quantity).toBeUndefined();
777
+ expect(schema.properties.age).toBeUndefined();
778
+ expect(warnings).toEqual([
779
+ {
780
+ type: 'PICK_MISSED_FIELD',
781
+ message:
782
+ 'The picked fields are in conditionals that refeer other fields. They added automatically: "has_premium". Check "meta" for more details.',
783
+ meta: { has_premium: { path: 'allOf[1].if' } },
784
+ },
785
+ ]);
786
+ });
787
+
788
+ it('reorder only handles the picked fields', () => {
789
+ const { schema, warnings } = modify(schemaTickets, {
790
+ pick: ['age', 'quantity'],
791
+ orderRoot: (original) => original.reverse(),
792
+ });
793
+
794
+ // The order only includes those 2 fields
795
+ expect(schema['x-jsf-order']).toEqual(['quantity', 'age']);
796
+ // There are no warnings about forgotten fields.
797
+ expect(warnings).toHaveLength(0);
798
+
799
+ // Sanity check the result
800
+ expect(schema.properties.quantity).toBeDefined();
801
+ expect(schema.properties.age).toBeDefined();
802
+ expect(schema.properties.has_premium).toBeUndefined();
803
+ expect(schema.properties.premium_id).toBeUndefined();
804
+ expect(schema.allOf).toEqual([]);
805
+ });
806
+
807
+ // For later on when needed.
808
+ it.todo('ignore conditionals with unpicked fields');
809
+
810
+ it.todo('pick nested fields (fieldsets)');
811
+ /* Use cases:
812
+ - conditionals inside fieldstes. eg properties.family.allOf[0].if...
813
+ - conditional in the root pointing to nested fields: eg if properties.family.properties.simblings is 0 then hide properties.playTogether ...
814
+ - variations of each one of these similar to the existing tests.
815
+ */
300
816
  });