@remoteoss/json-schema-form 0.10.1-beta.0 → 0.11.0-dev.20240725074901

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