Jira Backlog Enhancements

Collapse/Expand all buttons, filter by sprint name

当前为 2025-05-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Jira Backlog Enhancements
  3. // @namespace miffin
  4. // @version 2025-5-19
  5. // @description Collapse/Expand all buttons, filter by sprint name
  6. // @author Craig Whiffin
  7. // @match https://*.atlassian.net/jira/*/backlog*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=atlassian.net
  9. // @license MIT
  10. // @grant window.onurlchange
  11. // ==/UserScript==
  12.  
  13. // ###################################################################
  14. // JBE stuff here
  15. // ###################################################################
  16. var JBE = (window.JBE = {});
  17.  
  18. let JBE_define = () => {
  19. JBE.top_bar_selector = 'div._16jx1txw._1hftv77o._1uf81b66._1o94h2mm._yj55idpf > ul';
  20.  
  21. // stolen from: https://stackoverflow.com/a/61511955
  22. function waitForElm(selector) {
  23. return new Promise((resolve) => {
  24. if (document.querySelector(selector)) {
  25. return resolve(document.querySelector(selector));
  26. }
  27.  
  28. const observer = new MutationObserver((mutations) => {
  29. if (document.querySelector(selector)) {
  30. observer.disconnect();
  31. resolve(document.querySelector(selector));
  32. }
  33. });
  34.  
  35. // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
  36. observer.observe(document.body, {
  37. childList: true,
  38. subtree: true,
  39. });
  40. });
  41. }
  42.  
  43. JBE.get_top_bar = async () => {
  44. console.log("JBE: waiting for top bar to load...");
  45. const top_bar = await waitForElm(JBE.top_bar_selector);
  46. console.log("JBE: top_bar loaded!");
  47. return top_bar;
  48. };
  49.  
  50. let top_bar_button_style = "css-48ccbj"; // ??
  51.  
  52. let spawn_top_bar_container = () => {
  53. let outer = document.createElement("div");
  54. outer.className = "sc-1krxkwp-0 jcJsAc"; // ??!
  55.  
  56. let inner = document.createElement("div");
  57. inner.className = "_19bv1b66 _u5f31b66"; // ???!1
  58. outer.appendChild(inner);
  59.  
  60. return inner;
  61. };
  62.  
  63. JBE.get_sprints = () => {
  64. // They keep changing the tags for these.
  65. return document.querySelectorAll(
  66. "._1mouidpf, .ahoa2g-3, .ahoa2g-2, .css-1w12hrg, .css-14lcwon"
  67. ); // ???1!1?
  68. };
  69.  
  70. // ===================================================================
  71. JBE.collapse_all = () => {
  72. document
  73. .querySelectorAll(
  74. 'div[aria-controls^="backlog-accordion"][aria-expanded=true]'
  75. )
  76. .forEach((elem) => elem.click());
  77. };
  78.  
  79. JBE.expand_all = () => {
  80. document
  81. .querySelectorAll(
  82. 'div[data-testid^="software-backlog.card-list.left-side"][aria-expanded=false]'
  83. )
  84. .forEach((elem) => {
  85. // only do this for visible elements, otherwise it takes forever
  86. if (elem.offsetParent !== null) {
  87. elem.click();
  88. }
  89. });
  90. };
  91.  
  92. JBE.show_only = (input_element) => {
  93. console.info("Showing only sprints matching:", input_element.value);
  94.  
  95. JBE.get_sprints().forEach((elem) => {
  96. let sprint_name = elem.innerHTML.toLowerCase();
  97. let query = input_element.value.toLowerCase();
  98. let is_match = sprint_name.includes(query);
  99. let parent_block = elem.closest(
  100. 'div[data-testid^="software-backlog.card-list.container"]'
  101. );
  102. console.assert(parent_block !== null, "Couldn't get the parent card for " + sprint_name)
  103. parent_block.style.display = is_match ? "block" : "none";
  104. });
  105. };
  106. // ===================================================================
  107.  
  108. // Should be run once the page has loaded, or else it won't be able to find the top_bar
  109. JBE.add_UI = async () => {
  110. let top_bar = await JBE.get_top_bar();
  111.  
  112. console.assert(top_bar !== null, "JBE: couldn't find top_bar!");
  113.  
  114. {
  115. let container = spawn_top_bar_container();
  116. container.style.flexDirection = "column";
  117. container.className = "JBE_container";
  118.  
  119. {
  120. let btn = document.createElement("button");
  121. btn.setAttribute("id", "collapse_all_sprints");
  122. btn.setAttribute("title", "Collapse All Sprints");
  123. btn.addEventListener("click", () => JBE.collapse_all(), false);
  124. btn.innerHTML = `
  125. <svg width="24" height="24" viewBox="0 0 24 24">
  126. <path
  127. d="m 13.264222,0.88771738 c -0.699238,-0.69923767 -1.834799,-0.69923767 -2.534037,0 L 1.7799447,9.8379574 c -0.6992377,0.6992386 -0.6992377,1.8347996 0,2.5340376 0.6992377,0.699238 1.834799,0.699238 2.5340367,0 L 12,4.6859761 19.686019,12.366401 c 0.699238,0.699238 1.834799,0.699238 2.534037,0 0.699237,-0.699238 0.699237,-1.834799 0,-2.5340374 l -8.950241,-8.95024 z m 8.95024,19.69052862 -8.95024,-8.950241 c -0.699238,-0.699237 -1.834799,-0.699237 -2.534037,0 l -8.9502403,8.950241 c -0.6992377,0.699237 -0.6992377,1.834799 0,2.534037 0.6992377,0.699237 1.834799,0.699237 2.5340367,0 L 12,15.426264 l 7.686019,7.680425 c 0.699238,0.699237 1.834799,0.699237 2.534037,0 0.699237,-0.699238 0.699237,-1.834799 0,-2.534037 z"
  128. fill="currentColor"
  129. fill-rule="evenodd"
  130. />
  131. </svg>
  132. `;
  133. btn.className = top_bar_button_style;
  134. container.appendChild(btn);
  135. }
  136.  
  137. {
  138. let btn = document.createElement("button");
  139. btn.setAttribute("id", "expand_all_sprints");
  140. btn.setAttribute("title", "Expand All Sprints");
  141. btn.addEventListener("click", () => JBE.expand_all(), false);
  142. btn.innerHTML = `
  143. <svg width="24" height="24" viewBox="0 0 24 24">
  144. <path
  145. d="m 13.264222,23.112282 c -0.699238,0.699238 -1.834799,0.699238 -2.534037,0 l -8.9502403,-8.95024 c -0.6992377,-0.699238 -0.6992377,-1.834799 0,-2.534037 0.6992377,-0.699238 1.834799,-0.699238 2.5340367,0 L 12,19.314024 19.686019,11.633599 c 0.699238,-0.699238 1.834799,-0.699238 2.534037,0 0.699237,0.699238 0.699237,1.834799 0,2.534037 l -8.950241,8.95024 z m 8.95024,-19.6905281 -8.95024,8.9502411 c -0.699238,0.699237 -1.834799,0.699237 -2.534037,0 L 1.7799447,3.4217539 c -0.6992377,-0.699237 -0.6992377,-1.834799 0,-2.53403702 0.6992377,-0.699237 1.834799,-0.699237 2.5340367,0 L 12,8.5737359 19.686019,0.89331088 c 0.699238,-0.699237 1.834799,-0.699237 2.534037,0 0.699237,0.69923802 0.699237,1.83479902 0,2.53403702 z"
  146. fill="currentColor"
  147. fill-rule="evenodd"
  148. />
  149. </svg>
  150. `;
  151. btn.className = top_bar_button_style;
  152. container.appendChild(btn);
  153. }
  154.  
  155. top_bar.appendChild(container);
  156. }
  157.  
  158. // filter by sprint name
  159. {
  160. let filter_input = document.createElement("input");
  161. filter_input.id = "filter_by_sprint";
  162. filter_input.class = "css-1cab8vv";
  163. filter_input.placeholder = "Sprint Name";
  164. filter_input.addEventListener(
  165. "input",
  166. () => JBE.show_only(filter_input),
  167. false
  168. );
  169.  
  170. let filter_icon = document.createElement("div");
  171. filter_icon.className = "css-tww5fb";
  172. filter_icon.innerHTML = `
  173. <span
  174. aria-hidden="true"
  175. class="css-1wits42"
  176. style="
  177. --icon-primary-color: currentColor;
  178. --icon-secondary-color: var(--ds-surface, #ffffff);
  179. "
  180. ><svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
  181. <path
  182. d="M16.436 15.085l3.94 4.01a1 1 0 01-1.425 1.402l-3.938-4.006a7.5 7.5 0 111.423-1.406zM10.5 16a5.5 5.5 0 100-11 5.5 5.5 0 000 11z"
  183. fill="currentColor"
  184. fill-rule="evenodd"
  185. ></path></svg
  186. ></span>
  187. `;
  188.  
  189. let filter_container = document.createElement("div");
  190. filter_container.className = "css-19p3uok";
  191. filter_container.style.minWidth = "64px";
  192. filter_container.appendChild(filter_input);
  193. filter_container.appendChild(filter_icon);
  194. top_bar.appendChild(filter_container);
  195. }
  196. };
  197.  
  198. JBE.UI_already_added = () => {
  199. var found_it = document.querySelector(".JBE_container");
  200. return found_it !== null;
  201. };
  202.  
  203. window.JBE = JBE;
  204. return JBE;
  205. };
  206.  
  207. JBE = JBE_define();
  208.  
  209. // ###################################################################
  210. // Event hookups here:
  211. // ###################################################################
  212.  
  213. // Add UI on page load
  214. let on_load_handler = async () => { await JBE.add_UI(); };
  215. window.addEventListener("load", on_load_handler, false);
  216.  
  217. // ..._and_ if the URL changes to '*/backlog' and we don't have the UI
  218. if (window.onurlchange === null) {
  219. // feature is supported
  220. // yes, !== null _would_ make more sense, but who cares
  221. let url_change_handler = async (info) => {
  222. let is_valid_url = info.url.endsWith("/backlog");
  223. let JBE = JBE_define();
  224. if (!JBE.UI_already_added() && is_valid_url) {
  225. console.log("JBE: URL ends with '/backlog', re-injecting JBE...");
  226. JBE_define();
  227. await JBE.add_UI();
  228. }
  229. };
  230.  
  231. window.addEventListener("urlchange", (info) => url_change_handler(info));
  232. }

QingJ © 2025

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