Jira - Toggle columns

Collapse Jira swimlane columns upon click

  1. // ==UserScript==
  2. // @name Jira - Toggle columns
  3. // @description Collapse Jira swimlane columns upon click
  4. // @namespace jiramod
  5. // @license MIT
  6. // @version 1.0
  7. // @match https://*.atlassian.net/jira/*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // ==/UserScript==
  12. /* jshint esversion: 6 */
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. const collapsedWidth = 40;
  18. const toggled = {};
  19.  
  20. let board = location.pathname;
  21.  
  22. let mainArea = "#ghx-content-main";
  23. let clickArea = "#ghx-pool";
  24. let header = ".ghx-column-headers .ghx-column";
  25. let column = ".ghx-columns .ghx-column";
  26. let issues = ".ghx-wrap-issue";
  27. let overlay = ".ghx-zone-overlay-table .ghx-zone-overlay-column";
  28. let ticketCount = ".ghx-qty";
  29. let swimlaneHeader = ".ghx-swimlane-header";
  30.  
  31. let hasSwimlaneHeaders = false;
  32.  
  33. // getComputedStyle(document.querySelector('.ghx-column')).padding
  34. let columnPadding = "10px";
  35. let columnPaddingNarrow = columnPadding;
  36. let columnPaddingXtraNarrow = "5px";
  37.  
  38. function rafAsync() {
  39. return new Promise((resolve) => {
  40. requestAnimationFrame(resolve); // Faster than setTimeout
  41. });
  42. }
  43. function checkElement(element) {
  44. if (document.querySelector(element) === null) {
  45. return rafAsync().then(() => checkElement(element));
  46. } else {
  47. return Promise.resolve(true);
  48. }
  49. }
  50. checkElement(mainArea).then((element) => {
  51. startJiraOverrides();
  52. });
  53. checkElement(clickArea).then((element) => {
  54. startClick();
  55. });
  56.  
  57. function startJiraOverrides() {
  58. // `GH` functions overwrite Jira's own logic with slight modifications
  59.  
  60. // Jira: Ignore collapsed columns when detecting narrow widths
  61. GH.SwimlaneView.updateIssueLayoutAccording2Size = function (e, firstWidth) {
  62. var widths = e
  63. .map(function () {
  64. return AJS.$(this).width();
  65. })
  66. .get(),
  67. uncollapsedWidth = widths.find(function (el) {
  68. return el > collapsedWidth;
  69. }),
  70. i = uncollapsedWidth <= GH.SwimlaneView.NARROW_CARD_WIDTH,
  71. l = uncollapsedWidth <= GH.SwimlaneView.XTRA_NARROW_CARD_WIDTH;
  72. e.toggleClass("ghx-narrow-card", i), e.toggleClass("ghx-xtra-narrow-card", l);
  73. };
  74.  
  75. // Jira: Show simple ticket count in header if nonzero
  76. GH.tpl.rapid.swimlane.renderColumnCount = function (opt_data, opt_ignored) {
  77. return (
  78. '<div class="ghx-qty">' +
  79. (opt_data.column.stats.visible > 0 ? soy.$$escapeHtml(opt_data.column.stats.visible) : "") +
  80. "</div>"
  81. );
  82. };
  83.  
  84. // Jira: Add token icons for each issue, to be rendered on collapsed columns
  85. GH.tpl.rapid.swimlane.renderColumnsHeader = function (opt_data, opt_ignored) {
  86. var output =
  87. '<div id="ghx-column-header-group" class="ghx-column-header-group' +
  88. (opt_data.statistics.fieldConfigured ? " ghx-has-stats" : "") +
  89. ' ghx-fixed"><ul id="ghx-column-headers" class="ghx-column-headers">';
  90. var columnList163 = opt_data.columns;
  91. var columnListLen163 = columnList163.length;
  92. for (var columnIndex163 = 0; columnIndex163 < columnListLen163; columnIndex163++) {
  93. var columnData163 = columnList163[columnIndex163];
  94. output +=
  95. '<li class="ghx-column' +
  96. (columnData163.minBusted ? " ghx-busted ghx-busted-min" : "") +
  97. (columnData163.maxBusted ? " ghx-busted ghx-busted-max" : "") +
  98. '" data-id="' +
  99. soy.$$escapeHtml(columnData163.id) +
  100. '" ><div class="ghx-column-header-flex"><div class="ghx-column-header-flex-1"><h2 data-tooltip="' +
  101. soy.$$escapeHtml(columnData163.name) +
  102. '" data-tokens="' +
  103. " ■".repeat(soy.$$escapeHtml({ column: columnData163 }.column.stats.visible)) +
  104. '">' +
  105. soy.$$escapeHtml(columnData163.name) +
  106. "</h2>" +
  107. (opt_data.statistics.fieldConfigured
  108. ? GH.tpl.rapid.swimlane.renderColumnCount({ column: columnData163 })
  109. : "") +
  110. "</div>" +
  111. (opt_data.statistics.fieldConfigured
  112. ? '<div class="ghx-limits">' +
  113. GH.tpl.rapid.swimlane.renderColumnConstraints({ column: columnData163 }) +
  114. "</div>"
  115. : "") +
  116. "</div></li>";
  117. }
  118. output +=
  119. "</ul>" +
  120. (!opt_data.isHorizontalScrollEnabled ? '<div id="ghx-swimlane-header-stalker"></div>' : "") +
  121. "</div>";
  122. return output;
  123. };
  124. }
  125.  
  126. function startClick() {
  127. function toggle(index) {
  128. console.log("Toggling column", index);
  129.  
  130. if (toggled[index] === undefined) {
  131. GM_addStyle(`
  132. body.hidden-${index} ${column}:nth-of-type(${index}),
  133. body.hidden-${index} ${header}:nth-of-type(${index}),
  134. body.hidden-${index} ${overlay}:nth-of-type(${index}) {
  135. width: ${collapsedWidth}px !important;
  136. }
  137. body.hidden-${index} ${column}:nth-of-type(${index}) ${issues} {
  138. display: none;
  139. }
  140.  
  141. body.hidden-${index} ${header}:nth-of-type(${index}) {
  142. overflow: visible !important;
  143. }
  144. body.hidden-${index} ${header}:nth-of-type(${index}) h2 {
  145. overflow: visible !important;
  146. transform: rotate(90deg);
  147. transform-origin: left;
  148. font-weight: normal;
  149. margin-left: calc((${collapsedWidth}px / 2) - ${columnPadding});
  150. }
  151. body.hidden-${index} ${header}.ghx-narrow-card:nth-of-type(${index}) h2 {
  152. margin-left: calc((${collapsedWidth}px / 2) - ${columnPaddingNarrow});
  153. }
  154. body.hidden-${index} ${header}.ghx-xtra-narrow-card:nth-of-type(${index}) h2 {
  155. margin-left: calc((${collapsedWidth}px / 2) - ${columnPaddingXtraNarrow});
  156. }
  157. body.hidden-${index} ${header}:nth-of-type(${index}) h2::after {
  158. content: " " attr(data-tokens);
  159. white-space: pre;
  160. opacity: 0.2;
  161. }
  162. body.hidden-${index} ${header}:nth-of-type(${index}) ${ticketCount} {
  163. display: none;
  164. }
  165. `);
  166.  
  167. if (hasSwimlaneHeaders) {
  168. // Hide column headers until hovered
  169. GM_addStyle(`
  170. body.hidden-${index} ${header}:nth-of-type(${index}) h2 {
  171. font-weight: 600;
  172. text-shadow:
  173. -1px -1px 0 #fff,
  174. -1px 1px 0 #fff,
  175. 1px -1px 0 #fff,
  176. 1px 1px 0 #fff,
  177. 0 0 12px #fff,
  178. 0 0 12px #fff,
  179. 0 0 12px #fff,
  180. 0 0 12px #fff,
  181. 0 0 12px #fff,
  182. 0 0 12px #fff,
  183. 0 0 12px #fff,
  184. 0 0 12px #fff,
  185. 0 0 24px #fff,
  186. 0 0 24px #fff,
  187. 0 0 24px #fff,
  188. 0 0 24px #fff,
  189. 0 0 24px #fff,
  190. 0 0 24px #fff,
  191. 0 0 24px #fff,
  192. 0 0 24px #fff;
  193. z-index: 100;
  194. visibility: hidden;
  195. }
  196. body.hidden-${index} ${header}:nth-of-type(${index}):hover h2 {
  197. visibility: visible;
  198. }
  199. `);
  200. }
  201. }
  202. toggled[index] = !toggled[index];
  203.  
  204. if (toggled[index]) {
  205. document.body.classList.add(`hidden-${index}`);
  206. } else {
  207. document.body.classList.remove(`hidden-${index}`);
  208. }
  209.  
  210. // Refresh in case it has been updated in another window
  211. globalToggled = JSON.parse(GM_getValue("globalToggled", "{}"));
  212.  
  213. globalToggled[board] = toggled;
  214. GM_setValue("globalToggled", JSON.stringify(globalToggled));
  215.  
  216. // Jira: Redraw issues as wide or narrow or extra-narrow
  217. GH.SwimlaneView.handleResizeEvent();
  218. }
  219.  
  220. // Toggle columns on click
  221. document.querySelector(clickArea).addEventListener(
  222. "click",
  223. (e) => {
  224. let target = e.target;
  225.  
  226. if (target.matches(column) || (target = target.closest(header))) {
  227. let index = [...target.parentElement.children].indexOf(target) + 1;
  228. toggle(index);
  229. }
  230. },
  231. true
  232. );
  233.  
  234. // Hide collapsed column headers if swimlane headers are present
  235. if (document.querySelector(swimlaneHeader)) {
  236. hasSwimlaneHeaders = true;
  237. }
  238.  
  239. // Collapse previously-toggled columns
  240. let globalToggled = JSON.parse(GM_getValue("globalToggled", "{}"));
  241. const loaded = globalToggled[board] === undefined ? {} : globalToggled[board];
  242. console.log("Previously-toggled columns:", loaded);
  243. Object.entries(loaded).forEach(([index, collapsed]) => {
  244. if (collapsed) toggle(index);
  245. });
  246.  
  247. // Style cursor and column headers
  248. GM_addStyle(`
  249. ${header}, ${column} { cursor: pointer; }
  250. ${header} h2 {
  251. /* Shortened titles look nicer with some space */
  252. margin-left: 8px;
  253. margin-right: 8px;
  254. }
  255. `);
  256. }
  257. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址