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      0.1
// @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 = 150
var delay = 0
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();
    addProgressLabel();
    addProgressIndicator();
})();

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 addProgressLabel() {
    const label = " Progress: "
    const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading')
    const id = "progress-label"
    const cssClasses = ""

    addSpan(label, targetElement, null, null, id, cssClasses)
}

function addProgressIndicator() {
    const label = ""
    const targetElement = document.getElementById('greenhopper-epics-issue-web-panel_heading')
    const id = "progress-indicator"
    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) {
  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)
  parentElement.insertBefore(s, beforeElement)
}

function sortIssuesByStatus(isAscending) {
    var rowsInEpicTable = document.querySelectorAll('table#ghx-issues-in-epic-table tr')
    if(!isAscending) sortedStatusNames.reverse()
    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)
                if(debug) console.log(Date.now() + ' Delay is ' + delay)
                let issueKey = document.querySelector(`table#ghx-issues-in-epic-table tr:nth-child(${i+1}) td:nth-child(2) a`).childNodes[0].textContent
                if(debug) console.log(Date.now() + ' setting clickOnRowToShowActionsMenu() to fire in ' + delay + ' milliseconds...')
                rankRowItem(i, issueKey)
            }
        }
    })
}

function rankRowItem(tableRow, issueKey) {
    updateProgressIndicator(1)
    window.setTimeout(function() { clickOnRowToShowActionsMenu(tableRow) }, delay) // can't pass params directly when calling functions using setTimeout :(
    delay += delayOffset
    if(debug) console.log(Date.now() + ' Delay is ' + delay)
    if(debug) console.log(Date.now() + ' setting clickOnRankToTopMenuItem() to fire in ' + delay + ' milliseconds...')
    window.setTimeout(function() { clickOnMenuItem(issueKey, "rank-top-operation") }, delay)
    delay += delayOffset
}

function clickOnRowToShowActionsMenu(rowIndex) {
    if(debug) console.log(`${Date.now()} clicking on row ${rowIndex} in table`)
    if(!debug) document.getElementsByClassName('issuerow')[rowIndex].children[6].children[0].children[0].click()
}

function clickOnMenuItem(issueKey, operation) {
    if(debug) console.log(`${Date.now()} clicking on ${issueKey} to rank to top`)
    if(!debug) {
        document.querySelector(`a[data-issuekey="${issueKey}"].issueaction-greenhopper-${operation}`).click()
        updateProgressIndicator(-1)
    }
}

function updateProgressIndicator(changeValue) {
    var currentText = document.getElementById('progress-indicator').childNodes[0].textContent
    var updatedText = ""
    itemsLeftToChange += changeValue
    updatedText = itemsLeftToChange
    if(debug) console.log('Updating progress indicator to ' + updatedText)
    document.getElementById('progress-indicator').childNodes[0].textContent = updatedText
    if(itemsLeftToChange == 0) {
        updatedText = "All done!"
        document.getElementById('progress-indicator').childNodes[0].textContent = updatedText
        if(!debug) window.location.assign(currentUrl);
    }
}