@elle96/ng-json-schema-manager 0.0.15 → 0.1.1
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,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
2
|
+
import { Injectable, EventEmitter, Output, Input, ChangeDetectionStrategy, ViewEncapsulation, Component, NgModule } from '@angular/core';
|
|
3
3
|
import * as i4 from '@angular/forms';
|
|
4
4
|
import { FormGroup, FormArray, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
5
5
|
import Ajv from 'ajv';
|
|
@@ -471,6 +471,39 @@ class JsonSchemaValidationService {
|
|
|
471
471
|
}
|
|
472
472
|
return validate.errors ?? [];
|
|
473
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* Validates the schema itself against its JSON Schema meta-schema.
|
|
476
|
+
* Returns a list of SchemaError describing what is wrong in the schema definition.
|
|
477
|
+
*/
|
|
478
|
+
validateSchema(schema) {
|
|
479
|
+
const draft = this.detectDraft(schema);
|
|
480
|
+
const ajv = this.getAjv(draft);
|
|
481
|
+
try {
|
|
482
|
+
const valid = ajv.validateSchema(schema);
|
|
483
|
+
if (valid)
|
|
484
|
+
return [];
|
|
485
|
+
return (ajv.errors ?? []).map((e) => ({
|
|
486
|
+
path: e.instancePath || '/',
|
|
487
|
+
message: e.message ?? e.keyword,
|
|
488
|
+
keyword: e.keyword,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
// ajv.validateSchema throws when the schema is so broken it cannot be compiled.
|
|
493
|
+
// The error message contains comma-separated individual violations — split them.
|
|
494
|
+
const raw = e instanceof Error ? e.message : String(e);
|
|
495
|
+
// Strip the "schema is invalid: " prefix if present
|
|
496
|
+
const body = raw.replace(/^schema is invalid:\s*/i, '');
|
|
497
|
+
// Each violation looks like "data/some/path must ..." — extract path and message
|
|
498
|
+
return body.split(', ').map((msg) => {
|
|
499
|
+
const trimmed = msg.trim();
|
|
500
|
+
const match = trimmed.match(/^(data[^\s]*)\s+(.+)$/);
|
|
501
|
+
return match
|
|
502
|
+
? { path: match[1].replace(/^data/, '') || '/', message: match[2], keyword: 'schema' }
|
|
503
|
+
: { path: '/', message: trimmed, keyword: 'schema' };
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
474
507
|
detectDraft(schema) {
|
|
475
508
|
const schemaId = schema.$schema ?? '';
|
|
476
509
|
if (schemaId.includes('draft-04')) {
|
|
@@ -557,6 +590,7 @@ class JsonSchemaNodeComponent {
|
|
|
557
590
|
errorsMap = new Map();
|
|
558
591
|
parent;
|
|
559
592
|
controlKey;
|
|
593
|
+
allowAdditionalProperties = false;
|
|
560
594
|
controlReplaced = new EventEmitter();
|
|
561
595
|
selectedOneOf = 0;
|
|
562
596
|
selectedAnyOf = new Set();
|
|
@@ -571,133 +605,121 @@ class JsonSchemaNodeComponent {
|
|
|
571
605
|
this.selectedAnyOf.add(0);
|
|
572
606
|
}
|
|
573
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Determina se un figlio di un object è "semplice" (primitive/enum/bool),
|
|
610
|
+
* quindi può stare in griglia 2-col, oppure "complesso" (object/array/combinator)
|
|
611
|
+
* che deve occupare la larghezza piena.
|
|
612
|
+
*/
|
|
613
|
+
isSimpleChild(key) {
|
|
614
|
+
const childSchema = this.schemaForKey(key);
|
|
615
|
+
if (childSchema.oneOf?.length || childSchema.anyOf?.length || childSchema.allOf?.length)
|
|
616
|
+
return false;
|
|
617
|
+
const t = this.schemaService.resolveType(childSchema);
|
|
618
|
+
return t !== 'object' && t !== 'array';
|
|
619
|
+
}
|
|
620
|
+
badgeClass(badge) {
|
|
621
|
+
return {
|
|
622
|
+
'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400': badge === 'Deprecated',
|
|
623
|
+
'bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400': badge === 'Read-only' || badge === 'Nullable',
|
|
624
|
+
'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400': badge === 'Write-only',
|
|
625
|
+
'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400': badge === 'Not constraint',
|
|
626
|
+
};
|
|
627
|
+
}
|
|
574
628
|
get nodeKind() {
|
|
575
|
-
if (this.schema.oneOf?.length)
|
|
629
|
+
if (this.schema.oneOf?.length)
|
|
576
630
|
return 'oneOf';
|
|
577
|
-
|
|
578
|
-
if (this.schema.anyOf?.length) {
|
|
631
|
+
if (this.schema.anyOf?.length)
|
|
579
632
|
return 'anyOf';
|
|
580
|
-
|
|
581
|
-
if (this.schema.allOf?.length) {
|
|
633
|
+
if (this.schema.allOf?.length)
|
|
582
634
|
return 'allOf';
|
|
583
|
-
}
|
|
584
635
|
const resolved = this.resolvedType;
|
|
585
|
-
if (resolved === 'object')
|
|
636
|
+
if (resolved === 'object')
|
|
586
637
|
return 'object';
|
|
587
|
-
|
|
588
|
-
if (resolved === 'array') {
|
|
638
|
+
if (resolved === 'array')
|
|
589
639
|
return 'array';
|
|
590
|
-
}
|
|
591
640
|
return 'primitive';
|
|
592
641
|
}
|
|
593
642
|
get resolvedType() {
|
|
594
643
|
return this.schemaService.resolveType(this.effectiveSchema) ?? 'string';
|
|
595
644
|
}
|
|
596
645
|
get inputKind() {
|
|
597
|
-
if (this.effectiveSchema.enum)
|
|
646
|
+
if (this.effectiveSchema.enum)
|
|
598
647
|
return 'select';
|
|
599
|
-
|
|
600
|
-
if (this.resolvedType === 'boolean') {
|
|
648
|
+
if (this.resolvedType === 'boolean')
|
|
601
649
|
return 'checkbox';
|
|
602
|
-
|
|
603
|
-
if (this.effectiveSchema.format === 'textarea' || (this.effectiveSchema.maxLength ?? 0) > 200) {
|
|
650
|
+
if (this.effectiveSchema.format === 'textarea' || (this.effectiveSchema.maxLength ?? 0) > 200)
|
|
604
651
|
return 'textarea';
|
|
605
|
-
}
|
|
606
652
|
return 'text';
|
|
607
653
|
}
|
|
608
654
|
get inputType() {
|
|
609
|
-
if (this.effectiveSchema.format === 'email')
|
|
655
|
+
if (this.effectiveSchema.format === 'email')
|
|
610
656
|
return 'email';
|
|
611
|
-
|
|
612
|
-
if (this.effectiveSchema.format === 'date') {
|
|
657
|
+
if (this.effectiveSchema.format === 'date')
|
|
613
658
|
return 'date';
|
|
614
|
-
|
|
615
|
-
if (this.effectiveSchema.format === 'time') {
|
|
659
|
+
if (this.effectiveSchema.format === 'time')
|
|
616
660
|
return 'time';
|
|
617
|
-
|
|
618
|
-
if (this.effectiveSchema.format === 'date-time') {
|
|
661
|
+
if (this.effectiveSchema.format === 'date-time')
|
|
619
662
|
return 'datetime-local';
|
|
620
|
-
|
|
621
|
-
if (this.resolvedType === 'number' || this.resolvedType === 'integer') {
|
|
663
|
+
if (this.resolvedType === 'number' || this.resolvedType === 'integer')
|
|
622
664
|
return 'number';
|
|
623
|
-
|
|
624
|
-
if (this.effectiveSchema.format === 'password') {
|
|
665
|
+
if (this.effectiveSchema.format === 'password')
|
|
625
666
|
return 'password';
|
|
626
|
-
}
|
|
627
667
|
return 'text';
|
|
628
668
|
}
|
|
629
|
-
get controlAsGroup() {
|
|
630
|
-
|
|
669
|
+
get controlAsGroup() { return this.control; }
|
|
670
|
+
get controlAsFormControl() { return this.control; }
|
|
671
|
+
get controlAsArray() { return this.control; }
|
|
672
|
+
get arrayControls() { return this.controlAsArray.controls; }
|
|
673
|
+
get objectKeys() { return Object.keys(this.controlAsGroup.controls); }
|
|
674
|
+
get simpleKeys() {
|
|
675
|
+
return this.objectKeys.filter(k => this.isSimpleChild(k));
|
|
631
676
|
}
|
|
632
|
-
get
|
|
633
|
-
return this.
|
|
634
|
-
}
|
|
635
|
-
get controlAsArray() {
|
|
636
|
-
return this.control;
|
|
637
|
-
}
|
|
638
|
-
get arrayControls() {
|
|
639
|
-
return this.controlAsArray.controls;
|
|
640
|
-
}
|
|
641
|
-
get objectKeys() {
|
|
642
|
-
return Object.keys(this.controlAsGroup.controls);
|
|
677
|
+
get complexKeys() {
|
|
678
|
+
return this.objectKeys.filter(k => !this.isSimpleChild(k));
|
|
643
679
|
}
|
|
644
680
|
get canAddProperty() {
|
|
681
|
+
if (!this.allowAdditionalProperties)
|
|
682
|
+
return false;
|
|
645
683
|
return this.effectiveSchema.additionalProperties !== false || !!this.effectiveSchema.patternProperties;
|
|
646
684
|
}
|
|
647
|
-
get errorsForPath() {
|
|
648
|
-
return this.errorsMap.get(this.path) ?? [];
|
|
649
|
-
}
|
|
685
|
+
get errorsForPath() { return this.errorsMap.get(this.path) ?? []; }
|
|
650
686
|
get metaBadges() {
|
|
651
687
|
const badges = [];
|
|
652
|
-
if (this.effectiveSchema.deprecated)
|
|
688
|
+
if (this.effectiveSchema.deprecated)
|
|
653
689
|
badges.push('Deprecated');
|
|
654
|
-
|
|
655
|
-
if (this.effectiveSchema.readOnly) {
|
|
690
|
+
if (this.effectiveSchema.readOnly)
|
|
656
691
|
badges.push('Read-only');
|
|
657
|
-
|
|
658
|
-
if (this.effectiveSchema.writeOnly) {
|
|
692
|
+
if (this.effectiveSchema.writeOnly)
|
|
659
693
|
badges.push('Write-only');
|
|
660
|
-
|
|
661
|
-
if (this.effectiveSchema.nullable) {
|
|
694
|
+
if (this.effectiveSchema.nullable)
|
|
662
695
|
badges.push('Nullable');
|
|
663
|
-
|
|
664
|
-
if (this.effectiveSchema.not) {
|
|
696
|
+
if (this.effectiveSchema.not)
|
|
665
697
|
badges.push('Not constraint');
|
|
666
|
-
}
|
|
667
698
|
return badges;
|
|
668
699
|
}
|
|
669
700
|
get activeVariant() {
|
|
670
|
-
if (this.schema.oneOf?.length)
|
|
701
|
+
if (this.schema.oneOf?.length)
|
|
671
702
|
return this.schema.oneOf[this.selectedOneOf] ?? null;
|
|
672
|
-
}
|
|
673
703
|
if (this.schema.anyOf?.length) {
|
|
674
|
-
const selections = Array.from(this.selectedAnyOf).map((
|
|
704
|
+
const selections = Array.from(this.selectedAnyOf).map((i) => this.schema.anyOf[i]);
|
|
675
705
|
return selections.length ? this.mergeSchemas(selections) : this.schema.anyOf[0] ?? null;
|
|
676
706
|
}
|
|
677
|
-
if (this.schema.allOf?.length)
|
|
707
|
+
if (this.schema.allOf?.length)
|
|
678
708
|
return this.mergeSchemas([this.schema, ...this.schema.allOf]);
|
|
679
|
-
}
|
|
680
709
|
return this.effectiveSchema;
|
|
681
710
|
}
|
|
682
711
|
get mergedAllOf() {
|
|
683
712
|
return this.schema.allOf?.length ? [this.schema, ...this.schema.allOf] : [this.schema];
|
|
684
713
|
}
|
|
685
|
-
isRequired(key) {
|
|
686
|
-
return this.effectiveSchema.required?.includes(key) ?? false;
|
|
687
|
-
}
|
|
714
|
+
isRequired(key) { return this.effectiveSchema.required?.includes(key) ?? false; }
|
|
688
715
|
schemaForKey(key) {
|
|
689
716
|
return (this.effectiveSchema.properties?.[key] ??
|
|
690
717
|
this.schemaService.resolveDynamicSchema(this.effectiveSchema, key) ??
|
|
691
718
|
{});
|
|
692
719
|
}
|
|
693
|
-
isDynamicKey(key) {
|
|
694
|
-
return !this.effectiveSchema.properties?.[key];
|
|
695
|
-
}
|
|
720
|
+
isDynamicKey(key) { return !this.effectiveSchema.properties?.[key]; }
|
|
696
721
|
pathForChild(key) {
|
|
697
|
-
|
|
698
|
-
return `/${key}`;
|
|
699
|
-
}
|
|
700
|
-
return `${this.path}/${key}`;
|
|
722
|
+
return this.path === '' ? `/${key}` : `${this.path}/${key}`;
|
|
701
723
|
}
|
|
702
724
|
addDynamicProperty() {
|
|
703
725
|
this.propertyError = '';
|
|
@@ -726,135 +748,107 @@ class JsonSchemaNodeComponent {
|
|
|
726
748
|
this.controlAsGroup.addControl(key, control);
|
|
727
749
|
this.newPropertyKey = '';
|
|
728
750
|
}
|
|
729
|
-
removeDynamicKey(key) {
|
|
730
|
-
this.controlAsGroup.removeControl(key);
|
|
731
|
-
}
|
|
751
|
+
removeDynamicKey(key) { this.controlAsGroup.removeControl(key); }
|
|
732
752
|
schemaForIndex(index) {
|
|
733
753
|
const prefixItems = this.effectiveSchema.prefixItems ?? [];
|
|
734
|
-
if (prefixItems[index])
|
|
754
|
+
if (prefixItems[index])
|
|
735
755
|
return prefixItems[index];
|
|
736
|
-
|
|
737
|
-
if (this.effectiveSchema.items && this.effectiveSchema.items !== true) {
|
|
756
|
+
if (this.effectiveSchema.items && this.effectiveSchema.items !== true)
|
|
738
757
|
return this.effectiveSchema.items;
|
|
739
|
-
|
|
740
|
-
if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true) {
|
|
758
|
+
if (this.effectiveSchema.additionalItems && this.effectiveSchema.additionalItems !== true)
|
|
741
759
|
return this.effectiveSchema.additionalItems;
|
|
742
|
-
|
|
743
|
-
if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true) {
|
|
760
|
+
if (this.effectiveSchema.unevaluatedItems && this.effectiveSchema.unevaluatedItems !== true)
|
|
744
761
|
return this.effectiveSchema.unevaluatedItems;
|
|
745
|
-
}
|
|
746
762
|
return {};
|
|
747
763
|
}
|
|
748
764
|
get canAddArrayItem() {
|
|
749
|
-
|
|
765
|
+
return !(this.effectiveSchema.items === false ||
|
|
750
766
|
this.effectiveSchema.additionalItems === false ||
|
|
751
|
-
this.effectiveSchema.unevaluatedItems === false)
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
return true;
|
|
767
|
+
this.effectiveSchema.unevaluatedItems === false);
|
|
755
768
|
}
|
|
756
769
|
addArrayItem() {
|
|
757
|
-
if (!this.canAddArrayItem)
|
|
770
|
+
if (!this.canAddArrayItem)
|
|
758
771
|
return;
|
|
759
|
-
}
|
|
760
772
|
const formArray = this.controlAsArray;
|
|
761
773
|
const index = formArray.length;
|
|
762
|
-
|
|
763
|
-
formArray.push(this.schemaService.buildControl(schemaForItem, undefined, false));
|
|
774
|
+
formArray.push(this.schemaService.buildControl(this.schemaForIndex(index), undefined, false));
|
|
764
775
|
formArray.markAsDirty();
|
|
765
776
|
}
|
|
766
777
|
removeArrayItem(index) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
formArray.markAsDirty();
|
|
778
|
+
this.controlAsArray.removeAt(index);
|
|
779
|
+
this.controlAsArray.markAsDirty();
|
|
770
780
|
}
|
|
771
781
|
onOneOfChange() {
|
|
772
|
-
if (!this.activeVariant)
|
|
782
|
+
if (!this.activeVariant)
|
|
773
783
|
return;
|
|
774
|
-
}
|
|
775
784
|
this.replaceControl(this.schemaService.buildControl(this.activeVariant, this.control.value, this.required));
|
|
776
785
|
}
|
|
777
786
|
toggleAnyOf(index) {
|
|
778
|
-
if (this.selectedAnyOf.has(index))
|
|
787
|
+
if (this.selectedAnyOf.has(index))
|
|
779
788
|
this.selectedAnyOf.delete(index);
|
|
780
|
-
|
|
781
|
-
else {
|
|
789
|
+
else
|
|
782
790
|
this.selectedAnyOf.add(index);
|
|
783
|
-
}
|
|
784
791
|
const variant = this.activeVariant;
|
|
785
|
-
if (variant)
|
|
792
|
+
if (variant)
|
|
786
793
|
this.replaceControl(this.schemaService.buildControl(variant, this.control.value, this.required));
|
|
787
|
-
}
|
|
788
794
|
}
|
|
789
795
|
replaceControl(control) {
|
|
790
796
|
if (this.parent) {
|
|
791
|
-
if (this.parent instanceof FormGroup && typeof this.controlKey === 'string')
|
|
797
|
+
if (this.parent instanceof FormGroup && typeof this.controlKey === 'string')
|
|
792
798
|
this.parent.setControl(this.controlKey, control);
|
|
793
|
-
|
|
794
|
-
if (this.parent instanceof FormArray && typeof this.controlKey === 'number') {
|
|
799
|
+
if (this.parent instanceof FormArray && typeof this.controlKey === 'number')
|
|
795
800
|
this.parent.setControl(this.controlKey, control);
|
|
796
|
-
}
|
|
797
801
|
this.control = control;
|
|
798
802
|
return;
|
|
799
803
|
}
|
|
800
804
|
this.control = control;
|
|
801
805
|
this.controlReplaced.emit(control);
|
|
802
806
|
}
|
|
803
|
-
childControl(key) {
|
|
804
|
-
return this.controlAsGroup.get(key);
|
|
805
|
-
}
|
|
807
|
+
childControl(key) { return this.controlAsGroup.get(key); }
|
|
806
808
|
get effectiveSchema() {
|
|
807
809
|
let schema = this.schema;
|
|
808
|
-
if (schema.allOf?.length)
|
|
810
|
+
if (schema.allOf?.length)
|
|
809
811
|
schema = this.mergeSchemas([schema, ...schema.allOf]);
|
|
810
|
-
}
|
|
811
812
|
schema = this.applyConditional(schema);
|
|
812
813
|
schema = this.applyDependencies(schema);
|
|
813
814
|
return schema;
|
|
814
815
|
}
|
|
815
816
|
applyConditional(schema) {
|
|
816
|
-
if (!schema.if)
|
|
817
|
+
if (!schema.if)
|
|
817
818
|
return schema;
|
|
818
|
-
}
|
|
819
819
|
const errors = this.validation.validate(schema.if, this.control.value);
|
|
820
820
|
const branch = errors.length === 0 ? schema.then : schema.else;
|
|
821
|
-
if (!branch)
|
|
821
|
+
if (!branch)
|
|
822
822
|
return schema;
|
|
823
|
-
}
|
|
824
823
|
return this.mergeSchemas([schema, branch]);
|
|
825
824
|
}
|
|
826
825
|
applyDependencies(schema) {
|
|
827
826
|
const value = this.control.value;
|
|
828
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
827
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
829
828
|
return schema;
|
|
830
|
-
}
|
|
831
829
|
let merged = { ...schema };
|
|
832
830
|
const valueKeys = Object.keys(value);
|
|
833
831
|
if (schema.dependentRequired) {
|
|
834
832
|
for (const key of valueKeys) {
|
|
835
833
|
const required = schema.dependentRequired[key];
|
|
836
|
-
if (required?.length)
|
|
834
|
+
if (required?.length)
|
|
837
835
|
merged = this.mergeSchemas([merged, { required }]);
|
|
838
|
-
}
|
|
839
836
|
}
|
|
840
837
|
}
|
|
841
838
|
if (schema.dependentSchemas) {
|
|
842
839
|
for (const key of valueKeys) {
|
|
843
840
|
const depSchema = schema.dependentSchemas[key];
|
|
844
|
-
if (depSchema)
|
|
841
|
+
if (depSchema)
|
|
845
842
|
merged = this.mergeSchemas([merged, depSchema]);
|
|
846
|
-
}
|
|
847
843
|
}
|
|
848
844
|
}
|
|
849
845
|
if (schema.dependencies) {
|
|
850
846
|
for (const key of valueKeys) {
|
|
851
847
|
const dep = schema.dependencies[key];
|
|
852
|
-
if (Array.isArray(dep))
|
|
848
|
+
if (Array.isArray(dep))
|
|
853
849
|
merged = this.mergeSchemas([merged, { required: dep }]);
|
|
854
|
-
|
|
855
|
-
else if (dep) {
|
|
850
|
+
else if (dep)
|
|
856
851
|
merged = this.mergeSchemas([merged, dep]);
|
|
857
|
-
}
|
|
858
852
|
}
|
|
859
853
|
}
|
|
860
854
|
return merged;
|
|
@@ -877,306 +871,377 @@ class JsonSchemaNodeComponent {
|
|
|
877
871
|
}, {});
|
|
878
872
|
}
|
|
879
873
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaNodeComponent, deps: [{ token: JsonSchemaFormService }, { token: JsonSchemaValidationService }], target: i0.ɵɵFactoryTarget.Component });
|
|
880
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaNodeComponent, isStandalone: true, selector: "jsm-schema-node", inputs: { schema: "schema", control: "control", label: "label", required: "required", path: "path", errorsMap: "errorsMap", parent: "parent", controlKey: "controlKey" }, outputs: { controlReplaced: "controlReplaced" }, ngImport: i0, template: `
|
|
881
|
-
<ng-container [ngSwitch]="nodeKind">
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
<
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
class="
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
[
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
[
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
<
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
<
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
[controlKey]="controlKey"
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
</
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
[
|
|
1007
|
-
[
|
|
1008
|
-
[
|
|
1009
|
-
[
|
|
1010
|
-
[
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
<
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
>
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
</
|
|
1098
|
-
</div>
|
|
1099
|
-
|
|
1100
|
-
<
|
|
1101
|
-
<
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
class="w-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
<
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
874
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaNodeComponent, isStandalone: true, selector: "jsm-schema-node", inputs: { schema: "schema", control: "control", label: "label", required: "required", path: "path", errorsMap: "errorsMap", parent: "parent", controlKey: "controlKey", allowAdditionalProperties: "allowAdditionalProperties" }, outputs: { controlReplaced: "controlReplaced" }, ngImport: i0, template: `
|
|
875
|
+
<ng-container [ngSwitch]="nodeKind">
|
|
876
|
+
|
|
877
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
878
|
+
oneOf — tab bar: ogni opzione è una tab, nessuna icona
|
|
879
|
+
═══════════════════════════════════════════════════════════ -->
|
|
880
|
+
<div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
881
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
882
|
+
<div>
|
|
883
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
884
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
885
|
+
</div>
|
|
886
|
+
<span class="ml-3 shrink-0 rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-blue-700 dark:text-blue-400">oneOf</span>
|
|
887
|
+
</div>
|
|
888
|
+
<div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
|
|
889
|
+
<button
|
|
890
|
+
*ngFor="let option of schema.oneOf; let i = index"
|
|
891
|
+
type="button"
|
|
892
|
+
class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
|
|
893
|
+
[class.border-indigo-500]="selectedOneOf === i"
|
|
894
|
+
[class.text-indigo-600]="selectedOneOf === i"
|
|
895
|
+
[class.dark:text-indigo-400]="selectedOneOf === i"
|
|
896
|
+
[class.font-semibold]="selectedOneOf === i"
|
|
897
|
+
[class.border-transparent]="selectedOneOf !== i"
|
|
898
|
+
[class.text-slate-500]="selectedOneOf !== i"
|
|
899
|
+
[class.dark:text-slate-400]="selectedOneOf !== i"
|
|
900
|
+
(click)="selectedOneOf = i; onOneOfChange()"
|
|
901
|
+
>{{ option.title || ('Option ' + (i + 1)) }}</button>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="p-4" *ngIf="activeVariant">
|
|
904
|
+
<jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
905
|
+
[path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
|
|
906
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
907
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
912
|
+
anyOf — pill toggle, nessuna icona
|
|
913
|
+
═══════════════════════════════════════════════════════════ -->
|
|
914
|
+
<div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
915
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
916
|
+
<div>
|
|
917
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
918
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
919
|
+
</div>
|
|
920
|
+
<span class="ml-3 shrink-0 rounded-full bg-violet-100 dark:bg-violet-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-violet-700 dark:text-violet-400">anyOf</span>
|
|
921
|
+
</div>
|
|
922
|
+
<div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
|
|
923
|
+
<label
|
|
924
|
+
*ngFor="let option of schema.anyOf; let i = index"
|
|
925
|
+
class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium cursor-pointer transition-colors select-none"
|
|
926
|
+
[class.border-indigo-400]="selectedAnyOf.has(i)"
|
|
927
|
+
[class.bg-indigo-50]="selectedAnyOf.has(i)"
|
|
928
|
+
[class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
|
|
929
|
+
[class.text-indigo-700]="selectedAnyOf.has(i)"
|
|
930
|
+
[class.dark:text-indigo-400]="selectedAnyOf.has(i)"
|
|
931
|
+
[class.border-slate-200]="!selectedAnyOf.has(i)"
|
|
932
|
+
[class.dark:border-slate-600]="!selectedAnyOf.has(i)"
|
|
933
|
+
[class.text-slate-600]="!selectedAnyOf.has(i)"
|
|
934
|
+
[class.dark:text-slate-400]="!selectedAnyOf.has(i)"
|
|
935
|
+
>
|
|
936
|
+
<input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
|
|
937
|
+
{{ option.title || ('Option ' + (i + 1)) }}
|
|
938
|
+
</label>
|
|
939
|
+
</div>
|
|
940
|
+
<div class="p-4" *ngIf="activeVariant">
|
|
941
|
+
<jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
942
|
+
[path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
|
|
943
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
944
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
945
|
+
</div>
|
|
946
|
+
</div>
|
|
947
|
+
|
|
948
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
949
|
+
allOf — sezioni in colonna con bordo sinistro
|
|
950
|
+
═══════════════════════════════════════════════════════════ -->
|
|
951
|
+
<div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
952
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
953
|
+
<div>
|
|
954
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
955
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
956
|
+
</div>
|
|
957
|
+
<span class="ml-3 shrink-0 rounded-full bg-teal-100 dark:bg-teal-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-teal-700 dark:text-teal-400">allOf</span>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="p-4 space-y-4">
|
|
960
|
+
<div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
|
|
961
|
+
<jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
962
|
+
[path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
|
|
963
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
964
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
|
|
969
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
970
|
+
object — fieldset con griglia SOLO per campi primitivi.
|
|
971
|
+
Logica layout:
|
|
972
|
+
- I figli vengono classificati come "semplici" (primitive/enum/bool)
|
|
973
|
+
o "complessi" (object/array/oneOf/anyOf/allOf).
|
|
974
|
+
- I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
|
|
975
|
+
- I complessi vanno sempre in colonna piena (col-span-2).
|
|
976
|
+
═══════════════════════════════════════════════════════════ -->
|
|
977
|
+
<fieldset *ngSwitchCase="'object'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-4 transition-shadow focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
|
|
978
|
+
<legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
|
|
979
|
+
|
|
980
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
|
|
981
|
+
|
|
982
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
|
|
983
|
+
<span *ngFor="let badge of metaBadges"
|
|
984
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
985
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
986
|
+
</div>
|
|
987
|
+
|
|
988
|
+
<!-- campi semplici (string/number/bool/enum) → griglia 2 colonne -->
|
|
989
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
|
|
990
|
+
<ng-container *ngFor="let key of simpleKeys">
|
|
991
|
+
<div class="flex flex-col gap-1">
|
|
992
|
+
<div *ngIf="isDynamicKey(key)"
|
|
993
|
+
class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
|
|
994
|
+
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
|
|
995
|
+
<button type="button"
|
|
996
|
+
class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
997
|
+
(click)="removeDynamicKey(key)">Remove</button>
|
|
998
|
+
</div>
|
|
999
|
+
<jsm-schema-node
|
|
1000
|
+
[schema]="schemaForKey(key)"
|
|
1001
|
+
[control]="childControl(key)"
|
|
1002
|
+
[parent]="controlAsGroup"
|
|
1003
|
+
[controlKey]="key"
|
|
1004
|
+
[path]="pathForChild(key)"
|
|
1005
|
+
[errorsMap]="errorsMap"
|
|
1006
|
+
[label]="key"
|
|
1007
|
+
[required]="isRequired(key)"
|
|
1008
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1009
|
+
(controlReplaced)="controlReplaced.emit($event)">
|
|
1010
|
+
</jsm-schema-node>
|
|
1011
|
+
</div>
|
|
1012
|
+
</ng-container>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<!-- campi complessi (object/array/combinator) → colonna piena -->
|
|
1016
|
+
<div class="space-y-4 mt-4" *ngIf="complexKeys.length > 0" [formGroup]="controlAsGroup">
|
|
1017
|
+
<ng-container *ngFor="let key of complexKeys">
|
|
1018
|
+
<div class="flex flex-col gap-1">
|
|
1019
|
+
<div *ngIf="isDynamicKey(key)"
|
|
1020
|
+
class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
|
|
1021
|
+
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
|
|
1022
|
+
<button type="button"
|
|
1023
|
+
class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
1024
|
+
(click)="removeDynamicKey(key)">Remove</button>
|
|
1025
|
+
</div>
|
|
1026
|
+
<jsm-schema-node
|
|
1027
|
+
[schema]="schemaForKey(key)"
|
|
1028
|
+
[control]="childControl(key)"
|
|
1029
|
+
[parent]="controlAsGroup"
|
|
1030
|
+
[controlKey]="key"
|
|
1031
|
+
[path]="pathForChild(key)"
|
|
1032
|
+
[errorsMap]="errorsMap"
|
|
1033
|
+
[label]="key"
|
|
1034
|
+
[required]="isRequired(key)"
|
|
1035
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1036
|
+
(controlReplaced)="controlReplaced.emit($event)">
|
|
1037
|
+
</jsm-schema-node>
|
|
1038
|
+
</div>
|
|
1039
|
+
</ng-container>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
<!-- add dynamic property -->
|
|
1043
|
+
<div *ngIf="canAddProperty" class="mt-4 rounded-lg border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/40 p-3 space-y-2">
|
|
1044
|
+
<p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
|
|
1045
|
+
<div class="flex gap-2">
|
|
1046
|
+
<input
|
|
1047
|
+
class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-1.5 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
|
|
1048
|
+
[(ngModel)]="newPropertyKey" placeholder="propertyName" />
|
|
1049
|
+
<button type="button"
|
|
1050
|
+
class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95"
|
|
1051
|
+
(click)="addDynamicProperty()">Add</button>
|
|
1052
|
+
</div>
|
|
1053
|
+
<p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1054
|
+
<!-- icona errore: unico caso in cui ha senso, segnala un problema -->
|
|
1055
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1056
|
+
{{ propertyError }}
|
|
1057
|
+
</p>
|
|
1058
|
+
</div>
|
|
1059
|
+
|
|
1060
|
+
<div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
|
|
1061
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1062
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1063
|
+
{{ error }}
|
|
1064
|
+
</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
</fieldset>
|
|
1067
|
+
|
|
1068
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1069
|
+
array — sempre in colonna piena, items in colonna
|
|
1070
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1071
|
+
<div *ngSwitchCase="'array'" class="space-y-3">
|
|
1072
|
+
<div class="flex items-start justify-between gap-2">
|
|
1073
|
+
<div>
|
|
1074
|
+
<p class="text-sm font-semibold text-slate-800">
|
|
1075
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1076
|
+
</p>
|
|
1077
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
|
|
1078
|
+
</div>
|
|
1079
|
+
<!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
|
|
1080
|
+
<button type="button"
|
|
1081
|
+
class="shrink-0 flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
1082
|
+
(click)="addArrayItem()" [disabled]="!canAddArrayItem">
|
|
1083
|
+
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1084
|
+
Add item
|
|
1085
|
+
</button>
|
|
1086
|
+
</div>
|
|
1087
|
+
|
|
1088
|
+
<div *ngIf="effectiveSchema.contains" class="rounded-md bg-indigo-50 dark:bg-indigo-900/30 px-3 py-1.5 text-xs text-indigo-700 dark:text-indigo-400">
|
|
1089
|
+
Must contain matching item(s)
|
|
1090
|
+
<span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
|
|
1091
|
+
<span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
|
|
1092
|
+
</div>
|
|
1093
|
+
|
|
1094
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
|
|
1095
|
+
<span *ngFor="let badge of metaBadges"
|
|
1096
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
1097
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
1098
|
+
</div>
|
|
1099
|
+
|
|
1100
|
+
<div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
|
|
1101
|
+
<div *ngFor="let item of arrayControls; let i = index"
|
|
1102
|
+
class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-3 shadow-sm transition focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
|
|
1103
|
+
<div class="flex items-center justify-between mb-3">
|
|
1104
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-[11px] font-bold text-indigo-600 dark:text-indigo-400">{{ i + 1 }}</span>
|
|
1105
|
+
<button type="button"
|
|
1106
|
+
class="rounded-md border border-red-200 dark:border-red-800 px-2 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 transition hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
1107
|
+
(click)="removeArrayItem(i)">Remove</button>
|
|
1108
|
+
</div>
|
|
1109
|
+
<jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
|
|
1110
|
+
[controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
|
|
1111
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1112
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
<ng-template #emptyArray>
|
|
1117
|
+
<div class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-slate-200 dark:border-slate-700 py-8 text-slate-400 dark:text-slate-500 text-xs">
|
|
1118
|
+
No items yet — click "Add item" to start.
|
|
1119
|
+
</div>
|
|
1120
|
+
</ng-template>
|
|
1121
|
+
|
|
1122
|
+
<div *ngIf="errorsForPath.length" class="space-y-1">
|
|
1123
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1124
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1125
|
+
{{ error }}
|
|
1126
|
+
</p>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1131
|
+
primitive — sempre in colonna, nessuna icona decorativa.
|
|
1132
|
+
Icona errore solo nei messaggi di validazione.
|
|
1133
|
+
Icona chevron nel select: funzionale (indica dropdown).
|
|
1134
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1135
|
+
<div *ngSwitchDefault class="space-y-1.5">
|
|
1136
|
+
<ng-container *ngIf="inputKind !== 'checkbox'">
|
|
1137
|
+
<label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
|
|
1138
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1139
|
+
</label>
|
|
1140
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
|
|
1141
|
+
</ng-container>
|
|
1142
|
+
|
|
1143
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
|
|
1144
|
+
<span *ngFor="let badge of metaBadges"
|
|
1145
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
1146
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
1147
|
+
</div>
|
|
1148
|
+
|
|
1149
|
+
<ng-container [ngSwitch]="inputKind">
|
|
1150
|
+
|
|
1151
|
+
<input *ngSwitchCase="'text'"
|
|
1152
|
+
class="w-full rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
|
|
1153
|
+
[class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1154
|
+
[class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1155
|
+
[class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1156
|
+
[class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1157
|
+
[class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1158
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1159
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1160
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1161
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1162
|
+
[class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1163
|
+
[class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1164
|
+
[attr.type]="inputType"
|
|
1165
|
+
[attr.placeholder]="schema.title || label"
|
|
1166
|
+
[formControl]="controlAsFormControl"
|
|
1167
|
+
/>
|
|
1168
|
+
|
|
1169
|
+
<textarea *ngSwitchCase="'textarea'" rows="4"
|
|
1170
|
+
class="w-full resize-y rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
|
|
1171
|
+
[class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1172
|
+
[class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1173
|
+
[class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1174
|
+
[class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1175
|
+
[class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1176
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1177
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1178
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1179
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1180
|
+
[class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1181
|
+
[class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1182
|
+
[attr.placeholder]="schema.title || label"
|
|
1183
|
+
[formControl]="controlAsFormControl"
|
|
1184
|
+
></textarea>
|
|
1185
|
+
|
|
1186
|
+
<!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
|
|
1187
|
+
<div *ngSwitchCase="'select'" class="relative">
|
|
1188
|
+
<select
|
|
1189
|
+
class="w-full appearance-none rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 px-3 py-2 pr-8 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
|
|
1190
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1191
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1192
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1193
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1194
|
+
[formControl]="controlAsFormControl">
|
|
1195
|
+
<option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
|
|
1196
|
+
</select>
|
|
1197
|
+
<svg class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500"
|
|
1198
|
+
viewBox="0 0 16 16" fill="none">
|
|
1199
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1200
|
+
</svg>
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1203
|
+
<!-- checkbox come toggle switch -->
|
|
1204
|
+
<label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
|
|
1205
|
+
<span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
|
|
1206
|
+
[class.bg-indigo-600]="controlAsFormControl.value"
|
|
1207
|
+
[class.bg-slate-300]="!controlAsFormControl.value"
|
|
1208
|
+
[class.dark:bg-slate-600]="!controlAsFormControl.value">
|
|
1209
|
+
<input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
|
|
1210
|
+
<span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
|
|
1211
|
+
[class.translate-x-4]="controlAsFormControl.value"
|
|
1212
|
+
[class.translate-x-0.5]="!controlAsFormControl.value"></span>
|
|
1213
|
+
</span>
|
|
1214
|
+
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
1215
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1216
|
+
</span>
|
|
1217
|
+
</label>
|
|
1218
|
+
|
|
1219
|
+
</ng-container>
|
|
1220
|
+
|
|
1221
|
+
<p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
|
|
1222
|
+
|
|
1223
|
+
<p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
|
|
1224
|
+
e.g. {{ effectiveSchema.examples?.[0] }}
|
|
1225
|
+
</p>
|
|
1226
|
+
|
|
1227
|
+
<p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
|
|
1228
|
+
{{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
|
|
1229
|
+
</p>
|
|
1230
|
+
|
|
1231
|
+
<!-- icona errore: funzionale, segnala un problema di validazione -->
|
|
1232
|
+
<div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
|
|
1233
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1234
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
|
|
1235
|
+
<circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
|
|
1236
|
+
<path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1237
|
+
</svg>
|
|
1238
|
+
{{ error }}
|
|
1239
|
+
</p>
|
|
1240
|
+
</div>
|
|
1241
|
+
</div>
|
|
1242
|
+
|
|
1243
|
+
</ng-container>
|
|
1244
|
+
`, isInline: true, dependencies: [{ kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties"], outputs: ["controlReplaced"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i3.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i3.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1180
1245
|
}
|
|
1181
1246
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaNodeComponent, decorators: [{
|
|
1182
1247
|
type: Component,
|
|
@@ -1184,305 +1249,377 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1184
1249
|
selector: 'jsm-schema-node',
|
|
1185
1250
|
standalone: true,
|
|
1186
1251
|
imports: [CommonModule, FormsModule, ReactiveFormsModule],
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
[
|
|
1201
|
-
|
|
1202
|
-
>
|
|
1203
|
-
<
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
[
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
[
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
</
|
|
1295
|
-
|
|
1296
|
-
<div class="
|
|
1297
|
-
<
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
[
|
|
1315
|
-
[
|
|
1316
|
-
[
|
|
1317
|
-
[
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
<
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
</
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
>
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
<
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
</
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
<
|
|
1452
|
-
*
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1252
|
+
encapsulation: ViewEncapsulation.None,
|
|
1253
|
+
template: `
|
|
1254
|
+
<ng-container [ngSwitch]="nodeKind">
|
|
1255
|
+
|
|
1256
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1257
|
+
oneOf — tab bar: ogni opzione è una tab, nessuna icona
|
|
1258
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1259
|
+
<div *ngSwitchCase="'oneOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
1260
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
1261
|
+
<div>
|
|
1262
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
1263
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
1264
|
+
</div>
|
|
1265
|
+
<span class="ml-3 shrink-0 rounded-full bg-blue-100 dark:bg-blue-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-blue-700 dark:text-blue-400">oneOf</span>
|
|
1266
|
+
</div>
|
|
1267
|
+
<div class="flex overflow-x-auto border-b border-slate-200 dark:border-slate-700">
|
|
1268
|
+
<button
|
|
1269
|
+
*ngFor="let option of schema.oneOf; let i = index"
|
|
1270
|
+
type="button"
|
|
1271
|
+
class="px-4 py-2.5 text-xs font-medium whitespace-nowrap border-b-2 transition-colors"
|
|
1272
|
+
[class.border-indigo-500]="selectedOneOf === i"
|
|
1273
|
+
[class.text-indigo-600]="selectedOneOf === i"
|
|
1274
|
+
[class.dark:text-indigo-400]="selectedOneOf === i"
|
|
1275
|
+
[class.font-semibold]="selectedOneOf === i"
|
|
1276
|
+
[class.border-transparent]="selectedOneOf !== i"
|
|
1277
|
+
[class.text-slate-500]="selectedOneOf !== i"
|
|
1278
|
+
[class.dark:text-slate-400]="selectedOneOf !== i"
|
|
1279
|
+
(click)="selectedOneOf = i; onOneOfChange()"
|
|
1280
|
+
>{{ option.title || ('Option ' + (i + 1)) }}</button>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div class="p-4" *ngIf="activeVariant">
|
|
1283
|
+
<jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
1284
|
+
[path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
|
|
1285
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1286
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
1287
|
+
</div>
|
|
1288
|
+
</div>
|
|
1289
|
+
|
|
1290
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1291
|
+
anyOf — pill toggle, nessuna icona
|
|
1292
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1293
|
+
<div *ngSwitchCase="'anyOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
1294
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
1295
|
+
<div>
|
|
1296
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
1297
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
1298
|
+
</div>
|
|
1299
|
+
<span class="ml-3 shrink-0 rounded-full bg-violet-100 dark:bg-violet-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-violet-700 dark:text-violet-400">anyOf</span>
|
|
1300
|
+
</div>
|
|
1301
|
+
<div class="flex flex-wrap gap-2 px-4 py-3 border-b border-slate-200 dark:border-slate-700">
|
|
1302
|
+
<label
|
|
1303
|
+
*ngFor="let option of schema.anyOf; let i = index"
|
|
1304
|
+
class="inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium cursor-pointer transition-colors select-none"
|
|
1305
|
+
[class.border-indigo-400]="selectedAnyOf.has(i)"
|
|
1306
|
+
[class.bg-indigo-50]="selectedAnyOf.has(i)"
|
|
1307
|
+
[class.dark:bg-indigo-900/30]="selectedAnyOf.has(i)"
|
|
1308
|
+
[class.text-indigo-700]="selectedAnyOf.has(i)"
|
|
1309
|
+
[class.dark:text-indigo-400]="selectedAnyOf.has(i)"
|
|
1310
|
+
[class.border-slate-200]="!selectedAnyOf.has(i)"
|
|
1311
|
+
[class.dark:border-slate-600]="!selectedAnyOf.has(i)"
|
|
1312
|
+
[class.text-slate-600]="!selectedAnyOf.has(i)"
|
|
1313
|
+
[class.dark:text-slate-400]="!selectedAnyOf.has(i)"
|
|
1314
|
+
>
|
|
1315
|
+
<input type="checkbox" class="h-3.5 w-3.5 accent-indigo-600" [checked]="selectedAnyOf.has(i)" (change)="toggleAnyOf(i)" />
|
|
1316
|
+
{{ option.title || ('Option ' + (i + 1)) }}
|
|
1317
|
+
</label>
|
|
1318
|
+
</div>
|
|
1319
|
+
<div class="p-4" *ngIf="activeVariant">
|
|
1320
|
+
<jsm-schema-node [schema]="activeVariant" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
1321
|
+
[path]="path" [errorsMap]="errorsMap" [label]="label" [required]="required"
|
|
1322
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1323
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
1324
|
+
</div>
|
|
1325
|
+
</div>
|
|
1326
|
+
|
|
1327
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1328
|
+
allOf — sezioni in colonna con bordo sinistro
|
|
1329
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1330
|
+
<div *ngSwitchCase="'allOf'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
|
1331
|
+
<div class="flex items-start justify-between px-4 py-3 bg-slate-50 dark:bg-slate-800/60 border-b border-slate-200 dark:border-slate-700">
|
|
1332
|
+
<div>
|
|
1333
|
+
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</p>
|
|
1334
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">{{ schema.description }}</p>
|
|
1335
|
+
</div>
|
|
1336
|
+
<span class="ml-3 shrink-0 rounded-full bg-teal-100 dark:bg-teal-900/40 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-teal-700 dark:text-teal-400">allOf</span>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div class="p-4 space-y-4">
|
|
1339
|
+
<div *ngFor="let subSchema of mergedAllOf; let i = index" class="border-l-2 border-indigo-200 dark:border-indigo-700 pl-4">
|
|
1340
|
+
<jsm-schema-node [schema]="subSchema" [control]="control" [parent]="parent" [controlKey]="controlKey"
|
|
1341
|
+
[path]="path" [errorsMap]="errorsMap" [label]="subSchema.title || ('Section ' + (i + 1))" [required]="required"
|
|
1342
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1343
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
1344
|
+
</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
</div>
|
|
1347
|
+
|
|
1348
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1349
|
+
object — fieldset con griglia SOLO per campi primitivi.
|
|
1350
|
+
Logica layout:
|
|
1351
|
+
- I figli vengono classificati come "semplici" (primitive/enum/bool)
|
|
1352
|
+
o "complessi" (object/array/oneOf/anyOf/allOf).
|
|
1353
|
+
- I semplici vanno in griglia 2 colonne (sm:grid-cols-2).
|
|
1354
|
+
- I complessi vanno sempre in colonna piena (col-span-2).
|
|
1355
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1356
|
+
<fieldset *ngSwitchCase="'object'" class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-sm p-4 transition-shadow focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
|
|
1357
|
+
<legend class="px-2 text-sm font-bold text-slate-800 dark:text-slate-200">{{ schema.title || label }}</legend>
|
|
1358
|
+
|
|
1359
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 mb-3 mt-1">{{ schema.description }}</p>
|
|
1360
|
+
|
|
1361
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5 mb-3">
|
|
1362
|
+
<span *ngFor="let badge of metaBadges"
|
|
1363
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
1364
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
1365
|
+
</div>
|
|
1366
|
+
|
|
1367
|
+
<!-- campi semplici (string/number/bool/enum) → griglia 2 colonne -->
|
|
1368
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4" [formGroup]="controlAsGroup">
|
|
1369
|
+
<ng-container *ngFor="let key of simpleKeys">
|
|
1370
|
+
<div class="flex flex-col gap-1">
|
|
1371
|
+
<div *ngIf="isDynamicKey(key)"
|
|
1372
|
+
class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
|
|
1373
|
+
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
|
|
1374
|
+
<button type="button"
|
|
1375
|
+
class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
1376
|
+
(click)="removeDynamicKey(key)">Remove</button>
|
|
1377
|
+
</div>
|
|
1378
|
+
<jsm-schema-node
|
|
1379
|
+
[schema]="schemaForKey(key)"
|
|
1380
|
+
[control]="childControl(key)"
|
|
1381
|
+
[parent]="controlAsGroup"
|
|
1382
|
+
[controlKey]="key"
|
|
1383
|
+
[path]="pathForChild(key)"
|
|
1384
|
+
[errorsMap]="errorsMap"
|
|
1385
|
+
[label]="key"
|
|
1386
|
+
[required]="isRequired(key)"
|
|
1387
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1388
|
+
(controlReplaced)="controlReplaced.emit($event)">
|
|
1389
|
+
</jsm-schema-node>
|
|
1390
|
+
</div>
|
|
1391
|
+
</ng-container>
|
|
1392
|
+
</div>
|
|
1393
|
+
|
|
1394
|
+
<!-- campi complessi (object/array/combinator) → colonna piena -->
|
|
1395
|
+
<div class="space-y-4 mt-4" *ngIf="complexKeys.length > 0" [formGroup]="controlAsGroup">
|
|
1396
|
+
<ng-container *ngFor="let key of complexKeys">
|
|
1397
|
+
<div class="flex flex-col gap-1">
|
|
1398
|
+
<div *ngIf="isDynamicKey(key)"
|
|
1399
|
+
class="flex items-center justify-between rounded-md border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/50 px-2 py-1 mb-1">
|
|
1400
|
+
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">{{ key }}</span>
|
|
1401
|
+
<button type="button"
|
|
1402
|
+
class="rounded px-1.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
|
1403
|
+
(click)="removeDynamicKey(key)">Remove</button>
|
|
1404
|
+
</div>
|
|
1405
|
+
<jsm-schema-node
|
|
1406
|
+
[schema]="schemaForKey(key)"
|
|
1407
|
+
[control]="childControl(key)"
|
|
1408
|
+
[parent]="controlAsGroup"
|
|
1409
|
+
[controlKey]="key"
|
|
1410
|
+
[path]="pathForChild(key)"
|
|
1411
|
+
[errorsMap]="errorsMap"
|
|
1412
|
+
[label]="key"
|
|
1413
|
+
[required]="isRequired(key)"
|
|
1414
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1415
|
+
(controlReplaced)="controlReplaced.emit($event)">
|
|
1416
|
+
</jsm-schema-node>
|
|
1417
|
+
</div>
|
|
1418
|
+
</ng-container>
|
|
1419
|
+
</div>
|
|
1420
|
+
|
|
1421
|
+
<!-- add dynamic property -->
|
|
1422
|
+
<div *ngIf="canAddProperty" class="mt-4 rounded-lg border border-dashed border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800/40 p-3 space-y-2">
|
|
1423
|
+
<p class="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">Add property</p>
|
|
1424
|
+
<div class="flex gap-2">
|
|
1425
|
+
<input
|
|
1426
|
+
class="flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-1.5 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
|
|
1427
|
+
[(ngModel)]="newPropertyKey" placeholder="propertyName" />
|
|
1428
|
+
<button type="button"
|
|
1429
|
+
class="rounded-lg bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95"
|
|
1430
|
+
(click)="addDynamicProperty()">Add</button>
|
|
1431
|
+
</div>
|
|
1432
|
+
<p *ngIf="propertyError" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1433
|
+
<!-- icona errore: unico caso in cui ha senso, segnala un problema -->
|
|
1434
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1435
|
+
{{ propertyError }}
|
|
1436
|
+
</p>
|
|
1437
|
+
</div>
|
|
1438
|
+
|
|
1439
|
+
<div *ngIf="errorsForPath.length" class="mt-3 space-y-1">
|
|
1440
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1441
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1442
|
+
{{ error }}
|
|
1443
|
+
</p>
|
|
1444
|
+
</div>
|
|
1445
|
+
</fieldset>
|
|
1446
|
+
|
|
1447
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1448
|
+
array — sempre in colonna piena, items in colonna
|
|
1449
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1450
|
+
<div *ngSwitchCase="'array'" class="space-y-3">
|
|
1451
|
+
<div class="flex items-start justify-between gap-2">
|
|
1452
|
+
<div>
|
|
1453
|
+
<p class="text-sm font-semibold text-slate-800">
|
|
1454
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1455
|
+
</p>
|
|
1456
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 mt-0.5">{{ schema.description }}</p>
|
|
1457
|
+
</div>
|
|
1458
|
+
<!-- icona + nel bottone: indica l'azione "aggiungi", non decorativa -->
|
|
1459
|
+
<button type="button"
|
|
1460
|
+
class="shrink-0 flex items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition hover:bg-indigo-700 active:scale-95 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
1461
|
+
(click)="addArrayItem()" [disabled]="!canAddArrayItem">
|
|
1462
|
+
<svg class="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1463
|
+
Add item
|
|
1464
|
+
</button>
|
|
1465
|
+
</div>
|
|
1466
|
+
|
|
1467
|
+
<div *ngIf="effectiveSchema.contains" class="rounded-md bg-indigo-50 dark:bg-indigo-900/30 px-3 py-1.5 text-xs text-indigo-700 dark:text-indigo-400">
|
|
1468
|
+
Must contain matching item(s)
|
|
1469
|
+
<span *ngIf="effectiveSchema.minContains"> · min {{ effectiveSchema.minContains }}</span>
|
|
1470
|
+
<span *ngIf="effectiveSchema.maxContains"> · max {{ effectiveSchema.maxContains }}</span>
|
|
1471
|
+
</div>
|
|
1472
|
+
|
|
1473
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
|
|
1474
|
+
<span *ngFor="let badge of metaBadges"
|
|
1475
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
1476
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
1477
|
+
</div>
|
|
1478
|
+
|
|
1479
|
+
<div class="space-y-2" *ngIf="arrayControls.length > 0; else emptyArray">
|
|
1480
|
+
<div *ngFor="let item of arrayControls; let i = index"
|
|
1481
|
+
class="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-3 shadow-sm transition focus-within:border-indigo-300 dark:focus-within:border-indigo-600 focus-within:ring-2 focus-within:ring-indigo-100 dark:focus-within:ring-indigo-900/40">
|
|
1482
|
+
<div class="flex items-center justify-between mb-3">
|
|
1483
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 text-[11px] font-bold text-indigo-600 dark:text-indigo-400">{{ i + 1 }}</span>
|
|
1484
|
+
<button type="button"
|
|
1485
|
+
class="rounded-md border border-red-200 dark:border-red-800 px-2 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400 transition hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
1486
|
+
(click)="removeArrayItem(i)">Remove</button>
|
|
1487
|
+
</div>
|
|
1488
|
+
<jsm-schema-node [schema]="schemaForIndex(i)" [control]="item" [parent]="controlAsArray"
|
|
1489
|
+
[controlKey]="i" [path]="pathForChild(i)" [errorsMap]="errorsMap" [label]="label + ' item'"
|
|
1490
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1491
|
+
(controlReplaced)="controlReplaced.emit($event)"></jsm-schema-node>
|
|
1492
|
+
</div>
|
|
1493
|
+
</div>
|
|
1494
|
+
|
|
1495
|
+
<ng-template #emptyArray>
|
|
1496
|
+
<div class="flex flex-col items-center gap-2 rounded-xl border border-dashed border-slate-200 dark:border-slate-700 py-8 text-slate-400 dark:text-slate-500 text-xs">
|
|
1497
|
+
No items yet — click "Add item" to start.
|
|
1498
|
+
</div>
|
|
1499
|
+
</ng-template>
|
|
1500
|
+
|
|
1501
|
+
<div *ngIf="errorsForPath.length" class="space-y-1">
|
|
1502
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1503
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/><path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
1504
|
+
{{ error }}
|
|
1505
|
+
</p>
|
|
1506
|
+
</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
|
|
1509
|
+
<!-- ═══════════════════════════════════════════════════════════
|
|
1510
|
+
primitive — sempre in colonna, nessuna icona decorativa.
|
|
1511
|
+
Icona errore solo nei messaggi di validazione.
|
|
1512
|
+
Icona chevron nel select: funzionale (indica dropdown).
|
|
1513
|
+
═══════════════════════════════════════════════════════════ -->
|
|
1514
|
+
<div *ngSwitchDefault class="space-y-1.5">
|
|
1515
|
+
<ng-container *ngIf="inputKind !== 'checkbox'">
|
|
1516
|
+
<label class="block text-[13px] font-semibold text-slate-700 dark:text-slate-300">
|
|
1517
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1518
|
+
</label>
|
|
1519
|
+
<p *ngIf="schema.description" class="text-xs text-slate-500 dark:text-slate-400 -mt-0.5">{{ schema.description }}</p>
|
|
1520
|
+
</ng-container>
|
|
1521
|
+
|
|
1522
|
+
<div *ngIf="metaBadges.length" class="flex flex-wrap gap-1.5">
|
|
1523
|
+
<span *ngFor="let badge of metaBadges"
|
|
1524
|
+
class="rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
|
|
1525
|
+
[ngClass]="badgeClass(badge)">{{ badge }}</span>
|
|
1526
|
+
</div>
|
|
1527
|
+
|
|
1528
|
+
<ng-container [ngSwitch]="inputKind">
|
|
1529
|
+
|
|
1530
|
+
<input *ngSwitchCase="'text'"
|
|
1531
|
+
class="w-full rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
|
|
1532
|
+
[class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1533
|
+
[class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1534
|
+
[class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1535
|
+
[class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1536
|
+
[class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1537
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1538
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1539
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1540
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1541
|
+
[class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1542
|
+
[class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1543
|
+
[attr.type]="inputType"
|
|
1544
|
+
[attr.placeholder]="schema.title || label"
|
|
1545
|
+
[formControl]="controlAsFormControl"
|
|
1546
|
+
/>
|
|
1547
|
+
|
|
1548
|
+
<textarea *ngSwitchCase="'textarea'" rows="4"
|
|
1549
|
+
class="w-full resize-y rounded-lg border bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 px-3 py-2 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:ring-2"
|
|
1550
|
+
[class.border-slate-300]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1551
|
+
[class.dark:border-slate-600]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1552
|
+
[class.focus:border-indigo-400]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1553
|
+
[class.focus:ring-indigo-100]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1554
|
+
[class.dark:focus:ring-indigo-900/40]="!(errorsForPath.length && controlAsFormControl.dirty)"
|
|
1555
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1556
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1557
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1558
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1559
|
+
[class.focus:border-red-400]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1560
|
+
[class.focus:ring-red-100]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1561
|
+
[attr.placeholder]="schema.title || label"
|
|
1562
|
+
[formControl]="controlAsFormControl"
|
|
1563
|
+
></textarea>
|
|
1564
|
+
|
|
1565
|
+
<!-- select: il chevron SVG è funzionale, indica che è un dropdown -->
|
|
1566
|
+
<div *ngSwitchCase="'select'" class="relative">
|
|
1567
|
+
<select
|
|
1568
|
+
class="w-full appearance-none rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 px-3 py-2 pr-8 text-sm shadow-sm outline-none transition hover:border-slate-400 dark:hover:border-slate-500 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:focus:ring-indigo-900/40"
|
|
1569
|
+
[class.border-red-300]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1570
|
+
[class.dark:border-red-700]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1571
|
+
[class.bg-red-50]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1572
|
+
[class.dark:bg-red-900/20]="errorsForPath.length && controlAsFormControl.dirty"
|
|
1573
|
+
[formControl]="controlAsFormControl">
|
|
1574
|
+
<option *ngFor="let option of effectiveSchema.enum" [ngValue]="option">{{ option }}</option>
|
|
1575
|
+
</select>
|
|
1576
|
+
<svg class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500"
|
|
1577
|
+
viewBox="0 0 16 16" fill="none">
|
|
1578
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1579
|
+
</svg>
|
|
1580
|
+
</div>
|
|
1581
|
+
|
|
1582
|
+
<!-- checkbox come toggle switch -->
|
|
1583
|
+
<label *ngSwitchCase="'checkbox'" class="inline-flex cursor-pointer items-center gap-2.5">
|
|
1584
|
+
<span class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
|
|
1585
|
+
[class.bg-indigo-600]="controlAsFormControl.value"
|
|
1586
|
+
[class.bg-slate-300]="!controlAsFormControl.value"
|
|
1587
|
+
[class.dark:bg-slate-600]="!controlAsFormControl.value">
|
|
1588
|
+
<input type="checkbox" class="sr-only" [formControl]="controlAsFormControl" />
|
|
1589
|
+
<span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
|
|
1590
|
+
[class.translate-x-4]="controlAsFormControl.value"
|
|
1591
|
+
[class.translate-x-0.5]="!controlAsFormControl.value"></span>
|
|
1592
|
+
</span>
|
|
1593
|
+
<span class="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
1594
|
+
{{ schema.title || label }}<span *ngIf="required" class="ml-0.5 text-red-500">*</span>
|
|
1595
|
+
</span>
|
|
1596
|
+
</label>
|
|
1597
|
+
|
|
1598
|
+
</ng-container>
|
|
1599
|
+
|
|
1600
|
+
<p *ngIf="inputKind === 'checkbox' && schema.description" class="text-xs text-slate-500 dark:text-slate-400">{{ schema.description }}</p>
|
|
1601
|
+
|
|
1602
|
+
<p *ngIf="effectiveSchema.examples?.length" class="text-[11px] text-slate-400 dark:text-slate-500">
|
|
1603
|
+
e.g. {{ effectiveSchema.examples?.[0] }}
|
|
1604
|
+
</p>
|
|
1605
|
+
|
|
1606
|
+
<p *ngIf="effectiveSchema.contentMediaType" class="text-[11px] text-slate-400 dark:text-slate-500">
|
|
1607
|
+
{{ effectiveSchema.contentMediaType }}<span *ngIf="effectiveSchema.contentEncoding"> ({{ effectiveSchema.contentEncoding }})</span>
|
|
1608
|
+
</p>
|
|
1609
|
+
|
|
1610
|
+
<!-- icona errore: funzionale, segnala un problema di validazione -->
|
|
1611
|
+
<div *ngIf="errorsForPath.length && controlAsFormControl.dirty" class="space-y-1 pt-0.5">
|
|
1612
|
+
<p *ngFor="let error of errorsForPath" class="flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400">
|
|
1613
|
+
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
|
|
1614
|
+
<circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5"/>
|
|
1615
|
+
<path d="M8 5v3.5M8 11v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1616
|
+
</svg>
|
|
1617
|
+
{{ error }}
|
|
1618
|
+
</p>
|
|
1619
|
+
</div>
|
|
1620
|
+
</div>
|
|
1621
|
+
|
|
1622
|
+
</ng-container>
|
|
1486
1623
|
`,
|
|
1487
1624
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1488
1625
|
}]
|
|
@@ -1504,6 +1641,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1504
1641
|
type: Input
|
|
1505
1642
|
}], controlKey: [{
|
|
1506
1643
|
type: Input
|
|
1644
|
+
}], allowAdditionalProperties: [{
|
|
1645
|
+
type: Input
|
|
1507
1646
|
}], controlReplaced: [{
|
|
1508
1647
|
type: Output
|
|
1509
1648
|
}] } });
|
|
@@ -1515,13 +1654,16 @@ class JsonSchemaFormComponent {
|
|
|
1515
1654
|
schema;
|
|
1516
1655
|
value;
|
|
1517
1656
|
data;
|
|
1657
|
+
allowAdditionalProperties = false;
|
|
1518
1658
|
formReady = new EventEmitter();
|
|
1519
1659
|
valueChange = new EventEmitter();
|
|
1520
1660
|
schemaReady = new EventEmitter();
|
|
1661
|
+
schemaErrors = new EventEmitter();
|
|
1521
1662
|
form;
|
|
1522
1663
|
rootControl;
|
|
1523
1664
|
resolvedSchema;
|
|
1524
1665
|
errorsMap = new Map();
|
|
1666
|
+
schemaErrorList = [];
|
|
1525
1667
|
loading = false;
|
|
1526
1668
|
valueSub;
|
|
1527
1669
|
constructor(schemaService, resolver, validation) {
|
|
@@ -1541,7 +1683,17 @@ class JsonSchemaFormComponent {
|
|
|
1541
1683
|
this.valueSub?.unsubscribe();
|
|
1542
1684
|
this.loading = true;
|
|
1543
1685
|
this.resolvedSchema = await this.resolver.resolve(this.schema, this.schema?.$id);
|
|
1686
|
+
// Validate the schema itself and emit any structural errors
|
|
1687
|
+
const schemaErrs = this.validation.validateSchema(this.resolvedSchema);
|
|
1688
|
+
this.schemaErrorList = schemaErrs;
|
|
1689
|
+
this.schemaErrors.emit(schemaErrs);
|
|
1544
1690
|
this.schemaReady.emit(this.resolvedSchema);
|
|
1691
|
+
// If the schema has structural errors, stop here — don't try to build the form
|
|
1692
|
+
if (schemaErrs.length > 0) {
|
|
1693
|
+
this.form = undefined;
|
|
1694
|
+
this.loading = false;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1545
1697
|
const initialData = this.value !== undefined ? this.value : this.data;
|
|
1546
1698
|
this.rootControl = this.schemaService.buildControl(this.resolvedSchema, initialData);
|
|
1547
1699
|
this.form = this.rootControl instanceof FormGroup ? this.rootControl : new FormGroup({ value: this.rootControl });
|
|
@@ -1588,61 +1740,121 @@ class JsonSchemaFormComponent {
|
|
|
1588
1740
|
});
|
|
1589
1741
|
}
|
|
1590
1742
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaFormComponent, deps: [{ token: JsonSchemaFormService }, { token: JsonSchemaResolverService }, { token: JsonSchemaValidationService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1591
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1592
|
-
<
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
<
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1743
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: JsonSchemaFormComponent, isStandalone: true, selector: "jsm-json-schema-form", inputs: { schema: "schema", value: "value", data: "data", allowAdditionalProperties: "allowAdditionalProperties" }, outputs: { formReady: "formReady", valueChange: "valueChange", schemaReady: "schemaReady", schemaErrors: "schemaErrors" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1744
|
+
<div class="jsm-root">
|
|
1745
|
+
<form class="w-full space-y-5">
|
|
1746
|
+
<div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
|
|
1747
|
+
<h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
|
|
1748
|
+
<p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
|
|
1749
|
+
</div>
|
|
1750
|
+
|
|
1751
|
+
<div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
|
|
1752
|
+
<svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
|
|
1753
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
1754
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
|
1755
|
+
</svg>
|
|
1756
|
+
Resolving schema…
|
|
1757
|
+
</div>
|
|
1758
|
+
|
|
1759
|
+
<!-- schema structural errors panel -->
|
|
1760
|
+
<div *ngIf="schemaErrorList.length > 0 && !loading"
|
|
1761
|
+
class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
|
|
1762
|
+
role="alert">
|
|
1763
|
+
<div class="flex items-center gap-2">
|
|
1764
|
+
<svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
|
|
1765
|
+
<path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
|
1766
|
+
<path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1767
|
+
</svg>
|
|
1768
|
+
<span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
|
|
1769
|
+
Schema errors ({{ schemaErrorList.length }})
|
|
1770
|
+
</span>
|
|
1771
|
+
</div>
|
|
1772
|
+
<p class="text-xs text-amber-700 dark:text-amber-400">
|
|
1773
|
+
The schema has structural issues. Fix them to get a valid form.
|
|
1774
|
+
</p>
|
|
1775
|
+
<div class="space-y-2">
|
|
1776
|
+
<div *ngFor="let err of schemaErrorList"
|
|
1777
|
+
class="rounded-lg border border-amber-200 dark:border-amber-800 bg-white dark:bg-slate-900 px-3 py-2 space-y-0.5">
|
|
1778
|
+
<p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
|
|
1779
|
+
<p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
|
|
1780
|
+
<p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
|
|
1781
|
+
</div>
|
|
1782
|
+
</div>
|
|
1783
|
+
</div>
|
|
1784
|
+
|
|
1785
|
+
<div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
|
|
1786
|
+
<jsm-schema-node
|
|
1787
|
+
[schema]="resolvedSchema"
|
|
1788
|
+
[control]="rootControl"
|
|
1789
|
+
[path]="''"
|
|
1790
|
+
[errorsMap]="errorsMap"
|
|
1791
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1792
|
+
label="Root"
|
|
1793
|
+
(controlReplaced)="onRootReplaced($event)"
|
|
1794
|
+
></jsm-schema-node>
|
|
1795
|
+
</div>
|
|
1796
|
+
</form>
|
|
1797
|
+
</div>
|
|
1798
|
+
`, isInline: true, styles: [".jsm-root *,.jsm-root *:before,.jsm-root *:after{box-sizing:border-box}.jsm-root input,.jsm-root textarea,.jsm-root select,.jsm-root button{font-family:inherit;font-size:inherit;line-height:inherit}.jsm-root input[type=number]::-webkit-inner-spin-button,.jsm-root input[type=number]::-webkit-outer-spin-button{opacity:1}.jsm-root input:not([type=checkbox]):not([type=radio]):not(.sr-only),.jsm-root textarea,.jsm-root select{border-width:1.5px!important;border-style:solid!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: JsonSchemaNodeComponent, selector: "jsm-schema-node", inputs: ["schema", "control", "label", "required", "path", "errorsMap", "parent", "controlKey", "allowAdditionalProperties"], outputs: ["controlReplaced"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1614
1799
|
}
|
|
1615
1800
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: JsonSchemaFormComponent, decorators: [{
|
|
1616
1801
|
type: Component,
|
|
1617
|
-
args: [{
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1802
|
+
args: [{ selector: 'jsm-json-schema-form', standalone: true, imports: [CommonModule, ReactiveFormsModule, JsonSchemaNodeComponent], encapsulation: ViewEncapsulation.None, template: `
|
|
1803
|
+
<div class="jsm-root">
|
|
1804
|
+
<form class="w-full space-y-5">
|
|
1805
|
+
<div *ngIf="resolvedSchema?.title || resolvedSchema?.description" class="space-y-0.5">
|
|
1806
|
+
<h2 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ resolvedSchema?.title || 'Form' }}</h2>
|
|
1807
|
+
<p *ngIf="resolvedSchema?.description" class="text-sm text-slate-500 dark:text-slate-400">{{ resolvedSchema?.description }}</p>
|
|
1808
|
+
</div>
|
|
1809
|
+
|
|
1810
|
+
<div *ngIf="loading" class="flex items-center gap-2 py-6 text-sm text-slate-500 dark:text-slate-400" aria-live="polite">
|
|
1811
|
+
<svg class="h-4 w-4 animate-spin text-indigo-500 dark:text-indigo-400" viewBox="0 0 24 24" fill="none">
|
|
1812
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
1813
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
|
1814
|
+
</svg>
|
|
1815
|
+
Resolving schema…
|
|
1816
|
+
</div>
|
|
1817
|
+
|
|
1818
|
+
<!-- schema structural errors panel -->
|
|
1819
|
+
<div *ngIf="schemaErrorList.length > 0 && !loading"
|
|
1820
|
+
class="rounded-xl border border-amber-300 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 p-4 space-y-3"
|
|
1821
|
+
role="alert">
|
|
1822
|
+
<div class="flex items-center gap-2">
|
|
1823
|
+
<svg class="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" viewBox="0 0 16 16" fill="none">
|
|
1824
|
+
<path d="M8 2L1.5 13.5h13L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
|
|
1825
|
+
<path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
1826
|
+
</svg>
|
|
1827
|
+
<span class="text-sm font-semibold text-amber-800 dark:text-amber-300">
|
|
1828
|
+
Schema errors ({{ schemaErrorList.length }})
|
|
1829
|
+
</span>
|
|
1830
|
+
</div>
|
|
1831
|
+
<p class="text-xs text-amber-700 dark:text-amber-400">
|
|
1832
|
+
The schema has structural issues. Fix them to get a valid form.
|
|
1833
|
+
</p>
|
|
1834
|
+
<div class="space-y-2">
|
|
1835
|
+
<div *ngFor="let err of schemaErrorList"
|
|
1836
|
+
class="rounded-lg border border-amber-200 dark:border-amber-800 bg-white dark:bg-slate-900 px-3 py-2 space-y-0.5">
|
|
1837
|
+
<p class="font-mono text-[11px] font-semibold text-amber-700 dark:text-amber-400">{{ err.path }}</p>
|
|
1838
|
+
<p class="text-xs text-slate-700 dark:text-slate-300">{{ err.message }}</p>
|
|
1839
|
+
<p class="text-[10px] uppercase tracking-wide text-slate-400 dark:text-slate-500">keyword: {{ err.keyword }}</p>
|
|
1840
|
+
</div>
|
|
1841
|
+
</div>
|
|
1842
|
+
</div>
|
|
1843
|
+
|
|
1844
|
+
<div *ngIf="form && resolvedSchema && !loading" class="space-y-5" [formGroup]="form">
|
|
1845
|
+
<jsm-schema-node
|
|
1846
|
+
[schema]="resolvedSchema"
|
|
1847
|
+
[control]="rootControl"
|
|
1848
|
+
[path]="''"
|
|
1849
|
+
[errorsMap]="errorsMap"
|
|
1850
|
+
[allowAdditionalProperties]="allowAdditionalProperties"
|
|
1851
|
+
label="Root"
|
|
1852
|
+
(controlReplaced)="onRootReplaced($event)"
|
|
1853
|
+
></jsm-schema-node>
|
|
1854
|
+
</div>
|
|
1855
|
+
</form>
|
|
1856
|
+
</div>
|
|
1857
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".jsm-root *,.jsm-root *:before,.jsm-root *:after{box-sizing:border-box}.jsm-root input,.jsm-root textarea,.jsm-root select,.jsm-root button{font-family:inherit;font-size:inherit;line-height:inherit}.jsm-root input[type=number]::-webkit-inner-spin-button,.jsm-root input[type=number]::-webkit-outer-spin-button{opacity:1}.jsm-root input:not([type=checkbox]):not([type=radio]):not(.sr-only),.jsm-root textarea,.jsm-root select{border-width:1.5px!important;border-style:solid!important}\n"] }]
|
|
1646
1858
|
}], ctorParameters: () => [{ type: JsonSchemaFormService }, { type: JsonSchemaResolverService }, { type: JsonSchemaValidationService }], propDecorators: { schema: [{
|
|
1647
1859
|
type: Input,
|
|
1648
1860
|
args: [{ required: true }]
|
|
@@ -1650,12 +1862,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
1650
1862
|
type: Input
|
|
1651
1863
|
}], data: [{
|
|
1652
1864
|
type: Input
|
|
1865
|
+
}], allowAdditionalProperties: [{
|
|
1866
|
+
type: Input
|
|
1653
1867
|
}], formReady: [{
|
|
1654
1868
|
type: Output
|
|
1655
1869
|
}], valueChange: [{
|
|
1656
1870
|
type: Output
|
|
1657
1871
|
}], schemaReady: [{
|
|
1658
1872
|
type: Output
|
|
1873
|
+
}], schemaErrors: [{
|
|
1874
|
+
type: Output
|
|
1659
1875
|
}] } });
|
|
1660
1876
|
|
|
1661
1877
|
class JsonSchemaFormModule {
|