@opencitylabs/formio-sdk 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +325 -3
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -79,7 +79,8 @@ class FormIoHelper {
|
|
|
79
79
|
const response = await axios.get(this.basePath + '/api/' + endPoint, {
|
|
80
80
|
headers: {
|
|
81
81
|
"Content-Type": "application/json",
|
|
82
|
-
"Authorization": "Bearer " + token
|
|
82
|
+
"Authorization": "Bearer " + token,
|
|
83
|
+
"x-locale": this.getCurrentLocale()
|
|
83
84
|
}
|
|
84
85
|
})
|
|
85
86
|
return response.data
|
|
@@ -92,7 +93,8 @@ class FormIoHelper {
|
|
|
92
93
|
const response = await axios.post(this.basePath + '/api/' + endPoint, JSON.stringify(params),{
|
|
93
94
|
headers: {
|
|
94
95
|
"Content-Type": "application/json",
|
|
95
|
-
"Authorization": "Bearer " + token
|
|
96
|
+
"Authorization": "Bearer " + token,
|
|
97
|
+
"x-locale": this.getCurrentLocale()
|
|
96
98
|
}
|
|
97
99
|
})
|
|
98
100
|
return response.data
|
|
@@ -115,6 +117,7 @@ class FormIoHelper {
|
|
|
115
117
|
headers: {
|
|
116
118
|
"Content-Type": "application/json",
|
|
117
119
|
Authorization: `Bearer ${token}`,
|
|
120
|
+
"x-locale": this.getCurrentLocale()
|
|
118
121
|
},
|
|
119
122
|
});
|
|
120
123
|
|
|
@@ -161,7 +164,8 @@ class FormIoHelper {
|
|
|
161
164
|
async anonymousCall(endPoint) {
|
|
162
165
|
const response = await axios.get(this.basePath + '/api/' + endPoint, {
|
|
163
166
|
headers: {
|
|
164
|
-
"Content-Type": "application/json"
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"x-locale": this.getCurrentLocale()
|
|
165
169
|
}
|
|
166
170
|
})
|
|
167
171
|
return response.data
|
|
@@ -568,6 +572,324 @@ class FormIoHelper {
|
|
|
568
572
|
}
|
|
569
573
|
}
|
|
570
574
|
|
|
575
|
+
// --- PROFILE BLOCKS (PDND + Once-Only) ---
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Applies PDND prefilling and once-only fallback to profile block components
|
|
579
|
+
* that have `sdk_mode: true` in their custom properties.
|
|
580
|
+
*
|
|
581
|
+
* @param {object} formInstance - Form.io instance (Webform/Wizard)
|
|
582
|
+
* @param {string} fiscalCode - User's codice fiscale
|
|
583
|
+
* @param {object} options
|
|
584
|
+
* @param {string} options.serviceId - Portal service ID (used to fetch PDND configs)
|
|
585
|
+
* @param {string} [options.userId] - Portal user ID (used for once-only API; omit for anonymous)
|
|
586
|
+
* @returns {Promise<Array<{data: object}>>} Data objects to merge via formInstance.setValue()
|
|
587
|
+
*/
|
|
588
|
+
async applyProfileBlocks(formInstance, fiscalCode, options = {}) {
|
|
589
|
+
const { serviceId, userId } = options;
|
|
590
|
+
|
|
591
|
+
if (!formInstance || !fiscalCode) return [];
|
|
592
|
+
|
|
593
|
+
// Load PDND configs autonomously
|
|
594
|
+
let pdndConfigs = {};
|
|
595
|
+
if (serviceId) {
|
|
596
|
+
try {
|
|
597
|
+
pdndConfigs = await this.authenticatedCall(`services/${serviceId}/pdnd-configs`) || {};
|
|
598
|
+
} catch (e) {
|
|
599
|
+
console.warn('[applyProfileBlocks] Could not load PDND configs:', e);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const profileBlockComponents = this._findProfileBlockComponents(formInstance);
|
|
604
|
+
if (!profileBlockComponents.length) return [];
|
|
605
|
+
|
|
606
|
+
// Register once-only event listener once per formInstance lifetime
|
|
607
|
+
if (userId && !formInstance._sdkOnceOnlyListenerAttached) {
|
|
608
|
+
formInstance._sdkOnceOnlyListenerAttached = true;
|
|
609
|
+
formInstance.on('sdkOnceOnlySelected', ({ key, value }) => {
|
|
610
|
+
formInstance.emit('sdkProfileBlockData', { data: { [key]: value } });
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const results = [];
|
|
615
|
+
|
|
616
|
+
for (const component of profileBlockComponents) {
|
|
617
|
+
await component.subFormReady.then(async (subForm) => {
|
|
618
|
+
if (!subForm?.components?.length) return;
|
|
619
|
+
|
|
620
|
+
// Already has PDND meta → just disable fields
|
|
621
|
+
if (component.getValue().meta !== undefined) {
|
|
622
|
+
await this._disableProfileBlockFields(component);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const pdndComponent = subForm.components[0];
|
|
627
|
+
const eserviceId = pdndComponent?.component?.properties?.eservice;
|
|
628
|
+
const format = pdndComponent?.component?.properties?.format || 'default';
|
|
629
|
+
|
|
630
|
+
if (!eserviceId) {
|
|
631
|
+
if (userId) await this._initOnceOnly(formInstance, component, pdndComponent, userId);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (pdndConfigs[eserviceId]?.is_active) {
|
|
636
|
+
try {
|
|
637
|
+
const callUrl =
|
|
638
|
+
pdndConfigs[eserviceId].call_url +
|
|
639
|
+
`&fiscal_code=${encodeURIComponent(fiscalCode)}&format=${encodeURIComponent(format)}`;
|
|
640
|
+
const response = await this._getWithToken(callUrl);
|
|
641
|
+
|
|
642
|
+
results.push({ data: { [component.key]: { ...response } } });
|
|
643
|
+
await this._disableProfileBlockFields(component);
|
|
644
|
+
} catch (e) {
|
|
645
|
+
console.warn('[applyProfileBlocks] PDND call failed, falling back to once-only:', e);
|
|
646
|
+
if (userId) await this._initOnceOnly(formInstance, component, pdndComponent, userId);
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
if (userId) await this._initOnceOnly(formInstance, component, pdndComponent, userId);
|
|
650
|
+
}
|
|
651
|
+
}).catch(e => {
|
|
652
|
+
console.warn('[applyProfileBlocks] subFormReady error:', e);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return results;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_findProfileBlockComponents(formInstance) {
|
|
660
|
+
const components = [];
|
|
661
|
+
if (typeof formInstance.everyComponent === 'function') {
|
|
662
|
+
formInstance.everyComponent(component => {
|
|
663
|
+
if (
|
|
664
|
+
component.component.properties?.profile_block !== undefined &&
|
|
665
|
+
component.component.properties?.sdk_mode
|
|
666
|
+
) {
|
|
667
|
+
components.push(component);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return components;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async _getWithToken(url) {
|
|
675
|
+
const token = await this.getCurrentToken();
|
|
676
|
+
const response = await axios.get(url, {
|
|
677
|
+
headers: {
|
|
678
|
+
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
|
679
|
+
'x-locale': this.getCurrentLocale(),
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
return response.data;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async _disableProfileBlockFields(component) {
|
|
686
|
+
const componentData = component.getValue();
|
|
687
|
+
const source = componentData?.meta?.source || '';
|
|
688
|
+
const createdAt = componentData?.meta?.created_at;
|
|
689
|
+
const formattedDate = createdAt
|
|
690
|
+
? new Date(createdAt).toLocaleDateString(this.getCurrentLocale() || 'it-IT')
|
|
691
|
+
: null;
|
|
692
|
+
|
|
693
|
+
// Inject PDND alert into component description (rendered natively by Form.io)
|
|
694
|
+
const alertHtml =
|
|
695
|
+
`<p class="mt-n4 pdnd-alert">I dati contrassegnati con ` +
|
|
696
|
+
`<span class="text-success"><i class="bi bi-check-circle"></i></span> ` +
|
|
697
|
+
`provengono da ${source}` +
|
|
698
|
+
(formattedDate ? ` aggiornati al ${formattedDate}` : '') +
|
|
699
|
+
`</p>`;
|
|
700
|
+
|
|
701
|
+
if (!component.component.description?.includes('pdnd-alert')) {
|
|
702
|
+
component.component.description = (component.component.description || '') + alertHtml;
|
|
703
|
+
if (typeof component.redraw === 'function') component.redraw();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (!component.subFormReady) return;
|
|
707
|
+
|
|
708
|
+
component.subFormReady.then(subForm => {
|
|
709
|
+
if (!subForm?.components) return;
|
|
710
|
+
|
|
711
|
+
this._traverseSubformComponents(subForm.components, subComponent => {
|
|
712
|
+
if (subComponent.type === 'datagrid') {
|
|
713
|
+
subComponent.component.disableAddingRemovingRows = true;
|
|
714
|
+
if (typeof subComponent.redraw === 'function') subComponent.redraw();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (subComponent.component.properties?.pdnd_field) {
|
|
718
|
+
// Add success icon via description (prefix/suffix would appear near input, not label)
|
|
719
|
+
if (!subComponent.component.description?.includes('pdnd-success-icon')) {
|
|
720
|
+
subComponent.component.description =
|
|
721
|
+
`<small class="me-1 text-success pdnd-success-icon"><i class="bi bi-check-circle"></i></small>` +
|
|
722
|
+
(subComponent.component.description || '');
|
|
723
|
+
}
|
|
724
|
+
subComponent.component.disabled = true;
|
|
725
|
+
if (typeof subComponent.redraw === 'function') subComponent.redraw();
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async _initOnceOnly(formInstance, component, nestedFormComponent, userId) {
|
|
732
|
+
if (!component.rendered) return;
|
|
733
|
+
if (
|
|
734
|
+
component.path === 'applicant.data.address' ||
|
|
735
|
+
component.component.key === 'applicant'
|
|
736
|
+
) return;
|
|
737
|
+
|
|
738
|
+
const componentKey = component.component.key;
|
|
739
|
+
|
|
740
|
+
// Avoid double initialization
|
|
741
|
+
if (component.component.description?.includes('data-once-only-key')) return;
|
|
742
|
+
|
|
743
|
+
const isDatagrid =
|
|
744
|
+
nestedFormComponent?.type === 'datagrid' ||
|
|
745
|
+
nestedFormComponent?.components?.[0]?.type === 'datagrid';
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
const response = await this.authenticatedCall(
|
|
749
|
+
`users/${userId}/profile-blocks?keys=${componentKey}`
|
|
750
|
+
);
|
|
751
|
+
if (!response?.data?.length) return;
|
|
752
|
+
|
|
753
|
+
if (isDatagrid) {
|
|
754
|
+
const datagridComponent =
|
|
755
|
+
nestedFormComponent.type === 'datagrid'
|
|
756
|
+
? nestedFormComponent
|
|
757
|
+
: nestedFormComponent.components[0];
|
|
758
|
+
|
|
759
|
+
const datagridKey = datagridComponent.component.key;
|
|
760
|
+
const datagridData = component.getValue().data?.[datagridKey];
|
|
761
|
+
const isEmpty =
|
|
762
|
+
!datagridData ||
|
|
763
|
+
(Array.isArray(datagridData) &&
|
|
764
|
+
(datagridData.length === 0 ||
|
|
765
|
+
(datagridData.length === 1 &&
|
|
766
|
+
!Object.values(datagridData[0]).some(Boolean))));
|
|
767
|
+
|
|
768
|
+
if (isEmpty) {
|
|
769
|
+
const value = { ...response.data[0].value };
|
|
770
|
+
if ('meta' in value) delete value.meta;
|
|
771
|
+
formInstance.emit('sdkProfileBlockData', {
|
|
772
|
+
data: { [datagridKey]: value },
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
const entries = {};
|
|
777
|
+
response.data.forEach(d => { entries[d.id] = d; });
|
|
778
|
+
|
|
779
|
+
// Inject CTA into description — Form.io renders this as HTML natively
|
|
780
|
+
const ctaHtml =
|
|
781
|
+
`<p class="once-only-cta mt-2">` +
|
|
782
|
+
`Compila i campi seguenti oppure ` +
|
|
783
|
+
`<a href="#" data-once-only-key="${componentKey}">` +
|
|
784
|
+
`utilizza le informazioni che hai già fornito ` +
|
|
785
|
+
`<i class="bi bi-box-arrow-up-right"></i></a></p>`;
|
|
786
|
+
|
|
787
|
+
component.component.description = (component.component.description || '') + ctaHtml;
|
|
788
|
+
component.redraw();
|
|
789
|
+
|
|
790
|
+
// Scoped click listener — NOT on document, only on this component's element
|
|
791
|
+
component.element?.addEventListener('click', e => {
|
|
792
|
+
const anchor = e.target.closest('[data-once-only-key]');
|
|
793
|
+
if (!anchor) return;
|
|
794
|
+
e.preventDefault();
|
|
795
|
+
this._showOnceOnlyModal(formInstance, component, entries, userId);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
} catch (e) {
|
|
799
|
+
console.warn('[applyProfileBlocks] once-only error:', e);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
_showOnceOnlyModal(formInstance, component, entries, userId) {
|
|
804
|
+
let html = '<div class="it-list-wrapper"><ul class="it-list">';
|
|
805
|
+
for (const id in entries) {
|
|
806
|
+
const d = entries[id];
|
|
807
|
+
const dateStr = new Date(d.updated_at).toLocaleDateString('it-IT');
|
|
808
|
+
const timeStr = new Date(d.updated_at).toLocaleTimeString('it-IT', {
|
|
809
|
+
hour: '2-digit',
|
|
810
|
+
minute: '2-digit',
|
|
811
|
+
});
|
|
812
|
+
let name = d.unique_id;
|
|
813
|
+
const subtitle = `Valore inserito in data ${dateStr} alle ${timeStr}`;
|
|
814
|
+
if (!name || name === 'undefined' || name === '') name = subtitle;
|
|
815
|
+
|
|
816
|
+
// Escape single quotes to avoid breaking data-content attribute
|
|
817
|
+
const safeContent = JSON.stringify(d.value).replace(/'/g, ''');
|
|
818
|
+
|
|
819
|
+
html += `<li>
|
|
820
|
+
<div class="list-item">
|
|
821
|
+
<button class="btn btn-link btn-xs text-left select-once-only-entry" data-content='${safeContent}' type="button">
|
|
822
|
+
<p class="text text-start mb-0">${name}</p>
|
|
823
|
+
<p class="text-info">${subtitle}</p>
|
|
824
|
+
</button>
|
|
825
|
+
<div class="it-right-zone justify-content-end">
|
|
826
|
+
<button class="btn btn-outline-danger btn-xs mr-1 delete-once-only-entry" data-id="${d.id}" type="button">
|
|
827
|
+
<i class="bi bi-trash px-1 fs-5"></i>
|
|
828
|
+
</button>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
</li>`;
|
|
832
|
+
}
|
|
833
|
+
html += '</ul></div>';
|
|
834
|
+
|
|
835
|
+
Swal.fire({
|
|
836
|
+
title: 'Seleziona tra i valori precedentemente salvati',
|
|
837
|
+
showCloseButton: true,
|
|
838
|
+
html,
|
|
839
|
+
showConfirmButton: false,
|
|
840
|
+
showCancelButton: false,
|
|
841
|
+
didOpen: () => {
|
|
842
|
+
Swal.getPopup().querySelectorAll('.select-once-only-entry').forEach(el => {
|
|
843
|
+
el.addEventListener('click', e => {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
const value = JSON.parse(el.dataset.content);
|
|
846
|
+
formInstance.emit('sdkOnceOnlySelected', { key: component.path, value });
|
|
847
|
+
Swal.close();
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
Swal.getPopup().querySelectorAll('.delete-once-only-entry').forEach(el => {
|
|
852
|
+
el.addEventListener('click', async () => {
|
|
853
|
+
const id = el.dataset.id;
|
|
854
|
+
const result = await Swal.fire({
|
|
855
|
+
title: 'Sei sicuro di voler procedere?',
|
|
856
|
+
showDenyButton: true,
|
|
857
|
+
showCancelButton: false,
|
|
858
|
+
confirmButtonText: 'Elimina',
|
|
859
|
+
denyButtonText: 'Annulla',
|
|
860
|
+
customClass: {
|
|
861
|
+
confirmButton: 'btn btn-sm btn-danger',
|
|
862
|
+
denyButton: 'btn btn-sm btn-outline',
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
if (result.isConfirmed) {
|
|
866
|
+
try {
|
|
867
|
+
await this.authenticatedRequest('DELETE', `users/${userId}/profile-blocks/${id}`);
|
|
868
|
+
delete entries[id];
|
|
869
|
+
el.closest('li').remove();
|
|
870
|
+
if (Swal.getPopup().querySelectorAll('.delete-once-only-entry').length === 0) {
|
|
871
|
+
Swal.close();
|
|
872
|
+
}
|
|
873
|
+
} catch (err) {
|
|
874
|
+
console.error('[applyProfileBlocks] delete profile block error:', err);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
_traverseSubformComponents(components, callback) {
|
|
884
|
+
if (!components?.length) return;
|
|
885
|
+
components.forEach(component => {
|
|
886
|
+
callback(component);
|
|
887
|
+
if (component.components?.length) {
|
|
888
|
+
this._traverseSubformComponents(component.components, callback);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
571
893
|
}
|
|
572
894
|
|
|
573
895
|
function createFormioHelper(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencitylabs/formio-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Node/browser SDK helper for Form.io APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"author": "OpencityLabs",
|
|
25
25
|
"license": "ISC",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"axios": "^1.
|
|
27
|
+
"axios": "^1.18.1",
|
|
28
28
|
"jwt-decode": "^4.0.0",
|
|
29
29
|
"lodash.get": "^4.4.2",
|
|
30
|
-
"sweetalert2": "^11.26.
|
|
30
|
+
"sweetalert2": "^11.26.25"
|
|
31
31
|
}
|
|
32
32
|
}
|