NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Jira sort issues in epic by status // @namespace https://openuserjs.org/users/floodmeadows // @description Adds buttons to sort issues by status. // @copyright 2023, floodmeadows (https://openuserjs.org/users/floodmeadows) // @license MIT // @version 1.0 // @include https://jira.*.uk/browse/* // @updateURL https://openuserjs.org/meta/floodmeadows/Jira_sort_issues_in_epic_by_status.meta.js // @downloadURL https://openuserjs.org/install/floodmeadows/Jira_sort_issues_in_epic_by_status.user.js // @grant none // ==/UserScript== /* jshint esversion: 6 */ //--- Customise this to your Jira project -----// const debug = false; const delayOffset = 1000 var delayDuration = 100 var itemsLeftToChange = 0 const sortedStatusNames = new Array( "Backlog", "To be Prioritised", "To Do", "Ready for Analysis", "In Analysis", "Ready for Refinement", "Ready for Dev", "In Progress", "In Dev", "In PR", "Ready for Test", "In Test", "In Review", "Ready for Release", "Done", "Won't Fix" ); //---------------------------------------------// const currentUrl = new URL(document.URL); const jiraBaseUrl = currentUrl.protocol + '//' + currentUrl.host; const issueKey = document.getElementById("key-val").childNodes[0].nodeValue; const issueName = document.getElementById("summary-val").childNodes[0].nodeValue; const issueUrl = `${jiraBaseUrl}/browse/${issueKey}`; (function() { 'use strict'; addButtonSortByStatusAscending(); addButtonSortByStatusDecending(); addProgressIndicatorLabel(); addProgressIndicatorCurrentValue(); addProgressIndicatorTotalValue(); })(); function addButtonSortByStatusAscending() { const buttonLabel = "Sort by status ('Done' at top)" const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading') const eventHandler = function() { sortIssuesByStatus(true) } const buttonCssClasses = "aui-button toolbar-trigger issueaction-workflow-transition" addButton(buttonLabel, targetElement, null, eventHandler, buttonCssClasses) } function addButtonSortByStatusDecending() { const buttonLabel = "Sort by status ('Done' at bottom)" const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading') const eventHandler = function() { sortIssuesByStatus(false) } const buttonCssClasses = "aui-button toolbar-trigger issueaction-workflow-transition" addButton(buttonLabel, targetElement, null, eventHandler, buttonCssClasses) } function addProgressIndicatorLabel() { const label = " Progress: " const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading') const id = "progress-indicator-label" const cssClasses = "" const style = "display:none;" addSpan(label, targetElement, null, null, id, cssClasses, style) } function addProgressIndicatorCurrentValue() { const label = "" const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading') const id = "progress-indicator-current-value" const cssClasses = "" addSpan(label, targetElement, null, null, id, cssClasses, '') } function addProgressIndicatorTotalValue() { const label = "" const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading') const id = "progress-indicator-total-value" const cssClasses = "" addSpan(label, targetElement, null, null, id, cssClasses, '') } function addButton(text, parentElement, beforeElement, clickEventFunctionName, cssClass) { if(debug) console.log("addButton called") const btn = document.createElement("a") const textNode = document.createTextNode(text) btn.appendChild(textNode) btn.setAttribute("href","#") btn.addEventListener("click", clickEventFunctionName) btn.setAttribute("class", cssClass) parentElement.insertBefore(btn, beforeElement) } function addSpan(text, parentElement, beforeElement, clickEventFunctionName, id, cssClasses, style) { if(debug) console.log("addSpan called") const s = document.createElement("span") const textNode = document.createTextNode(text) s.appendChild(textNode) s.addEventListener("click", clickEventFunctionName) s.setAttribute("id", id) s.setAttribute("class", cssClasses) s.setAttribute("style", style) parentElement.insertBefore(s, beforeElement) } function sortIssuesByStatus(isAscending) { var rowsInEpicTable = document.querySelectorAll('table#ghx-issues-in-epic-table tr') if(!isAscending) sortedStatusNames.reverse() var tableRowNumbersInRankOrder = getTableRowNumbersInRankOrder(rowsInEpicTable) rankTableRows(tableRowNumbersInRankOrder) } async function rankTableRows(tableRowNumbersInRankOrder) { const totalRows = tableRowNumbersInRankOrder.length showProgressIndicator(totalRows) for (const rowIndex of tableRowNumbersInRankOrder) { if (debug) console.log('Ranking ' + rowIndex + ' to top'); updateProgressIndicator(1); await clickOnRowAndRank(rowIndex); // Wait for the row to be clicked and ranked } reloadPage() } async function clickOnRowAndRank(rowIndex) { if(debug) console.log(Date.now() + ' Ranking row ' + rowIndex) if(debug) console.log(Date.now() + ' About to click on row to show actions menu...') await clickOnRowToShowActionsMenu(rowIndex); // Wait for the menu to load and be clicked if(debug) console.log(Date.now() + ' Calling delay to give actions menu time to load...') await delay(); // Wait for a short delay to ensure menu has loaded if(debug) console.log(Date.now() + ' Delay completed. Actions menu should be showing now.') if(debug) console.log(Date.now() + ' About to click on Rank To Top...') await clickOnMenuItem(rowIndex, "rank-top-operation"); // Then rank the item if(debug) console.log(Date.now() + ' Finished ranking row ' + rowIndex) await delay(); // Wait for a short delay to ensure menu has loaded } function delay() { return new Promise(resolve => setTimeout(resolve, delayDuration)); } function getTableRowNumbersInRankOrder(rowsInEpicTable) { var tableRowNumbers = new Array() sortedStatusNames.forEach(function(statusName) { if(debug) console.log('Looking for ' + statusName) for(let i=0; i< rowsInEpicTable.length; i++) { var statusValueForRow = document.querySelector(`table#ghx-issues-in-epic-table tr:nth-child(${i+1}) td:nth-child(5) span`).childNodes[0].textContent if(statusValueForRow == statusName) { if(debug) console.log(Date.now() + ' Found ' + statusName) tableRowNumbers.push(i) } } }) return tableRowNumbers } function clickOnRowToShowActionsMenu(rowIndex) { if(debug) console.log(`${Date.now()} clicking on row ${rowIndex} to show actions menu`) document.getElementsByClassName('issuerow')[rowIndex].children[6].children[0].children[0].click() return delay(); // Return a promise that resolves after a short delay } function clickOnMenuItem(row, operation) { if(debug) console.log(`${Date.now()} clicking on row ${row} to rank to top`) var l = document.querySelectorAll(`a.issueaction-greenhopper-${operation}`).length document.querySelectorAll(`a.issueaction-greenhopper-${operation}`)[l-1].click() } function showProgressIndicator(totalRows) { var label = document.getElementById('progress-indicator-label') label.attributes.style.value = "display:inline;" label.childNodes[0].textContent = " Progress: " var totalValue = document.getElementById('progress-indicator-total-value') totalValue.childNodes[0].textContent = '/'+totalRows } function updateProgressIndicator(changeValueBy) { var currentValue = document.getElementById('progress-indicator-current-value').childNodes[0].textContent if (currentValue == '') { currentValue = 0 } else { currentValue = parseInt(currentValue) } var newValue = currentValue + changeValueBy if(debug) console.log('Updating progress indicator to ' + newValue) document.getElementById('progress-indicator-current-value').childNodes[0].textContent = newValue } function reloadPage() { if(!debug) window.location.assign(currentUrl); }