Jira Task Priority Colorizer

Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns

  1. // ==UserScript==
  2. // @name Jira Task Priority Colorizer
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.3
  5. // @description Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns
  6. // @author erolatex
  7. // @include https://*/secure/RapidBoard.jspa*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Columns to exclude from emoji update
  16. const excludedColumns = ['ready for development', 'deployed'];
  17.  
  18. // CSS styles to change card colors and position the emoji
  19. const styleContent = `
  20. .ghx-issue[data-priority*="P0"] {
  21. background-color: #FFADB0 !important;
  22. }
  23. .ghx-issue[data-priority*="P1"] {
  24. background-color: #FF8488 !important;
  25. }
  26. .ghx-issue[data-priority*="P2"] {
  27. background-color: #FFD3C6 !important;
  28. }
  29. .ghx-issue[data-priority*="P3"],
  30. .ghx-issue[data-priority*="P4"] {
  31. background-color: #FFF !important;
  32. }
  33. .stale-emoji {
  34. position: absolute;
  35. bottom: 5px;
  36. right: 5px;
  37. font-size: 14px;
  38. display: flex;
  39. align-items: center;
  40. background-color: #d2b48c;
  41. border-radius: 3px;
  42. padding: 2px 4px;
  43. font-weight: bold;
  44. }
  45. .stale-emoji span {
  46. margin-left: 5px;
  47. font-size: 12px;
  48. color: #000;
  49. }
  50. .ghx-issue {
  51. position: relative;
  52. }
  53. .column-badge.bad-count {
  54. margin-left: 5px;
  55. background-color: #d2b48c;
  56. border-radius: 3px;
  57. padding: 0 4px;
  58. font-size: 11px;
  59. color: #333;
  60. font-weight: normal;
  61. text-align: center;
  62. display: inline-block;
  63. vertical-align: middle;
  64. line-height: 20px;
  65. }
  66. .ghx-limits {
  67. display: flex;
  68. align-items: center;
  69. gap: 5px;
  70. }
  71. `;
  72.  
  73. // Inject CSS styles into the page
  74. const styleElement = document.createElement('style');
  75. styleElement.type = 'text/css';
  76. styleElement.appendChild(document.createTextNode(styleContent));
  77. document.head.appendChild(styleElement);
  78.  
  79. /**
  80. * Calculates the number of working days between two dates.
  81. * Excludes Saturdays and Sundays.
  82. * @param {Date} startDate - The start date.
  83. * @param {Date} endDate - The end date.
  84. * @returns {number} - The number of working days.
  85. */
  86. function calculateWorkingDays(startDate, endDate) {
  87. let count = 0;
  88. let currentDate = new Date(startDate);
  89. // Set time to midnight to avoid timezone issues
  90. currentDate.setHours(0, 0, 0, 0);
  91. endDate = new Date(endDate);
  92. endDate.setHours(0, 0, 0, 0);
  93.  
  94. while (currentDate <= endDate) {
  95. const dayOfWeek = currentDate.getDay();
  96. if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
  97. count++;
  98. }
  99. currentDate.setDate(currentDate.getDate() + 1);
  100. }
  101. return count;
  102. }
  103.  
  104. /**
  105. * Calculates the number of working days based on the total days in the column.
  106. * Assumes days are counted backward from the current date.
  107. * @param {number} daysInColumn - Total number of days in the column.
  108. * @returns {number} - The number of working days.
  109. */
  110. function calculateWorkingDaysFromDaysInColumn(daysInColumn) {
  111. let workingDays = 0;
  112. let currentDate = new Date();
  113. // Set time to midnight to avoid timezone issues
  114. currentDate.setHours(0, 0, 0, 0);
  115.  
  116. for (let i = 0; i < daysInColumn; i++) {
  117. const dayOfWeek = currentDate.getDay();
  118. if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
  119. workingDays++;
  120. }
  121. // Move to the previous day
  122. currentDate.setDate(currentDate.getDate() - 1);
  123. }
  124. return workingDays;
  125. }
  126.  
  127. /**
  128. * Updates the priorities of the cards and adds/removes the emoji based on working days.
  129. */
  130. function updateCardPriorities() {
  131. // Disconnect the observer to prevent it from reacting to our changes
  132. observer.disconnect();
  133.  
  134. let cards = document.querySelectorAll('.ghx-issue');
  135. let poopCountPerColumn = {};
  136.  
  137. cards.forEach(card => {
  138. // Update priority attribute
  139. let priorityElement = card.querySelector('.ghx-priority');
  140. if (priorityElement) {
  141. let priority = priorityElement.getAttribute('title') || priorityElement.getAttribute('aria-label') || priorityElement.innerText || priorityElement.textContent;
  142. if (priority) {
  143. card.setAttribute('data-priority', priority);
  144. }
  145. }
  146.  
  147. // Initialize working days count
  148. let workingDays = 0;
  149.  
  150. // Attempt to get the start date from a data attribute (e.g., data-start-date)
  151. let startDateAttr = card.getAttribute('data-start-date'); // Example: '2024-04-25'
  152. if (startDateAttr) {
  153. let startDate = new Date(startDateAttr);
  154. let today = new Date();
  155. workingDays = calculateWorkingDays(startDate, today);
  156. } else {
  157. // If start date is not available, use daysInColumn
  158. let daysElement = card.querySelector('.ghx-days');
  159. if (daysElement) {
  160. let title = daysElement.getAttribute('title');
  161. if (title) {
  162. let daysMatch = title.match(/(\d+)\s+days?/);
  163. if (daysMatch && daysMatch[1]) {
  164. let daysInColumn = parseInt(daysMatch[1], 10);
  165. workingDays = calculateWorkingDaysFromDaysInColumn(daysInColumn);
  166. }
  167. }
  168. }
  169. }
  170.  
  171. // Check and update the emoji 💩
  172. let columnElement = card.closest('.ghx-column');
  173. if (workingDays > 3 && columnElement) {
  174. let columnTitle = columnElement.textContent.trim().toLowerCase();
  175. if (!excludedColumns.some(col => columnTitle.includes(col))) {
  176. let existingEmoji = card.querySelector('.stale-emoji');
  177. if (!existingEmoji) {
  178. let emojiContainer = document.createElement('div');
  179. emojiContainer.className = 'stale-emoji';
  180.  
  181. let emojiElement = document.createElement('span');
  182. emojiElement.textContent = '💩';
  183.  
  184. let daysText = document.createElement('span');
  185. daysText.textContent = `${workingDays} d`;
  186.  
  187. emojiContainer.appendChild(emojiElement);
  188. emojiContainer.appendChild(daysText);
  189.  
  190. card.appendChild(emojiContainer);
  191. } else {
  192. let daysText = existingEmoji.querySelector('span:last-child');
  193. daysText.textContent = `${workingDays} d`;
  194. }
  195.  
  196. // Count poop emoji per column
  197. let columnId = columnElement.getAttribute('data-column-id') || columnElement.getAttribute('data-id');
  198. if (columnId) {
  199. if (!poopCountPerColumn[columnId]) {
  200. poopCountPerColumn[columnId] = 0;
  201. }
  202. poopCountPerColumn[columnId]++;
  203. }
  204. }
  205. } else {
  206. let existingEmoji = card.querySelector('.stale-emoji');
  207. if (existingEmoji) {
  208. existingEmoji.remove();
  209. }
  210. }
  211. });
  212.  
  213. // Update poop count badges for each column
  214. Object.keys(poopCountPerColumn).forEach(columnId => {
  215. let columnHeader = document.querySelector(`.ghx-column[data-id="${columnId}"]`);
  216. if (columnHeader) {
  217. let limitsContainer = columnHeader.querySelector('.ghx-column-header .ghx-limits');
  218. let existingBadge = columnHeader.querySelector('.column-badge.bad-count');
  219. if (!existingBadge) {
  220. // Change from 'div' to 'span' and adjust classes
  221. existingBadge = document.createElement('span');
  222. existingBadge.className = 'ghx-constraint aui-lozenge aui-lozenge-subtle column-badge bad-count';
  223. }
  224. existingBadge.textContent = `💩 ${poopCountPerColumn[columnId]}`;
  225. existingBadge.style.display = 'inline-block';
  226.  
  227. if (limitsContainer) {
  228. let maxBadge = limitsContainer.querySelector('.ghx-constraint.ghx-busted-max');
  229. if (maxBadge) {
  230. limitsContainer.insertBefore(existingBadge, maxBadge);
  231. } else {
  232. limitsContainer.appendChild(existingBadge);
  233. }
  234. } else {
  235. // If limitsContainer doesn't exist, create it
  236. limitsContainer = document.createElement('div');
  237. limitsContainer.className = 'ghx-limits';
  238. limitsContainer.appendChild(existingBadge);
  239. let headerContent = columnHeader.querySelector('.ghx-column-header-content');
  240. if (headerContent) {
  241. headerContent.appendChild(limitsContainer);
  242. }
  243. }
  244. }
  245. });
  246.  
  247. // Reconnect the observer after making changes
  248. observer.observe(document.body, { childList: true, subtree: true });
  249. }
  250.  
  251. // MutationObserver to watch for changes in the DOM and update priorities accordingly
  252. const observer = new MutationObserver(() => {
  253. updateCardPriorities();
  254. });
  255.  
  256. // Start observing the body
  257. observer.observe(document.body, { childList: true, subtree: true });
  258.  
  259. // Update priorities when the page loads
  260. window.addEventListener('load', function() {
  261. updateCardPriorities();
  262. });
  263.  
  264. // Periodically update priorities every 5 seconds
  265. setInterval(updateCardPriorities, 5000);
  266. })();