Jira Branch Name Copier with Type Selector

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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);
})();