@igea/oac_frontend 1.0.48 → 1.0.49

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.49",
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,17 @@
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
+ }
99
+ },
89
100
  "search": {
90
101
  "fast": {
91
102
  "title": "Fast search"
@@ -89,6 +89,17 @@
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
+ }
102
+ },
92
103
  "search": {
93
104
  "title" : "Ricerca",
94
105
  "fast": {
Binary file
@@ -0,0 +1,250 @@
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
+ initShaclForm() {
56
+ var _this = this;
57
+ this.form = document.querySelector("shacl-form");
58
+ const output = document.getElementById("shacl-output")
59
+ this.form.addEventListener('change', event => {
60
+ // check if form data validates according to the SHACL shapes
61
+ _this.validForm = event.detail?.valid;
62
+ _this.serializedForm = _this.form.serialize();
63
+ });
64
+ this.form.addEventListener("ready", () => {
65
+ var intervalId = setInterval(() => {
66
+ if(_this.form.shadowRoot){
67
+ clearInterval(intervalId);
68
+ _this.inputIdentifizier();
69
+ if(!_this.enabled)
70
+ _this.disableInteractions(_this.form);
71
+ }
72
+ }, 100);
73
+ });
74
+ },
75
+ searchByPrefixStart(rokitInput, prefix){
76
+ this.search.offset = 0;
77
+ this.search.prefix = prefix;
78
+ this.search.results = [];
79
+ this.search.end = false;
80
+ this.search.input = rokitInput;
81
+ this.search.selected = null;
82
+ if(prefix) this.searchByPrefix();
83
+ },
84
+ confirmSelection(){
85
+ if(this.search.selected && this.search.selected.instance)
86
+ this.search.input.value = this.search.selected.instance;
87
+ this.searchByPrefixStart(null, null);
88
+ },
89
+ searchByPrefix(){
90
+ if(this.search.end) return;
91
+ var _this = this;
92
+ var request = axios.post("/backend/fuseki/search/by-prefix", {
93
+ limit: this.search.limit,
94
+ offset: this.search.offset,
95
+ prefix: this.search.prefix
96
+ });
97
+ request.then(response => {
98
+ var data = response.data;
99
+ if(data.success){
100
+ _this.search.results = _this.search.results.concat(data.data);
101
+ _this.search.offset += data.data.length;
102
+ if(data.data.length < _this.search.limit){
103
+ _this.search.end = true;
104
+ }
105
+ }
106
+ }).catch(error => {
107
+ console.log(error);
108
+ });
109
+ },
110
+ monitorChanges(root, callback) {
111
+ const observer = new MutationObserver(mutations => {
112
+ mutations.forEach(mutation => {
113
+ callback(mutation);
114
+ });
115
+ });
116
+ observer.observe(root, {
117
+ childList: true, // nuove aggiunte / rimozioni di nodi
118
+ subtree: true // osserva rischiosamente tutto l’albero
119
+ //attributes: true, // monitora cambi di attributi
120
+ //characterData: true // monitora testo interno dei nodi
121
+ });
122
+ return observer;
123
+ },
124
+ inputIdentifizier(){
125
+ const form = this.form;
126
+ const _this = this;
127
+
128
+ const disableIDField = () => {
129
+ const shadow = form.shadowRoot;
130
+ if (!shadow) return false;
131
+
132
+ // Trova la label "ID"
133
+ const labels = Array.from(shadow.querySelectorAll("label"))
134
+ .filter(l => l.textContent.trim() === "ID");
135
+
136
+ if (labels.length == 0) return false;
137
+ for(var i=0; i<labels.length; i++){
138
+ const label = labels[i];
139
+ if (!label) continue;
140
+ // Container della property
141
+ const container = label.closest(".property-instance");
142
+ if (!container) return false;
143
+
144
+ // Disabilita rokit-input
145
+ const rokitInput = container.querySelector("rokit-input");
146
+ if (rokitInput) {
147
+ if(rokitInput.value == ""){
148
+ rokitInput.value = _this.generateIRI(rokitInput.placeholder);
149
+ }
150
+ rokitInput.setAttribute("disabled", "true");
151
+ rokitInput.style.opacity = "0.6";
152
+ rokitInput.style.pointerEvents = "none";
153
+
154
+ var escludeSearchButton = templateIRIToExcludeFromSearch.includes(rokitInput.placeholder);
155
+
156
+ if(_this.enabled && !escludeSearchButton){
157
+ // Aggiungo bottone IMG per la ricerca
158
+ let next = label.nextElementSibling;
159
+ let imgClass = "search-identifier-icon";
160
+ if (!(next && next.tagName === "IMG"
161
+ && next.classList.contains(imgClass))) {
162
+ // creo immagine
163
+ const img = document.createElement("img");
164
+ img.src = "/frontend/images/search.png";
165
+ img.style.width = "24px"; img.style.height = "24px";
166
+ img.style["margin-left"] = "5px"; img.style["margin-right"] = "5px";
167
+ img.style.cursor = "pointer";
168
+ img.classList.add(imgClass);
169
+ img.onclick = function(){
170
+ var prefix = rokitInput.placeholder.replace("$uuid$", "").replace("$UUID$", "");
171
+ prefix = "http://diagnostica/vocabularies/quesito-diagnostico/";
172
+ _this.searchByPrefixStart(rokitInput, prefix);
173
+ }
174
+ // inserisco subito dopo la label
175
+ label.insertAdjacentElement("afterend", img);
176
+ }
177
+ }
178
+ }
179
+ // SHACL NODE superiore che contiene TUTTO il blocco ID
180
+ const shaclNode = container.closest("shacl-node");
181
+ // Rimuovi TUTTI i remove-button
182
+ if (shaclNode) {
183
+ shaclNode.querySelectorAll("div.remove-button-wrapper").forEach(el => {
184
+ el.style.display = "none";
185
+ });
186
+ shaclNode.querySelectorAll("rokit-button.remove-button").forEach(btn => {
187
+ btn.style.display = "none";
188
+ });
189
+ // Rimuovi TUTTI gli add-button all'interno del nodo ID
190
+ shaclNode.querySelectorAll("rokit-select.add-button").forEach(btn => {
191
+ btn.style.display = "none";
192
+ });
193
+ }
194
+ }
195
+ return true;
196
+ };
197
+
198
+ const observer = this.monitorChanges(form.shadowRoot, (mutation) => {
199
+ disableIDField();
200
+ });
201
+
202
+ disableIDField();
203
+ },
204
+ disableInteractions(root) {
205
+ root.style.pointerEvents = 'none';
206
+
207
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
208
+
209
+ while (walker.nextNode()) {
210
+ const el = walker.currentNode;
211
+
212
+ // Blocca click e interazioni mouse
213
+ el.style.pointerEvents = 'none';
214
+
215
+ // Blocca editing su input / textarea / select
216
+ if (el instanceof HTMLInputElement ||
217
+ el instanceof HTMLTextAreaElement ||
218
+ el instanceof HTMLSelectElement ||
219
+ el instanceof HTMLButtonElement) {
220
+ el.disabled = true;
221
+ }
222
+
223
+ // Blocca contenteditable
224
+ if (el.isContentEditable) {
225
+ el.contentEditable = "false";
226
+ }
227
+
228
+ // Ricorsione dentro Shadow DOM (se presente)
229
+ if (el.shadowRoot) {
230
+ disableInteractions(el.shadowRoot);
231
+ }
232
+ }
233
+ },
234
+ generateIRI(template, uuid){
235
+ var _uuid = uuid || crypto.randomUUID();
236
+ return template.replace("$uuid$", _uuid).replace("$UUID$", _uuid);
237
+ },
238
+ load() {
239
+
240
+ },
241
+ save() {
242
+
243
+ },
244
+ reset() {
245
+
246
+ }
247
+ }
248
+ });
249
+ app.mount(`#${appId}`);
250
+ });
@@ -0,0 +1,136 @@
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 @click="reset()" type="button" class="btn btn-secondary">{{ t('investigation.new.reset') }}</button>
89
+ </div>
90
+ {% if user.role in [0, 1] %}
91
+ <fieldset id="shacl-output" style="margin-top:10px;
92
+ border: solid 1px gray;
93
+ border-radius: 10px;
94
+ padding: 10px;">
95
+ <legend>Generated output: [{@ validForm @}]</legend>
96
+ <pre :style="outputStyle">{@ serializedForm @}</pre>
97
+ </fieldset>
98
+ {% endif %}
99
+ {% endif %}
100
+
101
+ <div style="margin-top:200px;"></div>
102
+
103
+ <div class="modal-overlay" v-cloak v-if="search.input != null">
104
+ <div class="modal-content">
105
+ <div class="row" style="line-height:40px; margin-bottom:10px;">
106
+ <span style="font-weight:bold; float:left; max-width:570px;">
107
+ Seleziona istanza:
108
+ </span>
109
+ <span style="float:right; max-width:24px;" class="close-btn" @click="searchByPrefixStart(null, null);">&times;</span>
110
+ </div>
111
+ <select size="10" v-model="search.selected">
112
+ <option v-for="item in search.results" :value="item">{@ (item.label && item.label.trim()) ? item.label : item.instance @}</option>
113
+ </select>
114
+ <div v-if="search.selected">
115
+ <span style="font-size:12px; color:gray;">{@ search.selected.instance @}</span>
116
+ </div>
117
+ <div style="margin-top:10px;">
118
+ <span @click="searchByPrefix()" v-if="!search.end" style="float:left;
119
+ cursor: pointer; color: blue;
120
+ border: solid 1px blue;
121
+ border-radius: 4px; padding: 4px;">
122
+ {{ t('investigation.new.show_more') }}...
123
+ </span>
124
+ <span v-if="search.selected" @click="confirmSelection()" style="float:right;
125
+ cursor: pointer; color: green;
126
+ border: solid 1px green;
127
+ border-radius: 4px; padding: 4px;">
128
+ {{ t('investigation.new.confirm') }}
129
+ </span>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ </div>
135
+
136
+ {% 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 %}