// ==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);
})();