您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jira enhancements.
// ==UserScript== // @name Devabit Jira+ // @namespace http://tampermonkey.net/ // @version 3.1 // @description Jira enhancements. // @match https://devabit.atlassian.net/browse/* // @match https://devabit.atlassian.net/jira/* // @grant none // @license 3-clause BSD // ==/UserScript== (function () { "use strict"; const deadlineMap = new Map(); const estimatedHoursMap = new Map(); const uaMonths = { січ: 0, лют: 1, бер: 2, квіт: 3, трав: 4, черв: 5, лип: 6, серп: 7, вер: 8, жовт: 9, лист: 10, груд: 11, }; const jiraColors = { red: "#a10a0a", green: "#315e1d", yellow: "#ffd414", white: "#ffffff", black: "#000000", }; function parseDateString(str) { // Parses date like "16 лип. 2025 р." or "16 лип. 2025 р., 17:00" // Ignores time after comma const regex = /(\d{1,2})\s([а-яіїєґ]{3})\.?\s(\d{4})/i; const m = regex.exec(str); if (!m) return null; return { day: +m[1], month: m[2].toLowerCase(), year: +m[3], }; } function parseDateTimeString(str) { const regex = /(\d{1,2})\s([а-яіїєґ]{3,5})\.?\s(\d{4})\sр\.,?\s(\d{1,2}):(\d{2})/i; const m = regex.exec(str); if (!m) return null; const day = parseInt(m[1], 10); const month = uaMonths[m[2].toLowerCase()]; const year = parseInt(m[3], 10); const hour = parseInt(m[4], 10); const minute = parseInt(m[5], 10); if (month === undefined) return null; return new Date(year, month, day, hour, minute); } function formatTimeLeft(ms) { const absMs = Math.abs(ms); const totalSeconds = Math.floor(absMs / 1000); const totalMinutes = Math.floor(totalSeconds / 60); const totalHours = Math.floor(totalMinutes / 60); const totalDays = Math.floor(totalHours / 24); const totalWeeks = Math.floor(totalDays / 7); const totalMonths = Math.floor(totalDays / 30); let parts = []; if (totalMonths >= 1) { parts.push(`${totalMonths}mo`); const remainingDays = totalDays % 30; if (remainingDays) parts.push(`${remainingDays}d`); } else if (totalWeeks >= 1) { parts.push(`${totalWeeks}w`); const remainingDays = totalDays % 7; if (remainingDays) parts.push(`${remainingDays}d`); } else { const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (totalDays) parts.push(`${totalDays}d`); if (hours) parts.push(`${hours}h`); if (minutes) parts.push(`${minutes}m`); if (seconds || parts.length === 0) { parts.push(`${seconds.toString().padStart(2, "0")}s`); } } const label = parts.join(" "); if (ms <= 0) { return `over deadline by ${label}`; } else { return `${label} left`; } } function datesMatch(date1, date2) { return ( date1 && date2 && date1.day === date2.day && date1.month === date2.month && date1.year === date2.year ); } function jiraTimeToHours(input) { if (!/\d/.test(input)) return null; // ignore if no digits const timeUnits = { w: 40, d: 8, h: 1, m: 1 / 60, }; const regex = /(\d+)\s*(w|d|h|m)/gi; let totalHours = 0, match; while ((match = regex.exec(input)) !== null) { totalHours += parseInt(match[1], 10) * timeUnits[match[2].toLowerCase()]; } return +totalHours.toFixed(2); } function isDueDeadlineApply() { const el = document.querySelector( 'button[id="issue.fields.status-view.status-button"] > span.css-178ag6o' ); return !( el && (el.innerText === "Delivered" || el.innerText === "Within client" || el.innerText === "Archive" || el.innerText === "Scheduled") ); } function feature_highlightIfMonthBilledIsIncorrect() { if (!isDueDeadlineApply()) return; const tags = document.querySelectorAll( 'span[data-testid="issue.views.common.tag.tag-item"] > span' ); const now = new Date(); const currentMonth = now.toLocaleString("en-US", { month: "long", }); const currentYear = now.getFullYear(); tags.forEach((tag) => { // Check for Delivered in sibling span.css-178ag6o const parent = tag.closest( 'span[data-testid="issue.views.common.tag.tag-item"]' ); if (!parent) return; const deliveredSpan = parent.querySelector("span.css-178ag6o"); if (deliveredSpan && deliveredSpan.textContent.includes("Delivered")) { // Skip highlighting & timer for this task parent.style.backgroundColor = ""; parent.style.color = ""; parent.style.border = ""; return; } const text = tag.textContent.trim(); const regex = /^([A-Za-z]+)\s+(\d{4})$/; const match = text.match(regex); if (!match) return; const [_, tagMonth, tagYearStr] = match; const tagYear = parseInt(tagYearStr, 10); parent.style.border = "none"; // remove border if ( tagMonth.toLowerCase() === currentMonth.toLowerCase() && tagYear === currentYear ) { parent.style.backgroundColor = jiraColors.green; // green parent.style.color = "white"; } else { parent.style.backgroundColor = jiraColors.red; // red parent.style.color = "white"; } }); } function feature_highlightIfPOisReceived() { const spans = document.querySelectorAll( 'div[data-testid="issue.views.field.checkbox-inline-edit.customfield_10357--container"] > form > div._1pfhu2gc > div._vwz4kb7n > div > div > div > div > div > div > span > span._11c82smr._1reo15vq._18m915vq._p12fmgvx._1bto1l2s._o5721q9c' ); if (!spans.length) return; spans.forEach((span) => { const value = span.textContent.trim().toLowerCase(); const parent = span.parentElement?.closest("span"); if (!parent) return; parent.style.border = "none"; parent.style.borderRadius = "4px"; if (value === "yes") { parent.style.backgroundColor = jiraColors.green; parent.style.color = "#ffffff"; } else if (value === "no") { parent.style.backgroundColor = jiraColors.red; parent.style.color = "#ffffff"; } else { parent.style.backgroundColor = ""; parent.style.color = ""; } }); } function feature_updateJiraTime() { const selectors = [ ".css-v44io0", "span > span > ._k48p1wq8", 'span[data-testid="issue.issue-view.common.logged-time.value"]', 'span[data-testid="issue.component.logged-time.remaining-time"] > span', ]; document.querySelectorAll(selectors.join(",")).forEach((el) => { let original = el.getAttribute("data-original"); if (!original) { original = el.textContent.trim(); el.setAttribute("data-original", original); } // Skip if no digits in string if (!/\d/.test(original)) return; // Skip non-time strings in .css-v44io0 if (el.classList.contains("css-v44io0") && !/[wdhm]/i.test(original)) { return; } const hours = jiraTimeToHours(original); if (hours == null) return; el.textContent = `${hours}h`; }); } function feature_bailando() { const aEl = document.querySelector( 'a[aria-label="Go to your Jira homepage"]' ); if (aEl) { aEl.removeAttribute("style"); aEl.style.textDecoration = "none"; } const logoWrapper = document.querySelector( 'span[data-testid="atlassian-navigation--product-home--logo--wrapper"]' ); if (!logoWrapper) return; // Remove existing SVG const svg = logoWrapper.querySelector("svg"); if (svg) svg.remove(); // Use flex container logoWrapper.style.display = "flex"; logoWrapper.style.alignItems = "center"; logoWrapper.style.gap = "8px"; // Add GIF only once if (!logoWrapper.querySelector("img.devabit-gif")) { const img = document.createElement("img"); img.src = "https://media.tenor.com/vX-qFMkapQQAAAAj/cat-dancing.gif"; img.className = "devabit-gif"; img.style.height = "32px"; img.style.width = "auto"; img.style.verticalAlign = "middle"; logoWrapper.appendChild(img); } // Add/update Жира span let textSpan = logoWrapper.querySelector("span.devabit-text"); if (!textSpan) { textSpan = document.createElement("span"); textSpan.className = "devabit-text"; textSpan.textContent = "жира :)"; textSpan.style.fontWeight = "700"; textSpan.style.fontFamily = '"Atlassian Sans", Arial, sans-serif'; textSpan.style.fontSize = "14px"; textSpan.style.cursor = "pointer"; textSpan.style.userSelect = "none"; textSpan.style.paddingRight = "6px"; textSpan.style.setProperty("text-decoration", "none", "important"); logoWrapper.appendChild(textSpan); } textSpan.style.color = "#ffffff"; textSpan.style.mixBlendMode = "difference"; } function feature_setupLiveDeadlineCountdown() { const containers = document.querySelectorAll( 'div[data-testid="issue-field-date-time.ui.issue-field-date-time--container"] > div' ); containers.forEach((el) => { if (!deadlineMap.has(el)) { //let original = el.getAttribute("data-original"); //if (!original) { const original = el.textContent.trim(); //el.setAttribute("data-original", original); //} const deadline = parseDateTimeString(original); if (!deadline) return; deadlineMap.set(el, { deadline, original, }); } }); } function feature_updateLiveDeadlineCountdown() { const now = new Date(); deadlineMap.forEach((info, el) => { const msLeft = info.deadline - now; const hoursLeft = msLeft / (1000 * 60 * 60); let label = formatTimeLeft(msLeft); if (isDueDeadlineApply()) { el.innerText = `${info.original}\n(${label})`; el.style.whiteSpace = "pre-line"; el.style.flexDirection = "column"; el.style.gap = "0rem"; el.style.alignItems = "flex-start"; } else { el.textContent = `${info.original}`; el.style.whiteSpace = "normal"; el.style.backgroundColor = ""; return; } if (msLeft <= 0) { el.style.backgroundColor = jiraColors.red; el.style.color = "#ffffff"; } else if (hoursLeft < 0.5) { el.style.backgroundColor = jiraColors.yellow; // yellow el.style.color = "#ffffff"; } else { el.style.backgroundColor = jiraColors.green; // green el.style.color = "#ffffff"; } }); } function getProjectCode() { const heading = document.querySelector( 'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]' ); if (!heading) return null; const title = heading.textContent.trim(); const casePatterns = [ /[A-Z]{2}-[A-Z]{3}\d{7}-\d{3}/g, // AI-DFL2507006-016 /[A-Z]{2}-[A-Z]{3}\d{7}/g, // AI-DFL2507006 /\d{4}\/\d{5}(?:\/#\d+)?/g, // 2025/62049/#2 or 2025/62049 /\b\d{4}\/\d{4}\b/g, // 3411/2025 (including "Quote 3411/2025") ]; for (const pattern of casePatterns) { const matches = title.match(pattern); if (matches?.length) { return matches[matches.length - 1]; } } return null; } function getDealineText() { const deadlineContainer = document.querySelector( 'div[data-testid="issue-field-date-time.ui.issue-field-date-time--container"] > div' ); if (!deadlineContainer) return; const deadlineText = deadlineContainer.textContent.trim(); if (!deadlineText) return; return deadlineText; } function getServerPath() { const description = document.querySelector('[data-testid="issue.views.field.rich-text.description"]')?.innerText || ""; const match = description.match(/M:\\[^\s\n\r]*/i); if (!match) return null; // Remove any trailing word-like junk (like "Instructions" stuck to the path) const cleaned = match[0].replace(/([A-Za-z0-9_-]+)(?=[A-Z][a-z])/, "$1"); // optional: fine-tune return cleaned; } function feature_insertInfoCopyButton() { const projectCode = getProjectCode(); const deadlineText = getDealineText(); const serverPath = getServerPath(); const previousParent = document.querySelector( 'div[data-testid="issue.views.issue-base.context.status-and-approvals-wrapper.status-and-approval"] > div' ); if (!previousParent) return; previousParent.style.paddingLeft = "0"; // ← add this line const container = previousParent.parentElement; if (!container) return; const existing = container.querySelector("#case-id-overlay"); if (existing) existing.remove(); const overlay = document.createElement("div"); overlay.id = "case-id-overlay"; overlay.style.display = "flex"; overlay.style.flexDirection = "column"; overlay.style.gap = "6px"; overlay.style.marginBottom = "12px"; function createRow(buttonText, valueText) { const row = document.createElement("div"); row.style.display = "flex"; row.style.alignItems = "center"; row.style.gap = "8px"; const btn = document.createElement("button"); btn.textContent = buttonText; btn.className = "_mizu194a _1ah31bk5 _ra3xnqa1 _128m1bk5 _1cvmnqa1 _4davt94y _19itglyw _vchhusvi _r06hglyw _80omtlke _2rkosqtm _11c82smr _v5649dqc _189eidpf _1rjc12x7 _1e0c116y _1bsbviql _p12f1osq _kqswh2mm _4cvr1q9y _1bah1h6o _gy1p1b66 _1o9zidpf _4t3iviql _k48p1wq8 _y4tize3t _bozgze3t _y3gn1h6o _s7n4nkob _14mj1kw7 _9v7aze3t _1tv3nqa1 _39yqe4h9 _11fnglyw _18postnw _bfhk1w7a _syaz1gjq _8l3mmuej _aetrb3bt _10531gjq _f8pj1gjq _30l31gjq _9h8h1gjq _irr3166n _1di61dty _4bfu18uv _1hmsglyw _ajmmnqa1 _1a3b18uv _4fprglyw _5goinqa1 _9oik18uv _1bnxglyw _jf4cnqa1 _1nrm18uv _c2waglyw _1iohnqa1"; btn.style.padding = "6px 8px 6px 8px"; btn.style.width = "fit-content"; btn.addEventListener("click", () => { navigator.clipboard.writeText(valueText); }); const text = document.createElement("span"); text.textContent = valueText; text.title = valueText; text.style.userSelect = "text"; text.style.fontWeight = "normal"; text.style.flex = "1"; text.style.whiteSpace = "nowrap"; text.style.overflow = "hidden"; text.style.textOverflow = "ellipsis"; row.appendChild(btn); row.appendChild(text); return row; } if (projectCode) overlay.appendChild(createRow("Copy", projectCode)); if (deadlineText) overlay.appendChild(createRow("Copy", deadlineText)); if (serverPath) overlay.appendChild(createRow("Copy", serverPath)); container.appendChild(overlay); } function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; } function runAllEnhancements() { feature_insertInfoCopyButton(); feature_highlightIfPOisReceived(); feature_bailando(); feature_setupLiveDeadlineCountdown(); feature_updateLiveDeadlineCountdown(); feature_highlightIfMonthBilledIsIncorrect(); feature_updateJiraTime(); } const debouncedUpdate = debounce(runAllEnhancements, 150); //const observer = new MutationObserver(debouncedUpdate); // observer.observe(document.body, { //childList: true, // subtree: true, //}); // window.addEventListener("load", runAllEnhancements); setInterval(runAllEnhancements, 150); // setInterval(feature_updateJiraTime, 150); })();