/* * OPTIONS DEFINED FROM CONFIGURATION FILE */ if (typeof window._CONF_FORCE_HTTPS === 'undefined') var _CONF_FORCE_HTTPS = false; if (typeof window._CONF_DISPLAY_EDIT_ICON === 'undefined') var _CONF_DISPLAY_EDIT_ICON = "single"; if (typeof window._CONF_LISTENER_TYPE_VALUE === 'undefined') var _CONF_LISTENER_TYPE_VALUE = "click"; if (typeof window._CONF_LISTENER_TYPE_ICON === 'undefined') var _CONF_LISTENER_TYPE_ICON = "none"; if (typeof window._CONF_LISTENER_TARGET === 'undefined') var _CONF_LISTENER_TARGET = "value"; if (typeof window._CONF_EXCLUDED_FIELD_ID === 'undefined') var _CONF_EXCLUDED_FIELD_ID = []; if (typeof window._CONF_CHECK_ISSUE_UPDATE_CONFLICT === 'undefined') var _CONF_CHECK_ISSUE_UPDATE_CONFLICT = false; _CONF_LISTENER_TARGET = _CONF_LISTENER_TARGET === "all" ? "" : " ." + _CONF_LISTENER_TARGET; /* * SVG ICONS * Source : https://www.iconfinder.com/iconsets/glyphs */ const SVG_EDIT = ''; const SVG_VALID = ''; const SVG_CANCEL = ''; /* * Allow inclusion from other page * See https://github.com/Ilogeek/redmine_issue_dynamic_edit/commit/26684a2dd9b12dcc7377afd79e9fe5c142d26ebd for more info */ const cleanURL = function(url){ let u = new URL(url); return `${u.protocol}//${u.host}${u.pathname}`; } let LOCATION_HREF = typeof custom_location_href !== 'undefined' ? cleanURL(custom_location_href) : cleanURL(window.location.href); if (_CONF_FORCE_HTTPS) { LOCATION_HREF = LOCATION_HREF.replace(/^http:\/\//i, 'https://'); } /* Check if admin want to display all editable fields when hovering the whole details block * or if user has to hover every element to discover if (s)he can edit it */ if (_CONF_DISPLAY_EDIT_ICON === "block"){ document.querySelectorAll('body.controller-issues.action-show .issue.details').forEach((elt) => elt.classList.add('showPencils')); } const updateCSRFToken = function(token){ document.querySelectorAll('input[name="authenticity_token"]').forEach((elt) => elt.value = token); document.querySelector('meta[name="csrf-token"]').setAttribute("content", token); } const setCSRFTokenInput = function(token){ document.querySelectorAll('form[method="post"]').forEach((elt) => { if(!elt.querySelectorAll('input[name="authenticity_token"]').length){ const input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", "authenticity_token"); input.value = token; elt.insertBefore(input, null); } }); } /* Generate edit block */ const getEditFormHTML = function(attribute){ let formElement = document.querySelector('#issue_' + attribute + "_id"); formElement = formElement ? formElement : document.querySelector('#issue_' + attribute); formElement = formElement ? formElement : document.querySelector('#' + attribute); // Checkbox specific case let is_checkboxes = false; let is_file = false; let is_list = false; let CF_ID = false; if(!formElement && attribute.startsWith("custom_field_values_")){ CF_ID = attribute.split("custom_field_values_")[1]; /* Is it a checkbox block ? */ formElement = document.querySelector('#issue_custom_field_values_' + CF_ID); if(formElement){ formElement = formElement.closest('.check_box_group'); is_checkboxes = CF_ID; } else { /* Is it a file block ? */ formElement = document.querySelector('#issue_custom_field_values_' + CF_ID + '_blank'); if(formElement){ formElement = formElement.closest('p'); is_file = CF_ID; } else { /* Is it a checkbox/radio group ? */ formElement = document.querySelector('#issue-form .cf_' + CF_ID + '.check_box_group'); is_list = CF_ID; } } } if(formElement){ const clone = formElement.cloneNode(true); if(is_file) { clone.removeChild(clone.querySelector('label')); } if(clone.matches('select') && !clone.hasAttribute('multiple')) { clone.addEventListener('change', function(e){ sendData([{"name" : clone.getAttribute('name'), "value" : clone.value}]); }); } if(is_checkboxes || is_file || is_list) { clone.setAttribute('id', "issue_custom_field_values_" + CF_ID + "_dynamic"); } else { clone.setAttribute('id', formElement.getAttribute('id') + "_dynamic"); } const wrapper = document.createElement('div'); wrapper.classList.add('dynamicEditField'); wrapper.insertBefore(clone, null); if(!clone.matches('select') || clone.hasAttribute('multiple')) { let btn_valid = document.createElement('button'); btn_valid.classList.add('action', 'valid'); btn_valid.innerHTML = SVG_VALID; wrapper.insertBefore(btn_valid, null); } const btn_refuse = document.createElement('button'); btn_refuse.classList.add('action', 'refuse'); btn_refuse.innerHTML = SVG_CANCEL; wrapper.insertBefore(btn_refuse, null); return wrapper; } return null; } /* Loop over all form attribute and clone them into details part */ const cloneEditForm = function(){ const btn_refresh = document.createElement('button'); btn_refresh.classList.add('refreshData'); btn_refresh.innerHTML = "⟳"; document.querySelector('.issue.details div.subject').insertBefore(btn_refresh, null); document.querySelectorAll('div.issue.details .attribute').forEach(function(elt){ const classList = elt.classList.value.split(/\s+/); let attributes = classList.filter(function(elem) { return elem != "attribute"; }); // Specific case : all "-" are replaced by "_" into form id attributes = attributes.map((attr) => attr.replaceAll('-', '_')); let custom_field = false; attributes.forEach(function(part, index, arr) { if(arr[index] === "progress") arr[index] = "done_ratio"; if(arr[index].startsWith('cf_')) { arr[index] = arr[index].replace('cf', 'custom_field_values'); custom_field = arr[index]; } }); attributes = attributes.join(" "); let selected_elt = custom_field ? custom_field : attributes; if(attributes && !_CONF_EXCLUDED_FIELD_ID.includes(selected_elt)){ let dynamicEditField = getEditFormHTML(selected_elt); if(dynamicEditField){ let btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; elt.querySelector('.value').insertBefore(btn_edit, null); elt.querySelector('.value').insertBefore(dynamicEditField, null); } } }); // Specific Case : Description field if(!_CONF_EXCLUDED_FIELD_ID.includes("description") && document.querySelectorAll('div.issue.details .description').length){ const btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; document.querySelector('div.issue.details .description > p strong').insertAdjacentElement("afterend", btn_edit); const formDescription = getEditFormHTML("description"); formDescription.querySelector("#issue_description_dynamic").removeAttribute('data-tribute'); document.querySelector('div.issue.details .description').insertBefore(formDescription, null); if ( typeof(CKEDITOR) === "object" && typeof(CKEDITOR.instances['issue_description'] !== "undefined") && typeof(CKEDITOR.instances['issue_description'].getData) === typeof(Function) ) { const cfg = CKEDITOR.instances['issue_description'].config; cfg.height = 100; CKEDITOR.replace("issue_description_dynamic", cfg) }else if (typeof(jsToolBar) === typeof(Function)) { const DynamicDescriptionToolbar = new jsToolBar(document.querySelector('#issue_description_dynamic')); DynamicDescriptionToolbar.setHelpLink('/help/en/wiki_syntax_common_mark.html'); DynamicDescriptionToolbar.setPreviewUrl('/issues/preview?issue_id=' + _ISSUE_ID + '&project_id=' + _PROJECT_ID); DynamicDescriptionToolbar.draw(); } } // Specific Case : Title field if(!_CONF_EXCLUDED_FIELD_ID.includes("subject")){ const btn_edit = document.createElement('span'); btn_edit.classList.add('iconEdit'); btn_edit.innerHTML = SVG_EDIT; document.querySelector('div.issue.details div.subject h3').insertBefore(btn_edit, null); const formTitle = getEditFormHTML("issue_subject"); document.querySelector('div.issue.details div.subject').insertBefore(formTitle, null); } } /* Perform action on .value (display edit form) */ document.querySelector('body').addEventListener(_CONF_LISTENER_TYPE_VALUE, function(e){ let container = e.target.closest('.attribute' + _CONF_LISTENER_TARGET) || e.target.closest('.description') || e.target.closest('.subject'); if(_CONF_LISTENER_TARGET && e.target.closest('.attribute' + _CONF_LISTENER_TARGET)){ container = e.target.closest('.attribute'); } if(container) { if(e.target.closest('.dynamicEditField')) return; /* We're already into a dynamic field, ignore */ document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); if(!e.target.closest('a') && !e.target.closest('button')){ let selector = container.querySelector('.dynamicEditField'); if(selector) selector.classList.add('open') } } }); /* Perform action on .iconEdit (display edit form) */ document.querySelector('body').addEventListener(_CONF_LISTENER_TYPE_ICON, function(e){ let is_description = e.target.matches('div.issue.details div.description > p') || e.target.closest('div.issue.details div.description > p'); let is_subject = e.target.matches('div.issue.details div.subject') || e.target.closest('div.issue.details div.subject'); if(e.target.matches('.iconEdit') || e.target.closest('.iconEdit')){ document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); let selector = e.target.closest('.value'); if(is_description) selector = e.target.closest('.description'); if(is_subject) selector = e.target.closest('.subject'); if(selector.querySelector('.dynamicEditField')) selector.querySelector('.dynamicEditField').classList.add('open'); } }); /* Perform data update when clicking on valid button from edit form */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.dynamicEditField .action.valid') || e.target.closest('.dynamicEditField .action.valid')){ e.preventDefault(); let inputs = e.target.closest('.dynamicEditField').querySelectorAll('*[name]'); let formData = []; let existingIndex = []; inputs.forEach(elt => { let not_multiple = !elt.matches('input[type="radio"]') && !elt.matches('input[type="checkbox"]'); if(elt.matches('input[type="radio"]:checked') || elt.matches('input[type="checkbox"]:checked') || not_multiple){ if(!existingIndex.includes(elt.getAttribute('name'))){ existingIndex.push(elt.getAttribute('name')); formData.push({"name" : elt.getAttribute('name'), "value" : elt.value}) } } }); sendData(formData); e.target.closest('.dynamicEditField').classList.remove('open'); } }); /* Hide edit form when clicking on cancel button */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.dynamicEditField .action.refuse') || e.target.closest('.dynamicEditField .action.refuse')){ e.preventDefault(); e.target.closest('.dynamicEditField').classList.remove('open'); } }); /* Update whole .details block + history + form with global refresh button */ document.querySelector('body').addEventListener('click', function(e){ if(e.target.matches('.refreshData') || e.target.closest('.refreshData')){ e.preventDefault(); sendData(); } }); /* Listen on esc key press to close opened dialog box */ document.onkeydown = function(evt) { evt = evt || window.event; let isEscape = false; if ("key" in evt) { isEscape = (evt.key === "Escape" || evt.key === "Esc"); } else { isEscape = (evt.keyCode === 27); } if (isEscape) { document.querySelectorAll('.dynamicEditField').forEach(function(elt){ elt.classList.remove('open'); }); } }; const checkVersion = function(callback){ fetch(LOCATION_HREF, { method: 'GET', crossDomain: true, }).then(res => res.text()).then(data => { const parser = new DOMParser(); const doc = parser.parseFromString(data, 'text/html'); const distant_version = doc.querySelector('#issue_lock_version').value; const current_version = document.querySelector('#issue_lock_version').value; if(distant_version > current_version){ if(!document.querySelectorAll('#content .conflict').length){ let msg = document.createElement('div'); msg.classList.add('conflict'); msg.innerHTML = `${_TXT_CONFLICT_TITLE}

${_TXT_CONFLICT_LINK} ${_TXT_CONFLICT_TXT}

` document.querySelector('#content').insertBefore(msg, document.querySelector('#content').firstChild); } } if(callback) callback(distant_version); return distant_version; }).catch(err => { console.warn('Issue while trying to get version (avoiding conflict)'); console.log(err); }); } let checkVersionInterval = false; let setCheckVersionInterval = function(activate){ if(!_CONF_CHECK_ISSUE_UPDATE_CONFLICT) return false; if(activate && !checkVersionInterval){ checkVersionInterval = window.setInterval(function(){ if(document.visibilityState === "visible") checkVersion(); }, 5000); } else { clearInterval(checkVersionInterval); checkVersionInterval = false; } } setCheckVersionInterval(true); /* Global function to perform AJAX call */ let sendData = function(serialized_data){ let updateIssue = function(serialized_data){ setCheckVersionInterval(false); const token = document.querySelector("meta[name=csrf-token]").getAttribute('content'); let params = serialized_data || []; params.push({name: '_method', value: "patch"}); params.push({name: 'authenticity_token', value: token}); let request = new XMLHttpRequest(); request.open('POST', LOCATION_HREF, true); let formData = new FormData(); params.forEach(data => formData.append(data.name, data.value)); let callError = function(msg){ setCheckVersionInterval(true); document.querySelector('#ajax-indicator').style.display = 'none'; /* error and no update, info logged into console */ console.groupCollapsed('%c -------- Error while updating the issue attribute dynamically -------- ', 'background: #ff0000; color: white; font-weight:900'); console.log("POST " + LOCATION_HREF); console.log(msg); console.groupEnd(); } request.onreadystatechange = function() { if (this.readyState == 4) { if(this.status == 200) { const parser = new DOMParser(); const doc = parser.parseFromString(this.responseText, 'text/html'); let error = doc.querySelector("#errorExplanation"); if(error){ if (!document.querySelector("#errorExplanation")) { let err_div = document.createElement('div'); err_div.setAttribute("id", "errorExplanation"); err_div.innerHTML = error.innerHTML; document.querySelector('.issue.details').insertAdjacentElement("beforebegin", err_div); location.href = "#"; location.href = "#errorExplanation"; } else { document.querySelector("#errorExplanation").innerHTML = error.innerHTML; } doc = fetch(LOCATION_HREF, { method: 'GET', crossDomain: true, }).then(res => res.text()).then(data => { const parser = new DOMParser(); return parser.parseFromString(data, 'text/html'); }); } else { if(document.querySelector("#errorExplanation")) document.querySelector("#errorExplanation").remove(); } if(document.querySelector('form#issue-form')) document.querySelector('form#issue-form').innerHTML = doc.querySelector('form#issue-form').innerHTML; if(document.querySelector('#all_attributes')) document.querySelector('#all_attributes').innerHTML = doc.querySelector('#all_attributes').innerHTML; if(document.querySelector('div.issue.details')) document.querySelector('div.issue.details').innerHTML = doc.querySelector('div.issue.details').innerHTML; if(document.querySelector('#issue_lock_version')) document.querySelector('#issue_lock_version').value = doc.querySelector("#issue_lock_version").value; if(document.querySelector('#tab-content-history')) { if(_COMMENTS_IN_REVERSE_ORDER) { document.querySelector('#tab-content-history').insertAdjacentElement('afterbegin', doc.querySelector('#history .journal.has-details:first-child')); } else { document.querySelector('#tab-content-history').appendChild(doc.querySelector('#history .journal.has-details:last-child')); } } cloneEditForm(); //set datepicker fallback for input type date if ( document.querySelector('input[type=date]') && $('body').find('input[type=date]').datepickerFallback instanceof Function && typeof datepickerOptions !== 'undefined' ) { $('body').find('input[type=date]').datepickerFallback(datepickerOptions); } setCSRFTokenInput(doc.querySelector('input[name="authenticity_token"]').value); updateCSRFToken(doc.querySelector('input[name="authenticity_token"]').value); /* Once we've updated our issue, we have to reset the loadedDate to now to be up to date with the check version */ loadedDate = new Date(); setCheckVersionInterval(true); } else { callError(this.status); } } }; request.send(formData); } if(_CONF_CHECK_ISSUE_UPDATE_CONFLICT){ checkVersion(function(distant_version){ if(distant_version == document.querySelector('#issue_lock_version').value){ updateIssue(serialized_data); } else { } }); } else { updateIssue(serialized_data); } } // Init plugin cloneEditForm(); setCSRFTokenInput(document.querySelector('meta[name="csrf-token"]').getAttribute("content"));