@igea/oac_frontend 1.0.48 → 1.0.50

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igea/oac_frontend",
3
- "version": "1.0.48",
3
+ "version": "1.0.50",
4
4
  "description": "Frontend service for the OAC project",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,15 +4,25 @@ const DataModel = require('../models/DataModel');
4
4
 
5
5
  module.exports = function(serviceName) {
6
6
  /**
7
- * @route GET /users/manage: redirect to the users page with list of users
7
+ * @route GET /investigation/form/:uuid? redirect to the investigation form
8
8
  */
9
- router.get('/new', (req, res) => {
9
+
10
+ const renderForm = function(req, res, uuid){
10
11
  let data = new DataModel(req, {
11
12
  root: serviceName,
12
- title: 'Ivestigation Creation',
13
+ title: 'Investigation Creation',
13
14
  currentPath: req.baseUrl +req.path,
15
+ uuid
14
16
  });
15
- res.render('investigation/new.twig', data.toJson());
17
+ res.render('investigation/form.twig', data.toJson());
18
+ }
19
+
20
+ router.get('/form', (req, res) => {
21
+ renderForm(req, res, null);
22
+ });
23
+
24
+ router.get('/form/:uuid', (req, res) => {
25
+ renderForm(req, res, req.params.uuid);
16
26
  });
17
27
 
18
28
  return router
@@ -86,6 +86,18 @@
86
86
  "max_size": "Each file must be less than 10MB"
87
87
  }
88
88
  },
89
+ "investigation":{
90
+ "new": {
91
+ "title": "New Investigation",
92
+ "description": "Compila il modulo sottostante per creare una nuova indagine.",
93
+ "form_invalid": "The form contains errors. Please correct them before submitting.",
94
+ "submit": "Save",
95
+ "reset": "Reset",
96
+ "show_more": "Show more",
97
+ "confirm": "Confirm",
98
+ "download": "Download"
99
+ }
100
+ },
89
101
  "search": {
90
102
  "fast": {
91
103
  "title": "Fast search"
@@ -89,6 +89,18 @@
89
89
  "invalid_file": "Tipo di file non valido"
90
90
  }
91
91
  },
92
+ "investigation": {
93
+ "new": {
94
+ "title": "Nuova Indagine",
95
+ "description": "Compila il modulo sottostante per creare una nuova indagine.",
96
+ "form_invalid": "La scheda contiene errori. Correggerli per poterla salvare correttamente.",
97
+ "submit": "Invia",
98
+ "reset": "Reimposta",
99
+ "show_more": "Mostra di più",
100
+ "confirm": "Conferma",
101
+ "download": "Scarica"
102
+ }
103
+ },
92
104
  "search": {
93
105
  "title" : "Ricerca",
94
106
  "fast": {
Binary file
@@ -0,0 +1,273 @@
1
+ const {createApp} = Vue;
2
+
3
+ const appId = 'investigation-app';
4
+ const shaclId = 'shacl-form';
5
+ const templateIRIToExcludeFromSearch = [
6
+ "http://diagnostica/indagine/$UUID$"
7
+ ];
8
+
9
+ document.addEventListener('DOMContentLoaded', () => {
10
+ const el = document.getElementById(appId);
11
+
12
+ const app = createApp({
13
+ delimiters: ['{@', '@}'],
14
+ data() {
15
+ return {
16
+ cur_role: parseInt(el.dataset.cur_role),
17
+ uuid: parseInt(el.dataset.uuid) || null,
18
+ form: null,
19
+ enabled: el.dataset.editing == "true",
20
+ serializedForm: "",
21
+ validForm: false,
22
+ labels: {
23
+
24
+ },
25
+ search: {
26
+ offset: 0,
27
+ limit: 10,
28
+ prefix: "",
29
+ results: [],
30
+ end: false,
31
+ input: null,
32
+ selected: null
33
+ }
34
+ }
35
+ },
36
+ mounted() {
37
+ this.initShaclForm();
38
+ this.load();
39
+ },
40
+ computed:{
41
+ outputStyle(){
42
+ var style = {
43
+ "color": "red",
44
+ "font-weight": "bold",
45
+ "max-height": "400px",
46
+ "overflow-y": "auto",
47
+ "white-space": "pre-wrap"
48
+ }
49
+ if(this.validForm)
50
+ style["color"] = "green";
51
+ return style;
52
+ }
53
+ },
54
+ methods: {
55
+ download(outFormat){
56
+ this.openPostInNewTab("/backend/ontology/convert/ttl/" + outFormat, {
57
+ file: this.serializedForm
58
+ });
59
+ },
60
+ openPostInNewTab(url, params) {
61
+ const form = document.createElement("form");
62
+ form.method = "POST";
63
+ form.action = url;
64
+ form.target = "_blank";
65
+ for (const key in params) {
66
+ if (params.hasOwnProperty(key)) {
67
+ const input = document.createElement("input");
68
+ input.type = "hidden";
69
+ input.name = key;
70
+ input.value = params[key];
71
+ form.appendChild(input);
72
+ }
73
+ }
74
+ document.body.appendChild(form);
75
+ form.submit();
76
+ form.remove();
77
+ },
78
+ initShaclForm() {
79
+ var _this = this;
80
+ this.form = document.querySelector("shacl-form");
81
+ const output = document.getElementById("shacl-output")
82
+ this.form.addEventListener('change', event => {
83
+ // check if form data validates according to the SHACL shapes
84
+ _this.validForm = event.detail?.valid;
85
+ _this.serializedForm = _this.form.serialize();
86
+ });
87
+ this.form.addEventListener("ready", () => {
88
+ var intervalId = setInterval(() => {
89
+ if(_this.form.shadowRoot){
90
+ clearInterval(intervalId);
91
+ _this.inputIdentifizier();
92
+ if(!_this.enabled)
93
+ _this.disableInteractions(_this.form);
94
+ }
95
+ }, 100);
96
+ });
97
+ },
98
+ searchByPrefixStart(rokitInput, prefix){
99
+ this.search.offset = 0;
100
+ this.search.prefix = prefix;
101
+ this.search.results = [];
102
+ this.search.end = false;
103
+ this.search.input = rokitInput;
104
+ this.search.selected = null;
105
+ if(prefix) this.searchByPrefix();
106
+ },
107
+ confirmSelection(){
108
+ if(this.search.selected && this.search.selected.instance)
109
+ this.search.input.value = this.search.selected.instance;
110
+ this.searchByPrefixStart(null, null);
111
+ },
112
+ searchByPrefix(){
113
+ if(this.search.end) return;
114
+ var _this = this;
115
+ var request = axios.post("/backend/fuseki/search/by-prefix", {
116
+ limit: this.search.limit,
117
+ offset: this.search.offset,
118
+ prefix: this.search.prefix
119
+ });
120
+ request.then(response => {
121
+ var data = response.data;
122
+ if(data.success){
123
+ _this.search.results = _this.search.results.concat(data.data);
124
+ _this.search.offset += data.data.length;
125
+ if(data.data.length < _this.search.limit){
126
+ _this.search.end = true;
127
+ }
128
+ }
129
+ }).catch(error => {
130
+ console.log(error);
131
+ });
132
+ },
133
+ monitorChanges(root, callback) {
134
+ const observer = new MutationObserver(mutations => {
135
+ mutations.forEach(mutation => {
136
+ callback(mutation);
137
+ });
138
+ });
139
+ observer.observe(root, {
140
+ childList: true, // nuove aggiunte / rimozioni di nodi
141
+ subtree: true // osserva rischiosamente tutto l’albero
142
+ //attributes: true, // monitora cambi di attributi
143
+ //characterData: true // monitora testo interno dei nodi
144
+ });
145
+ return observer;
146
+ },
147
+ inputIdentifizier(){
148
+ const form = this.form;
149
+ const _this = this;
150
+
151
+ const disableIDField = () => {
152
+ const shadow = form.shadowRoot;
153
+ if (!shadow) return false;
154
+
155
+ // Trova la label "ID"
156
+ const labels = Array.from(shadow.querySelectorAll("label"))
157
+ .filter(l => l.textContent.trim() === "ID");
158
+
159
+ if (labels.length == 0) return false;
160
+ for(var i=0; i<labels.length; i++){
161
+ const label = labels[i];
162
+ if (!label) continue;
163
+ // Container della property
164
+ const container = label.closest(".property-instance");
165
+ if (!container) return false;
166
+
167
+ // Disabilita rokit-input
168
+ const rokitInput = container.querySelector("rokit-input");
169
+ if (rokitInput) {
170
+ if(rokitInput.value == ""){
171
+ rokitInput.value = _this.generateIRI(rokitInput.placeholder);
172
+ }
173
+ rokitInput.setAttribute("disabled", "true");
174
+ rokitInput.style.opacity = "0.6";
175
+ rokitInput.style.pointerEvents = "none";
176
+
177
+ var escludeSearchButton = templateIRIToExcludeFromSearch.includes(rokitInput.placeholder);
178
+
179
+ if(_this.enabled && !escludeSearchButton){
180
+ // Aggiungo bottone IMG per la ricerca
181
+ let next = label.nextElementSibling;
182
+ let imgClass = "search-identifier-icon";
183
+ if (!(next && next.tagName === "IMG"
184
+ && next.classList.contains(imgClass))) {
185
+ // creo immagine
186
+ const img = document.createElement("img");
187
+ img.src = "/frontend/images/search.png";
188
+ img.style.width = "24px"; img.style.height = "24px";
189
+ img.style["margin-left"] = "5px"; img.style["margin-right"] = "5px";
190
+ img.style.cursor = "pointer";
191
+ img.classList.add(imgClass);
192
+ img.onclick = function(){
193
+ var prefix = rokitInput.placeholder.replace("$uuid$", "").replace("$UUID$", "");
194
+ prefix = "http://diagnostica/vocabularies/quesito-diagnostico/";
195
+ _this.searchByPrefixStart(rokitInput, prefix);
196
+ }
197
+ // inserisco subito dopo la label
198
+ label.insertAdjacentElement("afterend", img);
199
+ }
200
+ }
201
+ }
202
+ // SHACL NODE superiore che contiene TUTTO il blocco ID
203
+ const shaclNode = container.closest("shacl-node");
204
+ // Rimuovi TUTTI i remove-button
205
+ if (shaclNode) {
206
+ shaclNode.querySelectorAll("div.remove-button-wrapper").forEach(el => {
207
+ el.style.display = "none";
208
+ });
209
+ shaclNode.querySelectorAll("rokit-button.remove-button").forEach(btn => {
210
+ btn.style.display = "none";
211
+ });
212
+ // Rimuovi TUTTI gli add-button all'interno del nodo ID
213
+ shaclNode.querySelectorAll("rokit-select.add-button").forEach(btn => {
214
+ btn.style.display = "none";
215
+ });
216
+ }
217
+ }
218
+ return true;
219
+ };
220
+
221
+ const observer = this.monitorChanges(form.shadowRoot, (mutation) => {
222
+ disableIDField();
223
+ });
224
+
225
+ disableIDField();
226
+ },
227
+ disableInteractions(root) {
228
+ root.style.pointerEvents = 'none';
229
+
230
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
231
+
232
+ while (walker.nextNode()) {
233
+ const el = walker.currentNode;
234
+
235
+ // Blocca click e interazioni mouse
236
+ el.style.pointerEvents = 'none';
237
+
238
+ // Blocca editing su input / textarea / select
239
+ if (el instanceof HTMLInputElement ||
240
+ el instanceof HTMLTextAreaElement ||
241
+ el instanceof HTMLSelectElement ||
242
+ el instanceof HTMLButtonElement) {
243
+ el.disabled = true;
244
+ }
245
+
246
+ // Blocca contenteditable
247
+ if (el.isContentEditable) {
248
+ el.contentEditable = "false";
249
+ }
250
+
251
+ // Ricorsione dentro Shadow DOM (se presente)
252
+ if (el.shadowRoot) {
253
+ disableInteractions(el.shadowRoot);
254
+ }
255
+ }
256
+ },
257
+ generateIRI(template, uuid){
258
+ var _uuid = uuid || crypto.randomUUID();
259
+ return template.replace("$uuid$", _uuid).replace("$UUID$", _uuid);
260
+ },
261
+ load() {
262
+
263
+ },
264
+ save() {
265
+
266
+ },
267
+ reset() {
268
+ window.location.reload();
269
+ }
270
+ }
271
+ });
272
+ app.mount(`#${appId}`);
273
+ });
@@ -0,0 +1,157 @@
1
+ {% set showSidebar = true %}
2
+ {% extends "../base.twig" %}
3
+
4
+ {% block extendHead %}
5
+ <script src="/{{ root }}/js/lib/shacl-form.bundle.js" type="module"></script>
6
+ <script src="/{{ root }}/js/app/vue-investigation.js"></script>
7
+
8
+
9
+ <style>
10
+ /* Sfondo fullscreen semi-trasparente */
11
+ .modal-overlay {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ width: 100%;
16
+ height: 100%;
17
+ background: rgba(0, 0, 0, 0.6); /* nero ombreggiato */
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ z-index: 1000;
22
+ visibility: visible;
23
+ transition: opacity 0.3s ease;
24
+ }
25
+
26
+ /* Box centrale della modale */
27
+ .modal-content {
28
+ background: #fff;
29
+ padding: 20px;
30
+ border-radius: 8px;
31
+ max-width: 650px;
32
+ width: 90%;
33
+ box-shadow: 0 5px 20px rgba(0,0,0,0.3);
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Label */
39
+ .modal-content label {
40
+ font-weight: bold;
41
+ }
42
+
43
+ /* Dropdown a 2 colonne */
44
+ .modal-content select {
45
+ width: 100%;
46
+ display: grid;
47
+ grid-template-columns: 1fr 1fr; /* 2 colonne */
48
+ gap: 10px;
49
+ padding: 5px;
50
+ font-size: 16px;
51
+ }
52
+
53
+ /* Bottone chiusura */
54
+ .close-btn {
55
+ align-self: flex-end;
56
+ background: transparent;
57
+ border: none;
58
+ font-size: 40px;
59
+ cursor: pointer;
60
+ float: right;
61
+ }
62
+ </style>
63
+
64
+ {% endblock %}
65
+
66
+ {% block content %}
67
+
68
+ <div>
69
+ <h2 style="display:flex; justify-content:space-between; align-items:center;">
70
+ <span>{{ t('investigation.new.title') }}:</span>
71
+ </h2>
72
+ </div>
73
+
74
+ <div id="investigation-app"
75
+ data-cur_role="{{ user.role }}"
76
+ data-uuid="{{ uuid }}"
77
+ data-editing="{{ user.role != 3 }}"
78
+ >
79
+
80
+ <shacl-form id="shacl-form" data-collapse="open"
81
+ data-shapes-url="/backend/ontology/schema/ttl2">
82
+ </shacl-form>
83
+
84
+ {% if user.role != 3 %}
85
+ <!-- Buttons -->
86
+ <div class="col-12 text-center mt-4">
87
+ <button v-if="validForm" @click="save()" type="submit" class="btn btn-primary me-2">{{ t('investigation.new.submit') }}</button>
88
+ <button style="margin-left:30px;" title="{{ t('investigation.new.download') }}"
89
+ @click="download('xml')" type="button" class="btn btn-secondary">
90
+ <span>RDF/XML</span><i class="fa-solid fa-file-arrow-down"></i>
91
+ </button>
92
+ <button style="margin-left:30px;" title="{{ t('investigation.new.download') }}"
93
+ @click="download('ttl')" type="button" class="btn btn-secondary">
94
+ <span>TURTLE</span><i class="fa-solid fa-file-arrow-down"></i>
95
+ </button>
96
+ <button style="margin-left:30px;" title="{{ t('investigation.new.reset') }}"
97
+ @click="reset()" type="button" class="btn btn-secondary">
98
+ <i class="fa-solid fa-broom"></i>
99
+ </button>
100
+ </div>
101
+ {% if user.role in [0, 1] %}
102
+ <fieldset id="shacl-output" style="margin-top:10px;
103
+ border: solid 1px gray;
104
+ border-radius: 10px;
105
+ padding: 10px;">
106
+ <legend>Generated output: [{@ validForm @}]</legend>
107
+ <pre :style="outputStyle">{@ serializedForm @}</pre>
108
+ </fieldset>
109
+ {% endif %}
110
+ {% else %}
111
+ <div class="col-12 text-center mt-4">
112
+ <button @click="download('xml')" type="button" class="btn btn-secondary">
113
+ <span>RDF/XML</span><i class="fa-solid fa-file-arrow-down"></i>
114
+ </button>
115
+ <button style="margin-left:30px;" title="{{ t('investigation.new.download') }}"
116
+ @click="download('ttl')" type="button" class="btn btn-secondary">
117
+ <span>TURTLE</span><i class="fa-solid fa-file-arrow-down"></i>
118
+ </button>
119
+ </div>
120
+ {% endif %}
121
+
122
+ <div style="margin-top:200px;"></div>
123
+
124
+ <div class="modal-overlay" v-cloak v-if="search.input != null">
125
+ <div class="modal-content">
126
+ <div class="row" style="line-height:40px; margin-bottom:10px;">
127
+ <span style="font-weight:bold; float:left; max-width:570px;">
128
+ Seleziona istanza:
129
+ </span>
130
+ <span style="float:right; max-width:24px;" class="close-btn" @click="searchByPrefixStart(null, null);">&times;</span>
131
+ </div>
132
+ <select size="10" v-model="search.selected">
133
+ <option v-for="item in search.results" :value="item">{@ (item.label && item.label.trim()) ? item.label : item.instance @}</option>
134
+ </select>
135
+ <div v-if="search.selected">
136
+ <span style="font-size:12px; color:gray;">{@ search.selected.instance @}</span>
137
+ </div>
138
+ <div style="margin-top:10px;">
139
+ <span @click="searchByPrefix()" v-if="!search.end" style="float:left;
140
+ cursor: pointer; color: blue;
141
+ border: solid 1px blue;
142
+ border-radius: 4px; padding: 4px;">
143
+ {{ t('investigation.new.show_more') }}...
144
+ </span>
145
+ <span v-if="search.selected" @click="confirmSelection()" style="float:right;
146
+ cursor: pointer; color: green;
147
+ border: solid 1px green;
148
+ border-radius: 4px; padding: 4px;">
149
+ {{ t('investigation.new.confirm') }}
150
+ </span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ </div>
156
+
157
+ {% endblock %}
@@ -47,12 +47,16 @@
47
47
  <i class="fa-solid fa-book-bookmark"></i>
48
48
  <span>{{ t('users.vocabolaries.title') }}</span>
49
49
  </a>
50
- <a href="/{{ root }}/investigation/new" class="{% if '/investigation/new' in currentPath %}active{% endif %}">
51
- <i class="fa-solid fa-clipboard-list"></i>
52
- <span>Indagine</span>
53
- </a>
54
50
  </div>
55
51
  </div>
52
+
53
+ <div class="menu-item">
54
+ <a href="/{{ root }}/investigation/form" class="{% if '/investigation/form' in currentPath %}active{% endif %}">
55
+ <i class="fa-solid fa-clipboard-list"></i>
56
+ <span>{{ t('investigation.new.title') }}</span>
57
+ </a>
58
+ </div>
59
+
56
60
  {% endif %}
57
61
 
58
62
  {# Ricerca #}
@@ -1,32 +0,0 @@
1
- {% set showSidebar = true %}
2
- {% extends "../base.twig" %}
3
-
4
- {% block extendHead %}
5
- <script src="/{{ root }}/js/lib/shacl-form.bundle.js" type="module"></script>
6
- {% endblock %}
7
-
8
- {% block content %}
9
-
10
- <div>
11
- <h2 style="display:flex; justify-content:space-between; align-items:center;">
12
- <span>Nuova Indagine:</span>
13
- </h2>
14
- </div>
15
-
16
- <shacl-form data-shapes-url="/backend/ontology/schema/ttl2"></shacl-form>
17
-
18
- <script>
19
- const form = document.querySelector("shacl-form")
20
- form.addEventListener('change', event => {
21
- // check if form data validates according to the SHACL shapes
22
- if (event.detail?.valid) {
23
- // get data graph as RDF triples and
24
- // log them to the browser console
25
- const triples = form.serialize()
26
- console.log('entered form data', triples)
27
- // store the data somewhere, e.g. in a triple store
28
- }
29
- })
30
- </script>
31
-
32
- {% endblock %}