Jira Copy Ticket

Adds a "Copy Ticket" button to copy the title and ticket link as rich text

  1. // ==UserScript==
  2. // @name Jira Copy Ticket
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.3.2
  5. // @description Adds a "Copy Ticket" button to copy the title and ticket link as rich text
  6. // @author Othman Shareef (othmanosx@gmail.com)
  7. // @match https://eventmobi.atlassian.net/*
  8. // @icon https://www.google.com/s2/favicons?domain=atlassian.net
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12. (function () {
  13. 'use strict';
  14. let copyTitleButton
  15. let copyIdButton
  16. let debounceTimer;
  17.  
  18. function getClassNames() {
  19. const element = document.querySelector('[data-testid="issue-view-foundation.quick-add.quick-add-items-compact.add-button-dropdown--trigger"]');
  20. return element ? element.className : null;
  21. }
  22.  
  23. // Wait for the Jira ticket title element to be available
  24. const waitForElement = (selector, callback) => {
  25. const element = document.querySelector(selector);
  26. if (element) {
  27. callback();
  28. } else {
  29. setTimeout(() => {
  30. waitForElement(selector, callback);
  31. }, 500);
  32. }
  33. };
  34.  
  35. // copy ticket title to clipboard
  36. const copyTicketTitle = () => {
  37. copyTitleButton.innerText = 'Loading...';
  38. const ticketTitleElement = document.querySelector('[data-testid="issue.views.issue-base.foundation.summary.heading"]');
  39. const ticketLinkElement = document.querySelector('[data-testid="issue.views.issue-base.foundation.breadcrumbs.current-issue.item"]');
  40. if (ticketTitleElement && ticketLinkElement) {
  41. const ticketTitle = ticketTitleElement.innerText;
  42. const ticketLink = ticketLinkElement.href
  43. const ticketID = ticketLinkElement.firstChild.innerText
  44. const html = `<a href="${ticketLink}">${ticketID}: ${ticketTitle}</a>`
  45. const clipboardItem = new ClipboardItem({
  46. 'text/html': new Blob([html], {
  47. type: 'text/html'
  48. }),
  49. 'text/plain': new Blob([html], {
  50. type: 'text/plain'
  51. })
  52. });
  53. navigator.clipboard.write([clipboardItem]).then(_ => {
  54. // Change button text to "Copied!" for a moment
  55. copyTitleButton.innerText = 'Copied!';
  56. setTimeout(() => {
  57. // Change button text back to the original text after a delay
  58. copyTitleButton.innerText = 'Copy Ticket Title';
  59. }, 1000); // You can adjust the delay (in milliseconds) as needed
  60. }, error => alert(error));
  61. } else {
  62. alert('Ticket title element not found!');
  63. }
  64. };
  65.  
  66. // copy ticket ID to clipboard
  67. const copyTicketId = () => {
  68. copyIdButton.innerText = 'Loading...';
  69. const idElement = document.querySelector('[data-testid="issue.views.issue-base.foundation.breadcrumbs.current-issue.item"]');
  70.  
  71. if (idElement) {
  72. const ticketID = idElement.firstChild.innerText
  73. const html = `${ticketID}`
  74. const clipboardItem = new ClipboardItem({
  75. 'text/html': new Blob([html], {
  76. type: 'text/html'
  77. }),
  78. 'text/plain': new Blob([html], {
  79. type: 'text/plain'
  80. })
  81. });
  82. navigator.clipboard.write([clipboardItem]).then(_ => {
  83. // Change button text to "Copied!" for a moment
  84. copyIdButton.innerText = 'Copied!';
  85. setTimeout(() => {
  86. // Change button text back to the original text after a delay
  87. copyIdButton.innerText = 'Copy ID';
  88. }, 1000); // You can adjust the delay (in milliseconds) as needed
  89. }, error => alert(error));
  90. } else {
  91. alert('Ticket title element not found!');
  92. }
  93. };
  94.  
  95. // Add button next to the ticket title
  96. const addCopyTitleButton = () => {
  97. const existingCopyBtn = document.getElementById("copy-title-button")
  98. if (existingCopyBtn) return
  99. const copyButton = document.createElement('button');
  100. const JiraButtonClassName = getClassNames()
  101. copyButton.innerText = 'Copy Ticket Title';
  102. copyButton.className = JiraButtonClassName;
  103. copyButton.id = "copy-title-button"
  104. copyButton.addEventListener('click', copyTicketTitle);
  105.  
  106. copyTitleButton = copyButton
  107.  
  108. const element = document.querySelector('[data-testid="issue-view-foundation.quick-add.quick-add-items-compact.add-button-dropdown--trigger"]');
  109. element.parentElement.parentElement.parentElement.parentElement.appendChild(copyTitleButton);
  110. };
  111.  
  112. // Add another button to copy the ticket ID
  113. const addCopyIdButton = () => {
  114. const existingCopyBtn = document.getElementById("copy-id-button")
  115. if (existingCopyBtn) return
  116. const copyButton = document.createElement('button');
  117. const JiraButtonClassName = getClassNames()
  118. copyButton.innerText = 'Copy ID';
  119. copyButton.className = JiraButtonClassName;
  120. copyButton.id = "copy-id-button"
  121. copyButton.addEventListener('click', copyTicketId);
  122.  
  123. copyIdButton = copyButton
  124.  
  125. const element = document.querySelector('[data-testid="issue-view-foundation.quick-add.quick-add-items-compact.add-button-dropdown--trigger"]');
  126. element.parentElement.parentElement.parentElement.parentElement.appendChild(copyIdButton);
  127. };
  128.  
  129. const debounce = (func, delay) => {
  130. clearTimeout(debounceTimer);
  131. debounceTimer = setTimeout(func, delay);
  132. };
  133.  
  134. // Use MutationObserver to detect changes in the DOM
  135. const observer = new MutationObserver(() => {
  136. debounce(() => {
  137. waitForElement('[data-testid="issue.views.issue-base.foundation.summary.heading"]', () => { addCopyTitleButton(); addCopyIdButton(); })
  138. }, 100);
  139. });
  140.  
  141. // Observe changes in the body and its descendants
  142. observer.observe(document.body, {
  143. childList: true,
  144. subtree: true,
  145. });
  146.  
  147. })();