您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jira enhancements.
// ==UserScript== // @name Devabit Jira+ // @namespace http://tampermonkey.net/ // @version 2.5.7 // @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) { 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 === "Scheduled") ); } function feature_highlighIfDeadlineAndDueDateMismatch() { if (!isDueDeadlineApply()) return; const dueDateContainer = document.querySelector( 'div[data-testid="coloured-due-date.ui.colored-due-date-container"]' ); const dueDateSpan = document.querySelector( 'div[data-testid="coloured-due-date.ui.colored-due-date-container"] > span' ); if (!dueDateSpan) return; const officialDate = parseDateString(dueDateSpan.textContent.trim()); if (!officialDate) return; deadlineMap.forEach((info, el) => { const originalDate = parseDateString(info.original); if (!datesMatch(originalDate, officialDate)) { dueDateContainer.style.backgroundColor = jiraColors.red; // red highlight } else { dueDateContainer.style.backgroundColor = ""; // clear highlight if matches } }); } 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 fature_updateJiraTime() { const selectors = [ ".css-v44io0", '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 non-time strings in css-v44io0 if (el.classList.contains("css-v44io0") && !/[wdhm]/i.test(original)) return; const hours = jiraTimeToHours(original); el.textContent = `${hours}h`; // Save estimate if it’s the main estimate field if (el.classList.contains("css-v44io0")) { estimatedHoursMap.set("estimate", hours); } }); } 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-inline-edit-read-view-container.ui.container"]' ); containers.forEach((el) => { if (!deadlineMap.has(el)) { let original = el.getAttribute("data-original"); if (!original) { 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.red; // red el.style.color = "#ffffff"; } else { el.style.backgroundColor = jiraColors.green; // green el.style.color = "#ffffff"; } }); } // function findFolderPathMatchingProjectCode() { // 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, // full code with -xxx // /[A-Z]{2}-[A-Z]{3}\d{7}/g, // code without suffix // /\d{4}\/\d{5,6}(?:\/#\d+)?/g, // ]; // let fullProjectCode = null; // for (const pattern of casePatterns) { // const matches = title.match(pattern); // if (matches?.length) { // fullProjectCode = matches[matches.length - 1]; // break; // } // } // if (!fullProjectCode) return null; // // Remove invalid Windows path chars // const cleanedCode = fullProjectCode.replace(/[<>:"/\\|?*]/g, ""); // const baseCode = cleanedCode.replace(/-\d{3}$/, ""); // strip -xxx if present // const escapedBase = baseCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // const pattern = new RegExp( // `M:\\\\[^\\s]*${escapedBase}(?:-\\d{3})?[^\\s]*`, // "gi" // ); // const root = document.querySelector("div.ak-renderer-document"); // if (!root) return null; // const elements = root.querySelectorAll("p, span, div"); // for (const el of elements) { // const text = el.innerText.trim(); // const match = text.match(pattern); // if (match?.length) { // return match[0]; // Return first matched path // } // } // return null; // } // function insertCaseIdFromTitle() { // const heading = document.querySelector( // 'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]' // ); // if (!heading) return; // const title = heading.textContent.trim(); // const casePatterns = [ // /[A-Z]{2}-[A-Z]{3}\d{7}-\d{3}/g, // /[A-Z]{2}-[A-Z]{3}\d{7}/g, // code without suffix // /\d{4}\/\d{5,6}(?:\/#\d+)?/g, // ]; // let match = null; // for (const pattern of casePatterns) { // const matches = title.match(pattern); // if (matches && matches.length) { // match = matches[matches.length - 1]; // break; // } // } // if (!match) return; // // Remove previous overlay if exists // const existing = document.getElementById("case-id-overlay"); // if (existing) existing.remove(); // const deadlineEl = [...deadlineMap.keys()][0]; // const deadlineText = deadlineEl?.getAttribute("data-original") || ""; // const estimateEl = document.querySelector( // 'div[data-testid="issue-field-inline-edit-read-view-container.ui.container"] > span > span' // ); // const estimateText = estimateEl?.textContent.trim() || ""; // const container = document.createElement("div"); // container.id = "case-id-overlay"; // Object.assign(container.style, { // position: "fixed", // bottom: "0", // left: "0", // zIndex: "99999", // backgroundColor: "#000", // color: "#fff", // padding: "6px 14px 6px 6px", // fontSize: "14px", // fontFamily: "Arial, sans-serif", // opacity: "0.95", // borderTopRightRadius: "4px", // userSelect: "text", // }); // const table = document.createElement("table"); // Object.assign(table.style, { // borderCollapse: "collapse", // width: "100%", // }); // function createRow(buttonText, valueText) { // const tr = document.createElement("tr"); // const tdBtn = document.createElement("td"); // const btn = document.createElement("button"); // btn.textContent = buttonText; // Object.assign(btn.style, { // background: "#444", // color: "#fff", // border: "none", // borderRadius: "2px", // padding: "2px 8px", // cursor: "pointer", // fontSize: "13px", // userSelect: "none", // whiteSpace: "nowrap", // }); // btn.addEventListener("click", () => { // navigator.clipboard.writeText(valueText); // btn.textContent = "Done"; // setTimeout(() => { // btn.textContent = buttonText; // }, 1000); // }); // tdBtn.appendChild(btn); // tdBtn.style.verticalAlign = "middle"; // const tdVal = document.createElement("td"); // tdVal.textContent = valueText; // tdVal.style.fontWeight = "normal"; // tdVal.style.userSelect = "text"; // tdVal.style.verticalAlign = "middle"; // tdVal.style.padding = "0px"; // tr.appendChild(tdBtn); // tr.appendChild(tdVal); // return tr; // } // table.appendChild(createRow("Copy", match)); // if (deadlineText) table.appendChild(createRow("Copy", deadlineText)); // if (estimateText) table.appendChild(createRow("Copy", `${estimateText}`)); // const folder = findFolderPathMatchingProjectCode(); // if (folder) table.appendChild(createRow("Copy", folder)); // container.appendChild(table); // document.body.appendChild(container); // } function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; } function isBrowsePage() { return /\/browse\/[A-Z]+-\d+/i.test(location.pathname); } function removeOverlayIfNotBrowse() { if (!isBrowsePage()) { // overlays to hide // document.getElementById("case-id-overlay")?.remove(); // document.getElementById('jira-top-overlay')?.remove(); } } window.addEventListener("popstate", removeOverlayIfNotBrowse); window.addEventListener("hashchange", removeOverlayIfNotBrowse); function runAllEnhancements() { // if (!isBrowsePage()) { // removeOverlayIfNotBrowse(); // } fature_updateJiraTime(); feature_bailando(); feature_setupLiveDeadlineCountdown(); feature_updateLiveDeadlineCountdown(); feature_highlightIfMonthBilledIsIncorrect(); feature_highlighIfDeadlineAndDueDateMismatch(); } const debouncedUpdate = debounce(runAllEnhancements, 300); const observer = new MutationObserver(debouncedUpdate); observer.observe(document.body, { childList: true, subtree: true, }); window.addEventListener("load", runAllEnhancements); setInterval(feature_updateLiveDeadlineCountdown, 1000); })();