Jira Branch Name Copier with Type Selector

Copy branch name to clipboard from Jira issue page with selectable type

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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
// ==/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: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
			  <path fill="#000000" fill-rule="evenodd" d="M11.5 4.5a1 1 0 1 0 0-2a1 1 0 0 0 0 2Zm2.5-1a2.501 2.501 0 0 1-1.872 2.42A3.502 3.502 0 0 1 8.75 8.5h-1.5a2 2 0 0 0-1.965 1.626a2.501 2.501 0 1 1-1.535-.011v-4.23a2.501 2.501 0 1 1 1.5 0v1.742a3.484 3.484 0 0 1 2-.627h1.5a2 2 0 0 0 1.823-1.177A2.5 2.5 0 1 1 14 3.5Zm-8.5 9a1 1 0 1 1-2 0a1 1 0 0 1 2 0Zm0-9a1 1 0 1 1-2 0a1 1 0 0 1 2 0Z" clip-rule="evenodd"/>
		  </svg>`,
	};

	const clipboardData = {
		svgHtml: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
			<path fill="#C1694F" d="M32 34a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h24a2 2 0 0 1 2 2v27z"/><path fill="#FFF" d="M29 32a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h20a1 1 0 0 1 1 1v23z"/><path fill="#CCD6DD" d="M25 3h-4a3 3 0 1 0-6 0h-4a2 2 0 0 0-2 2v5h18V5a2 2 0 0 0-2-2z"/><circle cx="18" cy="3" r="2" fill="#292F33"/><path fill="#99AAB5" d="M20 14a1 1 0 0 1-1 1h-9a1 1 0 0 1 0-2h9a1 1 0 0 1 1 1zm7 4a1 1 0 0 1-1 1H10a1 1 0 0 1 0-2h16a1 1 0 0 1 1 1zm0 4a1 1 0 0 1-1 1H10a1 1 0 1 1 0-2h16a1 1 0 0 1 1 1zm0 4a1 1 0 0 1-1 1H10a1 1 0 1 1 0-2h16a1 1 0 0 1 1 1zm0 4a1 1 0 0 1-1 1h-9a1 1 0 1 1 0-2h9a1 1 0 0 1 1 1z"/>
		  </svg>`,
	};

	const optionsData = [
		{
			id: "feature",
			prefix: "feature",
			title: "Feature",
			regex: /Task/i,
			svgHtml: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
				<path fill="#2F88FF" stroke="#000" stroke-linejoin="round" stroke-width="4" d="M30 4H18V18H4V30H18V44H30V30H44V18H30V4Z"/>
			 </svg>`,
		},
		{
			id: "hotfix",
			prefix: "hotfix",
			title: "HotFix",
			svgHtml: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
				<radialGradient id="notoFire0" cx="68.884" cy="124.296" r="70.587" gradientTransform="matrix(-1 -.00434 -.00713 1.6408 131.986 -79.345)" gradientUnits="userSpaceOnUse"><stop offset=".314" stop-color="#FF9800"/><stop offset=".662" stop-color="#FF6D00"/><stop offset=".972" stop-color="#F44336"/></radialGradient><path fill="url(#notoFire0)" d="M35.56 40.73c-.57 6.08-.97 16.84 2.62 21.42c0 0-1.69-11.82 13.46-26.65c6.1-5.97 7.51-14.09 5.38-20.18c-1.21-3.45-3.42-6.3-5.34-8.29c-1.12-1.17-.26-3.1 1.37-3.03c9.86.44 25.84 3.18 32.63 20.22c2.98 7.48 3.2 15.21 1.78 23.07c-.9 5.02-4.1 16.18 3.2 17.55c5.21.98 7.73-3.16 8.86-6.14c.47-1.24 2.1-1.55 2.98-.56c8.8 10.01 9.55 21.8 7.73 31.95c-3.52 19.62-23.39 33.9-43.13 33.9c-24.66 0-44.29-14.11-49.38-39.65c-2.05-10.31-1.01-30.71 14.89-45.11c1.18-1.08 3.11-.12 2.95 1.5z"/><radialGradient id="notoFire1" cx="64.921" cy="54.062" r="73.86" gradientTransform="matrix(-.0101 .9999 .7525 .0076 26.154 -11.267)" gradientUnits="userSpaceOnUse"><stop offset=".214" stop-color="#FFF176"/><stop offset=".328" stop-color="#FFF27D"/><stop offset=".487" stop-color="#FFF48F"/><stop offset=".672" stop-color="#FFF7AD"/><stop offset=".793" stop-color="#FFF9C4"/><stop offset=".822" stop-color="#FFF8BD" stop-opacity=".804"/><stop offset=".863" stop-color="#FFF6AB" stop-opacity=".529"/><stop offset=".91" stop-color="#FFF38D" stop-opacity=".209"/><stop offset=".941" stop-color="#FFF176" stop-opacity="0"/></radialGradient><path fill="url(#notoFire1)" d="M76.11 77.42c-9.09-11.7-5.02-25.05-2.79-30.37c.3-.7-.5-1.36-1.13-.93c-3.91 2.66-11.92 8.92-15.65 17.73c-5.05 11.91-4.69 17.74-1.7 24.86c1.8 4.29-.29 5.2-1.34 5.36c-1.02.16-1.96-.52-2.71-1.23a16.09 16.09 0 0 1-4.44-7.6c-.16-.62-.97-.79-1.34-.28c-2.8 3.87-4.25 10.08-4.32 14.47C40.47 113 51.68 124 65.24 124c17.09 0 29.54-18.9 19.72-34.7c-2.85-4.6-5.53-7.61-8.85-11.88z"/>
			</svg>`,
		},
		{
			id: "bug",
			prefix: "fix",
			title: "Bug",
			regex: /Bug/i,
			svgHtml: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
			  <path fill="red" d="M19 14h2a1 1 0 0 0 0-2h-2v-1a5.15 5.15 0 0 0-.21-1.36A5 5 0 0 0 22 5a1 1 0 0 0-2 0a3 3 0 0 1-2.14 2.87A5 5 0 0 0 16 6.4a2.58 2.58 0 0 0 0-.4a4 4 0 0 0-8 0a2.58 2.58 0 0 0 0 .4a5 5 0 0 0-1.9 1.47A3 3 0 0 1 4 5a1 1 0 0 0-2 0a5 5 0 0 0 3.21 4.64A5.15 5.15 0 0 0 5 11v1H3a1 1 0 0 0 0 2h2v1a7 7 0 0 0 .14 1.38A5 5 0 0 0 2 21a1 1 0 0 0 2 0a3 3 0 0 1 1.81-2.74a7 7 0 0 0 12.38 0A3 3 0 0 1 20 21a1 1 0 0 0 2 0a5 5 0 0 0-3.14-4.62A7 7 0 0 0 19 15Zm-8 5.9A5 5 0 0 1 7 15v-4a3 3 0 0 1 3-3h1ZM10 6a2 2 0 0 1 4 0Zm7 9a5 5 0 0 1-4 4.9V8h1a3 3 0 0 1 3 3Z"/>
			</svg>`,
		},
		{
			id: "refactoring",
			prefix: "refactoring",
			title: "Refactoring",
			svgHtml: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
			   <g fill="none"><g clip-path="url(#gravityUiArrows3RotateLeft0)"><path fill="green" fill-rule="evenodd" d="M11.39 2.503a6.473 6.473 0 0 0-3.383-.984a6.48 6.48 0 0 0-4.515 1.77l.005-.559a.75.75 0 1 0-1.5-.013l-.022 2.5a.75.75 0 0 0 .743.756l2.497.022a.75.75 0 1 0 .013-1.5l-.817-.007a4.983 4.983 0 0 1 3.583-1.469a4.973 4.973 0 0 1 2.602.756a.75.75 0 0 0 .795-1.272Zm-9.097 8.716a6.473 6.473 0 0 1-.84-3.422a.75.75 0 1 1 1.5.053a4.955 4.955 0 0 0 .646 2.63a4.983 4.983 0 0 0 3.064 2.37l-.403-.712a.75.75 0 0 1 1.306-.738l1.229 2.173a.75.75 0 0 1-.283 1.022l-2.176 1.23a.75.75 0 1 1-.739-1.305l.487-.275a6.48 6.48 0 0 1-3.79-3.026Zm11.258.099a6.473 6.473 0 0 1-2.544 2.438a.75.75 0 0 1-.704-1.325a4.974 4.974 0 0 0 1.955-1.875a4.983 4.983 0 0 0 .52-3.838l-.415.705a.75.75 0 1 1-1.292-.762l1.267-2.15a.75.75 0 0 1 1.027-.266l2.154 1.268a.75.75 0 1 1-.761 1.293l-.483-.284a6.48 6.48 0 0 1-.724 4.796" clip-rule="evenodd"/></g><defs><clipPath id="gravityUiArrows3RotateLeft0"><path fill="#000000" d="M0 0h16v16H0z"/></clipPath></defs></g>
			</svg>`,
		},
	];

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