Skip to content

Instantly share code, notes, and snippets.

@hushin
Created January 7, 2026 06:46
Show Gist options
  • Select an option

  • Save hushin/e7505b8663fe1caedf3e1846bb975653 to your computer and use it in GitHub Desktop.

Select an option

Save hushin/e7505b8663fe1caedf3e1846bb975653 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name JIRA Copy Assistant
// @namespace http://tampermonkey.net/
// @version 1.0
// @description JIRAの各画面(Backlog, Board, Browse)に応じたSelectorでリンク・タイトルを取得し、3形式でコピーする
// @author hushin
// @match https://*.atlassian.net/*
// @grant GM_addStyle
// @grant GM_setClipboard
// ==/UserScript==
(function () {
'use strict';
// --- 設定: フォーマット定義 ---
const FORMATS = [
{ label: 'URL', type: 'url', template: '%URL%' },
{ label: 'MD', type: 'md', template: '[%ISSUE_KEY%](%URL%) %TITLE%' },
{ label: 'ORG', type: 'org', template: '[[%URL%][%ISSUE_KEY%]] %TITLE%' },
];
// --- スタイル定義 ---
GM_addStyle(`
.jira-copy-container {
/* Popover配置 */
position: absolute;
top: 50%; /* 親要素(リンク)の下側に配置 */
left: 0;
margin-top: 2px;
z-index: 5000;
display: inline-flex;
gap: 4px;
margin-left: 8px;
align-items: center;
vertical-align: middle;
opacity: 0.1;
transition: opacity 0.2s;
z-index: 100;
}
.jira-copy-container:hover {
opacity: 1;
}
.jira-copy-btn {
font-size: 10px;
font-family: monospace;
padding: 2px 6px;
border: 1px solid #dfe1e6;
background: #fff;
color: #42526e;
cursor: pointer;
border-radius: 3px;
line-height: 1.2;
font-weight: bold;
}
.jira-copy-btn:hover {
background: #ebecf0;
color: #0052cc;
}
.jira-copy-btn.copied {
background: #e3fcef;
color: #006644;
border-color: #badcc3;
}
`);
// --- ユーティリティ ---
function getIssueKey(url) {
const match =
url.match(/browse\/([^/?#]+)/) || url.match(/selectedIssue=([^&]+)/);
return match ? match[1] : '';
}
function generateText(template, data) {
return template
.replace('%URL%', data.url)
.replace('%TITLE%', data.title)
.replace('%ISSUE_KEY%', data.key);
}
function createButtons(data) {
const container = document.createElement('span');
container.className = 'jira-copy-container';
FORMATS.forEach((fmt) => {
const btn = document.createElement('button');
btn.textContent = fmt.label;
btn.className = 'jira-copy-btn';
btn.title = fmt.template;
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const text = generateText(fmt.template, data);
navigator.clipboard
.writeText(text)
.then(() => {
const originalText = btn.textContent;
btn.textContent = 'OK';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = originalText;
btn.classList.remove('copied');
}, 1000);
})
.catch((err) => {
console.error('Copy failed:', err);
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text);
}
});
});
container.appendChild(btn);
});
return container;
}
// --- メイン処理ロジック ---
const processors = [
{
// 1. Backlog画面 (/backlog)
condition: () => window.location.pathname.endsWith('/backlog'),
run: () => {
const wrapperSelector =
'[data-testid="software-context-menu.ui.context-menu.children-wrapper"]';
const linkSelector =
'a[data-component-selector="software-backlog.card-list.card.card-contents.key"]';
const titleSelector =
'[data-testid="software-backlog.card-list.card.card-contents.summary-field.summary-field-static.content"]';
document.querySelectorAll(wrapperSelector).forEach((wrapper) => {
const linkEl = wrapper.querySelector(linkSelector);
const titleEl = wrapper.querySelector(titleSelector);
if (linkEl && titleEl) {
if (linkEl.dataset.hasCopyBtn) return;
const url = linkEl.href;
const key = getIssueKey(url);
const title = titleEl.innerText;
if (url && key) {
const btns = createButtons({ url, key, title: title.trim() });
linkEl.parentNode.insertBefore(btns, linkEl.nextSibling);
linkEl.dataset.hasCopyBtn = 'true';
}
}
});
},
},
{
// 2. Board画面 (/boards/)
condition: () => window.location.pathname.includes('/boards/'),
run: () => {
// 2-1. Story (Swimlane)
document
.querySelectorAll(
'[data-testid="platform-board-kit.ui.swimlane.link-button"]'
)
.forEach((storyContainer) => {
if (storyContainer.dataset.hasCopyBtn) return;
// storyContainer (= link-button) の中から直接探す
const linkEl = storyContainer.querySelector(
'[data-testid="platform-card.common.ui.key.key"] a'
);
const titleEl = storyContainer.querySelector(
'[data-testid="platform-board-kit.ui.swimlane.summary-section"]'
);
if (linkEl && titleEl) {
const url = linkEl.href;
const key = getIssueKey(url);
const title = titleEl.innerText;
const btns = createButtons({ url, key, title: title.trim() });
linkEl.parentNode.insertBefore(btns, linkEl.nextSibling);
storyContainer.dataset.hasCopyBtn = 'true'; // コンテナにフラグを立てて再処理防止
}
});
// 2-2. Subtask (Card)
document
.querySelectorAll('[data-testid="platform-board-kit.ui.card.card"]')
.forEach((card) => {
const linkContainer = card.querySelector(
'[data-testid="platform-card.common.ui.key.key"]'
);
if (!linkContainer) return;
const linkEl = linkContainer.querySelector('a');
if (!linkEl || linkEl.dataset.hasCopyBtn) return;
let titleEl = card.querySelector(
'[data-component-selector="platform-card.ui.card.card-content.content-section"]'
);
if (!titleEl) titleEl = card.querySelector('div[id^="summary-"]');
if (titleEl) {
const url = linkEl.href;
const key = getIssueKey(url);
const title = titleEl.innerText;
const btns = createButtons({ url, key, title: title.trim() });
linkEl.parentNode.insertBefore(btns, linkEl.nextSibling);
linkEl.dataset.hasCopyBtn = 'true';
}
});
},
},
{
// 3. Browse画面 (/browse/)
condition: () => window.location.pathname.includes('/browse/'),
run: () => {
const linkSelector =
'a[data-testid="native-issue-table.common.ui.issue-cells.issue-summary.issue-summary-cell"]';
document.querySelectorAll(linkSelector).forEach((linkEl) => {
if (linkEl.dataset.hasCopyBtn) return;
const url = linkEl.href;
const key = getIssueKey(url);
const title = linkEl.innerText;
if (url && key) {
const btns = createButtons({ url, key, title: title.trim() });
linkEl.parentNode.insertBefore(btns, linkEl.nextSibling);
linkEl.dataset.hasCopyBtn = 'true';
}
});
},
},
];
// --- 実行制御 ---
function runScript() {
processors.forEach((proc) => {
if (proc.condition()) {
try {
proc.run();
} catch (e) {
console.error('JIRA Copy script error:', e);
}
}
});
}
let timeout = null;
const observer = new MutationObserver(() => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(runScript, 500);
});
observer.observe(document.body, { childList: true, subtree: true });
runScript();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment