|
32 | 32 | var json = document.getElementById('gtt-fiware-' + target + '-json'); |
33 | 33 | if (!rows || !json) { return; } |
34 | 34 | var showJson = json.classList.contains('hidden'); |
35 | | - if (showJson) { serialize(); } |
| 35 | + if (showJson) { |
| 36 | + serialize(); |
| 37 | + } else if (!restoreRows(target)) { |
| 38 | + // The JSON cannot be shown by the picker; leaving JSON mode would |
| 39 | + // overwrite the hand-edited JSON with stale rows on submit, so the |
| 40 | + // form stays in JSON mode. |
| 41 | + return; |
| 42 | + } |
36 | 43 | json.classList.toggle('hidden', !showJson); |
37 | 44 | rows.style.display = showJson ? 'none' : ''; |
38 | 45 | } |
39 | 46 |
|
| 47 | + // Rebuilds the picker rows from the JSON field when leaving JSON mode. |
| 48 | + // Returns false when the JSON is not representable by the picker |
| 49 | + // (unparsable, non-array, extra keys, non-string values); the caller |
| 50 | + // then keeps JSON mode, so hand-edited JSON is never lost. |
| 51 | + function restoreRows(target) { |
| 52 | + var config = target === 'entities' ? { |
| 53 | + fieldId: 'subscription_template_entities_string', |
| 54 | + containerId: 'gtt-fiware-entity-rows', |
| 55 | + prototypeId: 'gtt-fiware-entity-prototype', |
| 56 | + rowClass: 'gtt-fiware-entity-row', |
| 57 | + addId: 'gtt-fiware-entity-add', |
| 58 | + rowValues: entityRowValues |
| 59 | + } : { |
| 60 | + fieldId: 'subscription_template_attachments_string', |
| 61 | + containerId: 'gtt-fiware-attachment-rows', |
| 62 | + prototypeId: 'gtt-fiware-attachment-prototype', |
| 63 | + rowClass: 'gtt-fiware-attachment-row', |
| 64 | + addId: 'gtt-fiware-attachment-add', |
| 65 | + rowValues: attachmentRowValues |
| 66 | + }; |
| 67 | + var field = document.getElementById(config.fieldId); |
| 68 | + var container = document.getElementById(config.containerId); |
| 69 | + var prototype = document.getElementById(config.prototypeId); |
| 70 | + if (!field || !container || !prototype) { return false; } |
| 71 | + |
| 72 | + var text = field.value.trim(); |
| 73 | + var parsed = []; |
| 74 | + if (text !== '' && text !== 'null') { |
| 75 | + try { parsed = JSON.parse(text); } catch (err) { return false; } |
| 76 | + if (!Array.isArray(parsed)) { return false; } |
| 77 | + } |
| 78 | + var rows = parsed.map(config.rowValues); |
| 79 | + if (rows.some(function(values) { return values === null; })) { return false; } |
| 80 | + |
| 81 | + container.querySelectorAll('.' + config.rowClass).forEach(function(row) { row.remove(); }); |
| 82 | + rows.forEach(function(values) { |
| 83 | + var clone = prototype.content.firstElementChild.cloneNode(true); |
| 84 | + Object.keys(values).forEach(function(selector) { |
| 85 | + var el = clone.querySelector(selector); |
| 86 | + if (el) { el.value = values[selector]; } |
| 87 | + }); |
| 88 | + container.insertBefore(clone, document.getElementById(config.addId)); |
| 89 | + }); |
| 90 | + return true; |
| 91 | + } |
| 92 | + |
| 93 | + // One picker row per entity selector: a type plus at most one |
| 94 | + // id/idPattern member. |
| 95 | + function entityRowValues(obj) { |
| 96 | + if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') { return null; } |
| 97 | + var rest = Object.keys(obj).filter(function(key) { return key !== 'type'; }); |
| 98 | + // Always set all three inputs: the cloned prototype row carries a |
| 99 | + // default match value that must not leak into a restored row. |
| 100 | + if (rest.length === 0) { |
| 101 | + return { '.js-entity-type': obj.type, '.js-entity-match-kind': 'idPattern', '.js-entity-match-value': '' }; |
| 102 | + } |
| 103 | + if (rest.length > 1 || (rest[0] !== 'id' && rest[0] !== 'idPattern')) { return null; } |
| 104 | + if (typeof obj[rest[0]] !== 'string') { return null; } |
| 105 | + return { |
| 106 | + '.js-entity-type': obj.type, |
| 107 | + '.js-entity-match-kind': rest[0], |
| 108 | + '.js-entity-match-value': obj[rest[0]] |
| 109 | + }; |
| 110 | + } |
| 111 | + |
| 112 | + function attachmentRowValues(obj) { |
| 113 | + if (!obj || typeof obj !== 'object' || typeof obj.url !== 'string') { return null; } |
| 114 | + var allowed = ['url', 'filename', 'description']; |
| 115 | + var keys = Object.keys(obj); |
| 116 | + if (keys.some(function(key) { return allowed.indexOf(key) === -1; })) { return null; } |
| 117 | + if (keys.some(function(key) { return typeof obj[key] !== 'string'; })) { return null; } |
| 118 | + return { |
| 119 | + '.js-attachment-url': obj.url, |
| 120 | + '.js-attachment-filename': obj.filename || '', |
| 121 | + '.js-attachment-description': obj.description || '' |
| 122 | + }; |
| 123 | + } |
| 124 | + |
40 | 125 | function rowValues(row, selectors) { |
41 | 126 | return selectors.map(function(sel) { |
42 | 127 | var el = row.querySelector(sel); |
|
98 | 183 | var georel = document.getElementById('subscription_template_expression_georel'); |
99 | 184 | var geometry = document.getElementById('subscription_template_expression_geometry'); |
100 | 185 | var coords = document.getElementById('subscription_template_expression_coords'); |
| 186 | + if (!georel || !geometry || !coords) { return; } |
101 | 187 | if (mode.value === 'anywhere') { |
102 | 188 | georel.value = ''; geometry.value = ''; coords.value = ''; |
103 | 189 | } else if (mode.value === 'boundary') { |
104 | | - var geom = JSON.parse(mode.dataset.geom).geometry.coordinates[0] |
| 190 | + var ring; |
| 191 | + try { |
| 192 | + ring = JSON.parse(mode.dataset.geom).geometry.coordinates[0]; |
| 193 | + } catch (err) { |
| 194 | + ring = null; |
| 195 | + } |
| 196 | + // data-geom is server-rendered; if it is missing, malformed or not |
| 197 | + // a ring of number pairs, leave the stored triple untouched rather |
| 198 | + // than throwing from inside the submit listener. |
| 199 | + var isPair = function(c) { |
| 200 | + return Array.isArray(c) && typeof c[0] === 'number' && typeof c[1] === 'number'; |
| 201 | + }; |
| 202 | + if (!Array.isArray(ring) || ring.length === 0 || !ring.every(isPair)) { return; } |
| 203 | + var geom = ring |
105 | 204 | .map(function(c) { return [Number(c[1].toFixed(5)), Number(c[0].toFixed(5))]; }) |
106 | 205 | .join(';'); |
107 | 206 | georel.value = 'coveredBy'; geometry.value = 'polygon'; coords.value = geom; |
|
250 | 349 | }); |
251 | 350 | } |
252 | 351 |
|
| 352 | + // Guards against out-of-order responses: rapid tracker/member changes |
| 353 | + // fire concurrent fetches, and without the token the last response to |
| 354 | + // ARRIVE would rebuild the select, which may belong to the first |
| 355 | + // request SENT (a stale tracker's statuses shown for the current one). |
| 356 | + var statusRequestToken = 0; |
| 357 | + |
253 | 358 | function refreshIssueStatuses() { |
254 | 359 | if (!issueStatusSelect || !issueStatusSelect.dataset.statusesUrl) { return; } |
255 | 360 | var url = issueStatusSelect.dataset.statusesUrl + |
256 | 361 | '?tracker_id=' + encodeURIComponent(trackerSelect ? trackerSelect.value : '') + |
257 | 362 | '&member_id=' + encodeURIComponent(memberSelect ? memberSelect.value : ''); |
| 363 | + var token = ++statusRequestToken; |
258 | 364 | fetch(url, { headers: { 'Accept': 'application/json' } }) |
259 | 365 | .then(function(response) { return response.json(); }) |
260 | 366 | .then(function(statuses) { |
| 367 | + if (token !== statusRequestToken) { return; } |
261 | 368 | var current = issueStatusSelect.value; |
262 | 369 | var currentText = issueStatusSelect.selectedOptions[0] ? |
263 | 370 | issueStatusSelect.selectedOptions[0].textContent : ''; |
|
312 | 419 | } |
313 | 420 |
|
314 | 421 | var previewButton = document.getElementById('gtt-fiware-preview-button'); |
| 422 | + // Same out-of-order guard as the status refetch: a double-click fires |
| 423 | + // two requests, and only the latest one may write the result box. |
| 424 | + var previewRequestToken = 0; |
315 | 425 | if (previewButton) { |
316 | 426 | previewButton.addEventListener('click', function(e) { |
317 | 427 | e.preventDefault(); |
318 | 428 | var out = document.getElementById('gtt-fiware-preview-result'); |
| 429 | + if (!out) { return; } |
| 430 | + var token = ++previewRequestToken; |
319 | 431 | out.style.display = ''; |
320 | 432 | out.textContent = previewButton.dataset.loading; |
321 | 433 |
|
|
342 | 454 | }).then(function(response) { |
343 | 455 | return response.json().then(function(json) { return { ok: response.ok, json: json }; }); |
344 | 456 | }).then(function(result) { |
| 457 | + if (token !== previewRequestToken) { return; } |
345 | 458 | if (!result.ok) { |
346 | 459 | out.textContent = result.json.error || previewButton.dataset.error; |
347 | 460 | return; |
|
359 | 472 | (result.json.has_geometry ? ', ' + previewButton.dataset.geometryLabel : ''); |
360 | 473 | out.appendChild(em); |
361 | 474 | }).catch(function() { |
| 475 | + if (token !== previewRequestToken) { return; } |
362 | 476 | out.textContent = previewButton.dataset.error; |
363 | 477 | }); |
364 | 478 | }); |
|
0 commit comments