@igea/oac_frontend 1.0.97 → 1.0.98
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/package.json
CHANGED
|
@@ -29,10 +29,13 @@ module.exports = function(serviceName) {
|
|
|
29
29
|
router.get('/view/:uuid', (req, res) => {
|
|
30
30
|
const { uuid } = req.params;
|
|
31
31
|
|
|
32
|
+
//const formId = OntologyForm.getIdFromUuid(uuid);
|
|
33
|
+
|
|
32
34
|
let data = new DataModel(req, {
|
|
33
35
|
root: serviceName,
|
|
34
36
|
title: 'Indagine viewer',
|
|
35
37
|
uuid,
|
|
38
|
+
//form_id: formId,
|
|
36
39
|
|
|
37
40
|
// 👇 NASCONDE MENU E SIDEBAR
|
|
38
41
|
activeMenu: null,
|
|
@@ -158,6 +158,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
158
158
|
form_keep_lock_timer: null,
|
|
159
159
|
form_keep_locking: false,
|
|
160
160
|
inEditing: false,
|
|
161
|
+
inEditingViewer: false,
|
|
161
162
|
isVisible: false,
|
|
162
163
|
enabled: el.dataset.editing == "true",
|
|
163
164
|
serializedForm: "",
|
|
@@ -190,6 +191,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
190
191
|
if (showForm && this.uuid) {
|
|
191
192
|
this.isVisible = true;
|
|
192
193
|
this.inEditing = true;
|
|
194
|
+
this.inEditingViewer = false;
|
|
193
195
|
this.resetShaclForm(this.uuid, true); // ❌ uuid non definito
|
|
194
196
|
}
|
|
195
197
|
|
|
@@ -242,6 +244,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
242
244
|
_this.form_id = null;
|
|
243
245
|
//_this.resetShaclForm(null, true);
|
|
244
246
|
clearInterval(_this.form_keep_lock_timer);
|
|
247
|
+
window.location.reload();
|
|
248
|
+
|
|
245
249
|
});
|
|
246
250
|
|
|
247
251
|
window.addEventListener('beforeunload', () => {
|
|
@@ -771,7 +775,58 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
771
775
|
},
|
|
772
776
|
reset() {
|
|
773
777
|
window.location.reload();
|
|
774
|
-
}
|
|
778
|
+
},
|
|
779
|
+
stopEdit() {
|
|
780
|
+
console.log('STOP EDIT (viewer)');
|
|
781
|
+
window.dispatchEvent(new CustomEvent('edit-stop'));
|
|
782
|
+
},
|
|
783
|
+
async startEdit() {
|
|
784
|
+
try {
|
|
785
|
+
// entra subito in editing → UI aggiornata
|
|
786
|
+
this.inEditingViewer = true;
|
|
787
|
+
this.enabled = true;
|
|
788
|
+
|
|
789
|
+
// cerco l'indagine tramite uuid
|
|
790
|
+
const res = await fetch('/backend/ontology/form/search', {
|
|
791
|
+
method: 'POST',
|
|
792
|
+
headers: { 'Content-Type': 'application/json' },
|
|
793
|
+
body: JSON.stringify({ query: this.uuid, limit: 1 })
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const response = await res.json();
|
|
797
|
+
if (!response.success || !response.data?.length) {
|
|
798
|
+
alert("Indagine non trovata");
|
|
799
|
+
this.inEditing = false;
|
|
800
|
+
this.enabled = false;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const item = response.data[0];
|
|
805
|
+
|
|
806
|
+
// lock
|
|
807
|
+
const lockResp = await fetch(
|
|
808
|
+
`/backend/ontology/form/lock/${item.id}/${CLIENT_UUID}`
|
|
809
|
+
).then(r => r.json());
|
|
810
|
+
|
|
811
|
+
if (!lockResp.success) {
|
|
812
|
+
alert("Indagine in modifica da un altro utente");
|
|
813
|
+
this.inEditing = false;
|
|
814
|
+
this.enabled = false;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// entra ufficialmente in editing
|
|
819
|
+
window.dispatchEvent(new CustomEvent('edit-item', {
|
|
820
|
+
detail: { id: item.id, uuid: item.uuid }
|
|
821
|
+
}));
|
|
822
|
+
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.error(e);
|
|
825
|
+
alert("Errore durante l'attivazione della modifica");
|
|
826
|
+
this.inEditing = false;
|
|
827
|
+
this.enabled = false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
775
830
|
}
|
|
776
831
|
});
|
|
777
832
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
//
|
|
2
3
|
// Place any custom JS here
|
|
3
4
|
//
|
|
@@ -11,14 +12,24 @@ const sparnatural = document.querySelector("spar-natural");
|
|
|
11
12
|
|
|
12
13
|
let viewMode = "direct";
|
|
13
14
|
|
|
15
|
+
// cache risultati
|
|
16
|
+
let directResults = null; // risposta SPARQL originale
|
|
17
|
+
let indaginiResults = null; // risposta SPARQL costruita
|
|
18
|
+
|
|
19
|
+
// per evitare race condition
|
|
20
|
+
let currentRunId = 0;
|
|
21
|
+
|
|
22
|
+
|
|
14
23
|
document.querySelectorAll('input[name="viewMode"]').forEach(radio => {
|
|
15
24
|
radio.addEventListener("change", e => {
|
|
16
25
|
viewMode = e.target.value;
|
|
17
|
-
|
|
26
|
+
updateView();
|
|
27
|
+
sparnatural.enablePlayBtn();
|
|
18
28
|
console.log("View mode:", viewMode);
|
|
19
29
|
});
|
|
20
30
|
});
|
|
21
31
|
|
|
32
|
+
|
|
22
33
|
/* =========================
|
|
23
34
|
INDAGINE RESOLVERS MAP
|
|
24
35
|
========================= */
|
|
@@ -38,6 +49,10 @@ const INDAGINE_RESOLVERS = {
|
|
|
38
49
|
},
|
|
39
50
|
"j.0:S13_Sample": {
|
|
40
51
|
property: "crm:P16_used_specific_object"
|
|
52
|
+
},
|
|
53
|
+
// 🆕 ATTIVITÀ DIAGNOSTICA
|
|
54
|
+
"crm:E7_Activity": {
|
|
55
|
+
property: "crm:P134_continued"
|
|
41
56
|
}
|
|
42
57
|
};
|
|
43
58
|
|
|
@@ -89,64 +104,179 @@ yasr.plugins["TableX"].config.uriHrefAdapter = function (uri) {
|
|
|
89
104
|
QUERY RESPONSE HANDLER
|
|
90
105
|
========================= */
|
|
91
106
|
|
|
92
|
-
yasqe.on("queryResponse", function (_yasqe, response, duration) {
|
|
93
|
-
|
|
107
|
+
yasqe.on("queryResponse", async function (_yasqe, response, duration) {
|
|
108
|
+
const runId = ++currentRunId;
|
|
94
109
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
const normalized = normalizeSparqlResponse(response);
|
|
111
|
+
sparnatural.enablePlayBtn();
|
|
112
|
+
|
|
113
|
+
if (!normalized) {
|
|
114
|
+
directResults = null;
|
|
115
|
+
indaginiResults = null;
|
|
116
|
+
yasr.setResponse(emptyResponse("Risposta SPARQL non valida"), duration);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// salva risultati diretti
|
|
121
|
+
directResults = normalized;
|
|
122
|
+
|
|
123
|
+
// mostra subito i diretti
|
|
124
|
+
yasr.setResponse(directResults, duration);
|
|
125
|
+
|
|
126
|
+
// calcola DERIVATE (in background)
|
|
127
|
+
// CASO SPECIALE: ricerca INDAGINE
|
|
128
|
+
if (await isIndagineSearch(normalized)) {
|
|
129
|
+
|
|
130
|
+
// Tabella B = stessa risposta della A
|
|
131
|
+
indaginiResults = normalized;
|
|
132
|
+
|
|
133
|
+
if (viewMode === "indagine") {
|
|
134
|
+
yasr.setResponse(indaginiResults);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} else {
|
|
138
|
+
resolveIndagini(normalized, runId);
|
|
139
|
+
}
|
|
102
140
|
|
|
103
141
|
|
|
104
|
-
sparnatural.enablePlayBtn();
|
|
105
142
|
});
|
|
106
143
|
|
|
144
|
+
|
|
107
145
|
/* =========================
|
|
108
146
|
RESOLVER PIPELINE
|
|
109
147
|
========================= */
|
|
110
148
|
|
|
111
|
-
function
|
|
149
|
+
async function isIndagineSearch(response) {
|
|
112
150
|
const uris = extractUrisFromResponse(response);
|
|
151
|
+
if (!uris.length) return false;
|
|
152
|
+
|
|
153
|
+
// sicurezza: tutte sotto namespace indagine
|
|
154
|
+
if (!uris.every(u => u.startsWith("http://indagine/"))) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// controllo RDF type
|
|
159
|
+
return await areAllIndaginiRoot(uris);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function updateView() {
|
|
163
|
+
if (viewMode === "direct") {
|
|
164
|
+
yasr.setResponse(directResults ?? emptyResponse("Nessun risultato"));
|
|
165
|
+
} else if (viewMode === "indagine") {
|
|
166
|
+
yasr.setResponse(indaginiResults ?? emptyResponse("Calcolo in corso..."));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function areAllIndaginiRoot(uris) {
|
|
171
|
+
const values = uris.map(u => `(<${u}>)`).join(" ");
|
|
172
|
+
|
|
173
|
+
const query = `
|
|
174
|
+
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
175
|
+
PREFIX crm: <http://www.cidoc-crm.org/cidoc-crm/>
|
|
176
|
+
|
|
177
|
+
SELECT ?x ?type WHERE {
|
|
178
|
+
VALUES (?x) { ${values} }
|
|
113
179
|
|
|
114
|
-
|
|
180
|
+
?x rdf:type crm:E7_Activity .
|
|
181
|
+
|
|
182
|
+
# root = non deve essere continuazione di un'altra activity
|
|
183
|
+
FILTER NOT EXISTS {
|
|
184
|
+
?parent crm:P134_continued ?x .
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const data = await fetchSparql(query);
|
|
190
|
+
const bindings = data?.results?.bindings || [];
|
|
191
|
+
|
|
192
|
+
// devono tornare TUTTE
|
|
193
|
+
return bindings.length === uris.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
function resolveIndagini(response, runId) {
|
|
199
|
+
const uris = extractUrisFromResponse(response);
|
|
200
|
+
|
|
201
|
+
if (!uris.length) {
|
|
202
|
+
indaginiResults = emptyResponse("Nessuna indagine collegata");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resolvedIndagini.clear();
|
|
207
|
+
|
|
208
|
+
let pending = 0;
|
|
115
209
|
|
|
116
210
|
uris.forEach(uri => {
|
|
117
|
-
|
|
211
|
+
pending++;
|
|
212
|
+
resolveIndagineFromUri(uri, runId, () => {
|
|
213
|
+
pending--;
|
|
214
|
+
if (pending === 0 && runId === currentRunId) {
|
|
215
|
+
buildIndaginiResponse();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
118
218
|
});
|
|
119
219
|
}
|
|
120
220
|
|
|
121
|
-
|
|
221
|
+
|
|
222
|
+
function buildIndaginiResponse() {
|
|
223
|
+
const bindings = Array.from(resolvedIndagini).map(uri => ({
|
|
224
|
+
indagine: { type: "uri", value: uri }
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
indaginiResults = bindings.length
|
|
228
|
+
? {
|
|
229
|
+
head: { vars: ["indagine"] },
|
|
230
|
+
results: { bindings }
|
|
231
|
+
}
|
|
232
|
+
: emptyResponse("Nessuna indagine collegata");
|
|
233
|
+
|
|
234
|
+
// se l’utente è già in vista “indagine”, aggiorna subito
|
|
235
|
+
if (viewMode === "indagine") {
|
|
236
|
+
yasr.setResponse(indaginiResults);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
function resolveIndagineFromUri(uri, runId, done) {
|
|
122
242
|
const typeQuery = `
|
|
123
243
|
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
124
|
-
|
|
125
|
-
SELECT DISTINCT ?type
|
|
126
|
-
WHERE {
|
|
244
|
+
SELECT DISTINCT ?type WHERE {
|
|
127
245
|
<${uri}> rdf:type ?type .
|
|
128
246
|
}
|
|
129
|
-
LIMIT 10
|
|
130
247
|
`;
|
|
131
248
|
|
|
132
249
|
fetchSparql(typeQuery).then(data => {
|
|
133
250
|
const types = data.results.bindings
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
251
|
+
.map(b => normalizeRdfType(b.type.value))
|
|
252
|
+
.filter(Boolean);
|
|
253
|
+
|
|
254
|
+
let innerPending = 0;
|
|
255
|
+
|
|
256
|
+
types.forEach(t => {
|
|
257
|
+
const resolver = INDAGINE_RESOLVERS[t];
|
|
258
|
+
if (!resolver) return;
|
|
259
|
+
|
|
260
|
+
innerPending++;
|
|
261
|
+
const q = buildResolverQuery(uri, resolver.property);
|
|
262
|
+
|
|
263
|
+
fetchSparql(q).then(r => {
|
|
264
|
+
r.results?.bindings?.forEach(b => {
|
|
265
|
+
if (b.indagine?.type === "uri") {
|
|
266
|
+
resolvedIndagini.add(b.indagine.value);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}).finally(() => {
|
|
270
|
+
innerPending--;
|
|
271
|
+
if (innerPending === 0) done();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
146
274
|
|
|
275
|
+
if (types.length === 0) done();
|
|
147
276
|
});
|
|
148
277
|
}
|
|
149
278
|
|
|
279
|
+
|
|
150
280
|
let resolvedIndagini = new Set();
|
|
151
281
|
|
|
152
282
|
function fetchIndagini(query) {
|
|
@@ -46,9 +46,7 @@
|
|
|
46
46
|
|
|
47
47
|
<div class="viewer-container">
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
<h2>Indagine</h2>
|
|
51
|
-
</div>
|
|
49
|
+
|
|
52
50
|
|
|
53
51
|
{# --------------------------------------------------
|
|
54
52
|
Vue root (SENZA search)
|
|
@@ -58,12 +56,39 @@
|
|
|
58
56
|
id="investigation-app"
|
|
59
57
|
data-cur_role="{{ user.role }}"
|
|
60
58
|
data-uuid="{{ uuid }}"
|
|
61
|
-
data-
|
|
59
|
+
data-form-id="{{ form_id }}"
|
|
60
|
+
data-editing="false"
|
|
62
61
|
data-show-form="true"
|
|
63
62
|
data-label_save_ok="{{ t('investigation.save_ok') }}"
|
|
64
63
|
data-label_save_err="{{ t('investigation.save_err') }}"
|
|
65
64
|
>
|
|
66
65
|
|
|
66
|
+
<div class="viewer-header">
|
|
67
|
+
{% if user.role != 3 %}
|
|
68
|
+
<!-- MODIFICA -->
|
|
69
|
+
<button v-if="!inEditingViewer"
|
|
70
|
+
title="{{ t('investigation.edit_item') }}"
|
|
71
|
+
@click="startEdit"
|
|
72
|
+
type="button"
|
|
73
|
+
class="btn btn-info">
|
|
74
|
+
<i class="fa-solid fa-pen-to-square"></i>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
<!-- STOP EDITING -->
|
|
78
|
+
<button v-if="inEditingViewer"
|
|
79
|
+
title="{{ t('investigation.stop_edit') }}"
|
|
80
|
+
@click="stopEdit"
|
|
81
|
+
type="button"
|
|
82
|
+
class="btn btn-success">
|
|
83
|
+
<i class="fa-solid fa-circle-stop" style="color:red;"></i>
|
|
84
|
+
</button>
|
|
85
|
+
|
|
86
|
+
{% endif %}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
</div>
|
|
91
|
+
|
|
67
92
|
|
|
68
93
|
<template v-if="isVisible">
|
|
69
94
|
|
|
@@ -81,7 +106,7 @@
|
|
|
81
106
|
TURTLE
|
|
82
107
|
</button>
|
|
83
108
|
|
|
84
|
-
<button v-if="
|
|
109
|
+
<button v-if="inEditingViewer && validForm"
|
|
85
110
|
:disabled="saving"
|
|
86
111
|
class="btn btn-primary mx-2"
|
|
87
112
|
@click="save(false)">
|
|
@@ -89,6 +114,8 @@
|
|
|
89
114
|
<i v-else class="fa-solid fa-save"></i>
|
|
90
115
|
</button>
|
|
91
116
|
|
|
117
|
+
|
|
118
|
+
|
|
92
119
|
</div>
|
|
93
120
|
{% endif %}
|
|
94
121
|
|
|
@@ -110,7 +137,7 @@
|
|
|
110
137
|
|
|
111
138
|
{# ---------------- DEBUG OUTPUT (admin) ---------------- #}
|
|
112
139
|
{% if user.role in [0,1] %}
|
|
113
|
-
<fieldset v-if="
|
|
140
|
+
<fieldset v-if="inEditingViewer"
|
|
114
141
|
style="margin-top:15px;
|
|
115
142
|
border: solid 1px gray;
|
|
116
143
|
border-radius: 6px;
|
|
@@ -150,6 +177,72 @@ function tryCloseViewer() {
|
|
|
150
177
|
if (hint) hint.style.display = 'block';
|
|
151
178
|
}, 200);
|
|
152
179
|
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async function startEditFromViewer(btn) {
|
|
183
|
+
if (btn) {
|
|
184
|
+
btn.disabled = true;
|
|
185
|
+
btn.classList.add('disabled');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const appEl = document.getElementById('investigation-app');
|
|
189
|
+
if (!appEl) return;
|
|
190
|
+
|
|
191
|
+
const uuid = appEl.dataset.uuid;
|
|
192
|
+
if (!uuid) {
|
|
193
|
+
alert("UUID non disponibile");
|
|
194
|
+
if (btn) btn.disabled = false;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// 🔍 riuso search
|
|
200
|
+
const res = await fetch('/backend/ontology/form/search', {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
203
|
+
body: JSON.stringify({ query: uuid, limit: 1 })
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const response = await res.json();
|
|
207
|
+
|
|
208
|
+
if (!response.success || !response.data?.length) {
|
|
209
|
+
alert("Indagine non trovata");
|
|
210
|
+
if (btn) btn.disabled = false;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const item = response.data[0];
|
|
215
|
+
|
|
216
|
+
// 🔐 lock
|
|
217
|
+
const lockResp = await fetch(
|
|
218
|
+
`/backend/ontology/form/lock/${item.id}/${window.CLIENT_UUID}`
|
|
219
|
+
).then(r => r.json());
|
|
220
|
+
|
|
221
|
+
if (!lockResp.success) {
|
|
222
|
+
alert("Indagine in modifica da un altro UTENTES");
|
|
223
|
+
if (btn) btn.disabled = false;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 🔥 entra in editing
|
|
228
|
+
window.dispatchEvent(
|
|
229
|
+
new CustomEvent('edit-item', {
|
|
230
|
+
detail: {
|
|
231
|
+
id: item.id,
|
|
232
|
+
uuid: item.uuid
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error(e);
|
|
239
|
+
alert("Errore durante l'attivazione della modifica");
|
|
240
|
+
if (btn) btn.disabled = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
153
246
|
</script>
|
|
154
247
|
|
|
155
|
-
{% endblock %}
|
|
248
|
+
{% endblock %}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
<div class="container my-4">
|
|
6
6
|
|
|
7
7
|
<h2>Indagine</h2>
|
|
8
|
+
|
|
9
|
+
<h2 class="d-flex justify-content-between align-items-center">
|
|
10
|
+
<span>Indagine</span>
|
|
11
|
+
|
|
12
|
+
<button id="btn-edit"
|
|
13
|
+
class="btn btn-primary btn-sm">
|
|
14
|
+
<i class="fa-solid fa-pen"></i> Modifica
|
|
15
|
+
</button>
|
|
16
|
+
</h2>
|
|
8
17
|
|
|
9
18
|
<div id="shacl-container"
|
|
10
19
|
style="margin-top:10px;
|
|
@@ -25,4 +34,19 @@
|
|
|
25
34
|
|
|
26
35
|
</div>
|
|
27
36
|
|
|
37
|
+
<script>
|
|
38
|
+
document.getElementById('btn-edit')?.addEventListener('click', function () {
|
|
39
|
+
const form = document.getElementById('shacl-form');
|
|
40
|
+
if (!form) return;
|
|
41
|
+
|
|
42
|
+
// rimuove readonly → entra in editing
|
|
43
|
+
form.removeAttribute('data-readonly');
|
|
44
|
+
|
|
45
|
+
// feedback UI
|
|
46
|
+
this.disabled = true;
|
|
47
|
+
this.innerHTML = '<i class="fa-solid fa-lock-open"></i> Modifica attiva';
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
|
|
28
52
|
{% endblock %}
|