floodmeadows / Jira sort issues in epic by status

// ==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);
}