floodmeadows / Jira combined wip limits

// ==UserScript==
// @name         Jira combined wip limits
// @description  Show WIP limits spanning multiple columns on a Jira kanban board
// @namespace    https://openuserjs.org/users/floodmeadows
// @copyright    2025, floodmeadows (https://openuserjs.org/users/floodmeadows)
// @license      MIT
// @version      0.1
// @author       floodmeadows
// @match        https://YOUR-JIRA-DOMAIN.com/secure/RapidBoard.jspa*
// @grant        none
// ==/UserScript==

// ==OpenUserJS==
// @authors       floodmeadows
// ==/OpenUserJS==

/* jshint esversion: 6 */
'use strict';

// list the set(s) of column names that you want to group together, and a WIP limit for them.
var columnGroups = [
    {
        "columns": ["In Progress","In PR","Ready for Test","In Test"],
        "combinedWipLimit": 3
    }
];

var columnGroupHeaderStyle = "2px dashed #aaa"

function getCombinedWipValue () {

    // for each set
    for (var i=0; i<columnGroups.length; i++) {

        //   for each column in the set
         var countOfItemsInColumnGroup = 0
         var columnElements = Array()
         for (var j=0; j<columnGroups[i].columns.length; j++) {
             // document.querySelector(`h6[title="${columnGroups[i].columns[j]}"]`).parentElement.querySelector('div.ghx-qty h6').textContent
             var combinedWipLimit = columnGroups[i].combinedWipLimit
             var columnTitleElement = document.querySelector(`h6[title="${columnGroups[i].columns[j]}"]`)
             var columnElement = columnTitleElement.parentElement.parentElement.parentElement
             columnElements.push(columnElement)
             var countOfItemsInCurrentColumn = columnTitleElement.parentElement.querySelector('div.ghx-qty h6').textContent
             countOfItemsInColumnGroup += parseInt(countOfItemsInCurrentColumn)

             if(isFirstColumnInGroup(j, columnGroups[i].columns)) {
                 addLeftBorderToColumnHeading(columnElement)
             }

             addTopBorderToColumnHeading(columnElement)

             if(isLastColumnInGroup(j, columnGroups[i].columns)) {
                 addRightBorderToColumnHeading(columnElement)
                 if(countOfItemsInColumnGroup > combinedWipLimit) {
                     makeAllColumnHeadingsInGroupShowAsHavingExceededCombinedWipLimit(columnElements)
                 }
                 // insert indicator after updating styles. Harder to update the styles if the indicator is added first.
                 insertCombinedWipLimitIndicator(columnTitleElement, countOfItemsInColumnGroup, combinedWipLimit)
             }
         }
    }
}

(function() {
    // Give time for the (asynchronously-retreived) page contents load before trying to access any of the elements
    window.setTimeout(getCombinedWipValue, 1 * 1000);
})();

function isFirstColumnInGroup(column, columns) {
    return column == 0 ? true : false
}

function isLastColumnInGroup(column, columns) {
    return column == columns.length-1 ? true : false
}

function addTopBorderToColumnHeading(element) {
    element.style.borderTop = columnGroupHeaderStyle
}

function addLeftBorderToColumnHeading(element) {
    element.style.borderLeft = columnGroupHeaderStyle
}

function addRightBorderToColumnHeading(element) {
    element.style.borderRight = columnGroupHeaderStyle
}

function makeAllColumnHeadingsInGroupShowAsHavingExceededCombinedWipLimit(columnElements) {
    columnElements.forEach( function(e) {
        e.className += " ghx-busted-max"
    })
}

function insertCombinedWipLimitIndicator(columnTitleElement, countOfItemsInColumnGroup, combinedWipLimit) {
    var columnElement = columnTitleElement.parentElement.parentElement.parentElement
    var htmlToInsert = `<div class="ghx-column-header-content">
    <div class="ghx-column-header-left">
        <h6 class="ghx-column-title" aria-describedby="aui-tooltip">Combined Total ${countOfItemsInColumnGroup}</h6>
    </div>
    <div class="ghx-limits">
        <span class="ghx-constraint ghx-busted ghx-busted-max aui-lozenge aui-lozenge-subtle" title="Maximum Constraint">Max ${combinedWipLimit}</span>
    </div>
</div>`
    columnElement.innerHTML += htmlToInsert
}