Jira Branch Name Copier with Type Selector

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();