// ==UserScript== // @name Jira Branch Name Copier with Type Selector // @description Copy branch name to clipboard from Jira issue page with selectable type // @namespace VovanNet/jira-branch-generator // @version 1.1.1 // @author VovanNet // @match https://*.atlassian.net/browse/* // @match https://*.atlassian.net/jira/software/*/projects/*/boards/* // @icon https://www.google.com/s2/favicons?sz=64&domain=atlassian.net // @grant GM_setClipboard // @grant GM_addStyle // @downloadURL https://update.greasyfork.icu/scripts/556214/Jira%20Branch%20Name%20Copier%20with%20Type%20Selector.user.js // @updateURL https://update.greasyfork.icu/scripts/556214/Jira%20Branch%20Name%20Copier%20with%20Type%20Selector.meta.js // ==/UserScript== GM_addStyle(` .copy-branch-panel { position: relative; display: inline-block; } .selected-branch-type-svg { width: 14px; padding: 6px 1px 0px 5px; } .selected-branch-type-svg svg:hover { transform: scale(1.1); } .branch-svg { width: 20px; padding: 4px 4px 0px 4px; } .branch-svg svg:hover { transform: scale(1.15); } .svg-buttons { display: flex; cursor: pointer; border: 1px solid green; border-radius: 5px; } .svg-buttons:hover { background-color: #F4FFE6; } .branch-type-dropdown { display: none; position: absolute; left: 0; background-color: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); z-index: 1000; margin: 1px -3px; } .branch-type-dropdown svg { width: 20px; height: 20px; padding: 2px; cursor: pointer; border-radius: 8px; transition: transform 0.2s; } .branch-type-dropdown svg:hover { transform: scale(1.1); outline: 1px solid #0074d9; } .svg-title-wrapper { margin: 4px; } `); (function () { "use strict"; // --- Constants & IDs const BRANCH_COMPONENT_ID = "ad-copy-branch-name"; const branchData = { title: "Copy Branch Name", svgHtml: ` `, }; const clipboardData = { svgHtml: ` `, }; const optionsData = [ { id: "feature", prefix: "feature", title: "Feature", regex: /Task/i, svgHtml: ` `, }, { id: "hotfix", prefix: "hotfix", title: "HotFix", svgHtml: ` `, }, { id: "bug", prefix: "fix", title: "Bug", regex: /Bug/i, svgHtml: ` `, }, { id: "refactoring", prefix: "refactoring", title: "Refactoring", svgHtml: ` `, }, ]; function getIssueInfo(container) { const keySelectors = [ '[data-testid="issue.views.issue-base.foundation.breadcrumbs.breadcrumb-current-issue-container"]', '[data-test-id="issue.key"]', ]; const titleSelectors = [ '[data-testid="issue.views.issue-base.foundation.summary.heading"]', '[data-test-id="issue.views.issue-base.foundation.summary"]', ]; const issueKey = keySelectors .map((sel) => container.querySelector(sel)) .filter(Boolean)[0] ?.textContent?.trim(); const issueTitle = titleSelectors .map((sel) => container.querySelector(sel)) .filter(Boolean)[0] ?.textContent?.trim(); return { issueKey, issueTitle }; } function getDefaultOptionByIssueType(container) { const el = container.querySelector( '[data-testid="issue.views.issue-base.foundation.change-issue-type.button"]' ); const issueTypeDescription = el?.getAttribute("aria-label")?.toLowerCase(); if (issueTypeDescription) { for (const option of optionsData) { if (option.regex?.test(issueTypeDescription)) { return option; } } } return optionsData[0]; } function toKebabCase(str) { if (!str) return ""; // Normalize accents, remove diacritics, remove non-word chars, collapse whitespace return str .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^^\w\s-]/g, "") .trim() .toLowerCase() .replace(/\s+/g, "-") .replace(/^[-]+|[-]+$/g, "") .substring(0, 140); } // Keep a single global click handler to close dropdowns function addGlobalDropdownCloser() { if (window.__ad_global_dropdown_installed) return; window.__ad_global_dropdown_installed = true; document.addEventListener("click", (e) => { if (!e.target.closest(".svg-dropdown")) { document.querySelector(".branch-type-dropdown").style.display = "none"; } }); } function createBranchPanel(container) { try { if (document.getElementById(BRANCH_COMPONENT_ID)) return; const jiraButtonPanel = container.querySelector( '[data-testid="issue.watchers.action-button.tooltip--container"]' )?.parentElement?.parentElement?.parentElement?.parentElement; if (!jiraButtonPanel) return; const { selectedTypeDiv, dropDownOptions } = createSelectedTypeSvgButtonAndDropDown(getDefaultOptionByIssueType(container)); const branchDiv = createCopyBranchSvgButton(selectedTypeDiv); const svgButtonsWrapperDiv = document.createElement("div"); svgButtonsWrapperDiv.className = "svg-buttons"; svgButtonsWrapperDiv.appendChild(selectedTypeDiv); svgButtonsWrapperDiv.appendChild(branchDiv); const copyBranchPanel = document.createElement("div"); copyBranchPanel.id = BRANCH_COMPONENT_ID; copyBranchPanel.className = "copy-branch-panel"; copyBranchPanel.appendChild(svgButtonsWrapperDiv); copyBranchPanel.appendChild(dropDownOptions); jiraButtonPanel.prepend(copyBranchPanel); } catch (err) { console.error("createSvgBranchDropDown error:", err); } function createCopyBranchSvgButton(selectedTypeDiv) { const branchSvgButtonDiv = document.createElement("div"); branchSvgButtonDiv.className = "branch-svg"; branchSvgButtonDiv.innerHTML = branchData.svgHtml; branchSvgButtonDiv.title = branchData.title; branchSvgButtonDiv.addEventListener("click", () => { const { issueKey, issueTitle } = getIssueInfo(container); if (!issueKey || !issueTitle) { console.warn( "Issue key or title not found — cannot build branch name" ); return; } const issueType = selectedTypeDiv.dataset.prefix; const kebabTitle = toKebabCase(issueTitle); const branchName = `${issueType}/${issueKey}-${kebabTitle}`; try { GM_setClipboard(branchName); console.log(`Copied to clipboard: ${branchName}`); } catch (err) { console.error("GM_setClipboard failed:", err); } // show quick visual feedback branchSvgButtonDiv.innerHTML = clipboardData.svgHtml; setTimeout(() => { branchSvgButtonDiv.innerHTML = branchData.svgHtml; }, 300); }); return branchSvgButtonDiv; } function createSelectedTypeSvgButtonAndDropDown(selectedOption){ const selectedTypeDiv = document.createElement("div"); selectedTypeDiv.className = "selected-branch-type-svg"; selectedTypeDiv.innerHTML = selectedOption.svgHtml; selectedTypeDiv.title = selectedOption.title; selectedTypeDiv.dataset.prefix = selectedOption.prefix; const dropDownOptions = document.createElement("div"); dropDownOptions.className = "branch-type-dropdown"; optionsData.forEach((option) => { const wrapperDiv = document.createElement("div"); wrapperDiv.innerHTML = option.svgHtml; wrapperDiv.title = option.title; wrapperDiv.className = "svg-title-wrapper"; const svgElement = wrapperDiv.firstChild; svgElement.setAttribute("data-name", option.id); svgElement.addEventListener("click", (e) => { e.stopPropagation(); // replace displayed svg selectedTypeDiv.innerHTML = option.svgHtml; selectedTypeDiv.title = option.title; selectedTypeDiv.dataset.prefix = option.prefix; dropDownOptions.style.display = "none"; }); wrapperDiv.appendChild(svgElement); dropDownOptions.appendChild(wrapperDiv); }); selectedTypeDiv.addEventListener("click", (e) => { e.stopPropagation(); dropDownOptions.style.display = dropDownOptions.style.display === "block" ? "none" : "block"; }); addGlobalDropdownCloser(); return { selectedTypeDiv, dropDownOptions }; } } const scanPage = () => { if (!document.getElementById(BRANCH_COMPONENT_ID)) { const modalDialog = document.querySelector( '[data-testid="issue.views.issue-details.issue-modal.modal-dialog"]' ); const issueDetailsView = document.querySelector( '[data-testid="issue.views.issue-details.issue-layout.issue-layout"]' ); if (modalDialog) { console.log("Modal dialog found"); createBranchPanel(modalDialog); } else if (issueDetailsView) { console.log("Issue details found"); createBranchPanel(issueDetailsView); } } setTimeout(() => requestAnimationFrame(scanPage), 1000); }; // Start requestAnimationFrame(scanPage); })();