NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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
}