// ==UserScript== // @name gitlab-booster // @namespace vite-plugin-monkey // @version 1.5.2 // @author // @description Boost productivity for code reviewers on gitlab // @license AGPL-3.0-or-later // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com // @homepage https://github.com/braineo/gitlab-booster#readme // @homepageURL https://github.com/braineo/gitlab-booster#readme // @source https://github.com/braineo/gitlab-booster.git // @supportURL https://github.com/braineo/gitlab-booster/issues // @match https://gitlab.com/* // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @grant GM_addElement // @grant window.onurlchange // @downloadURL https://update.greasyfork.icu/scripts/509169/gitlab-booster.user.js // @updateURL https://update.greasyfork.icu/scripts/509169/gitlab-booster.meta.js // ==/UserScript== (function ($) { 'use strict'; var _GM_addElement = /* @__PURE__ */ (() => typeof GM_addElement != "undefined" ? GM_addElement : void 0)(); const waitForKeyElements = function(selectorOrFunction, callback, waitOnce, interval, maxIntervals) { if (typeof waitOnce === "undefined") { waitOnce = true; } if (typeof interval === "undefined") { interval = 300; } if (typeof maxIntervals === "undefined") { maxIntervals = -1; } if (typeof waitForKeyElements.namespace === "undefined") { waitForKeyElements.namespace = Date.now().toString(); } var targetNodes = typeof selectorOrFunction === "function" ? selectorOrFunction() : document.querySelectorAll(selectorOrFunction); var targetsFound = targetNodes && targetNodes.length > 0; if (targetsFound) { targetNodes.forEach(function(targetNode) { var attrAlreadyFound = `data-userscript-${waitForKeyElements.namespace}-alreadyFound`; var alreadyFound = targetNode.getAttribute(attrAlreadyFound) || false; if (!alreadyFound) { var cancelFound = callback(targetNode); if (cancelFound) { targetsFound = false; } else { targetNode.setAttribute(attrAlreadyFound, "true"); } } }); } if (maxIntervals !== 0 && !(targetsFound && waitOnce)) { maxIntervals -= 1; setTimeout(function() { waitForKeyElements( selectorOrFunction, callback, waitOnce, interval, maxIntervals ); }, interval); } }; _GM_addElement("link", { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/nf-sauce-code-pro@2.1.3/nf-font.min.css" }); const getApiUrl = (url) => { return `${window.location.origin}/api/v4${url}`; }; async function fetchGitLabData(url) { const response = await fetch(url, { headers: { "Content-Type": "application/json" } }); if (!response.ok) { console.error("Failed to fetch GitLab data:", response.statusText); return null; } return await response.json(); } let currentUser; function createThreadsBadge(element, badgeClassName, resolved, resolvable) { const li = $("
  • ").addClass("issuable-comments d-none d-sm-flex").prependTo(element); $("").addClass( `gl-badge badge badge-pill badge-${badgeClassName} sm has-tooltip` ).text(`${resolved}/${resolvable} threads resolved`).prependTo(li); } function createThreadActionBadges(element, action) { const li = $("
  • ").addClass("issuable-comments d-none d-sm-flex").prependTo(element); const createIconText = (icon, title, text, badgeClassName) => { return $("", { title, class: `gl-badge badge badge-pill ${badgeClassName ? `badge-${badgeClassName}` : ""} sm has-tooltip` }).css({ "font-family": "SauceCodePro Mono" }).text(`${icon} ${text ?? ""}`); }; if (action.waitForOursCount) { createIconText( "", "need your response", action.waitForOursCount.toString(), "danger" ).prependTo(li); } if (action.waitForTheirsCount) { createIconText( "", "wait for response", action.waitForTheirsCount.toString(), "muted" ).prependTo(li); } if (action.otherUnresolvedCount) { createIconText( "", "other threads", action.otherUnresolvedCount.toString(), "warning" ).prependTo(li); } if (action.needUserReview) { createIconText("", "need your review", void 0, "danger").prependTo( li ); } } function createDiffStat(element, fileCount, addLineCount, deleteLinCount) { $("
    ").css({ display: "flex", "flex-direction": "row", gap: "3px" }).append( $("
    ", { class: "diff-stats-group" }).append( $("", { class: "gl-text-gray-500 bold", text: `${fileCount} files` }) ), $("
    ", { class: "diff-stats-group gl-display-flex gl-align-items-center bold" }).css({ color: "var(--gl-color-green-600)" }).append($("").text("+"), $("").text(`${addLineCount}`)), $("
    ", { class: "diff-stats-group gl-display-flex gl-align-items-center bold" }).css({ color: "var(--gl-color-red-600)" }).append($("").text("-"), $("").text(`${deleteLinCount}`)) ).prependTo(element); } function createIssueCardMergeRequestInfo(element, opened, total) { const inline = $("").appendTo(element); $("
    ", { class: "issue-milestone-details gl-flex gl-max-w-15 gl-gap-2 gl-mr-3 gl-inline-flex gl-max-w-15 gl-cursor-help gl-items-center gl-align-bottom gl-text-sm gl-text-gray-500" }).append( $("", { title: "Merge requests" }).css({ "font-family": "SauceCodePro Mono", "font-size": "1.1rem" }).text(""), $("", { class: "gl-inline-block gl-truncate gl-font-bold" }).text(total === 0 ? "-/-" : `${total - opened}/${total}`) ).appendTo(inline); } function createIssueCardIterationInfo(element, rollover) { if (rollover < 1) { return; } const inline = $("").appendTo(element); $("
    ", { class: "issue-milestone-details gl-flex gl-max-w-15 gl-gap-2 gl-mr-3 gl-inline-flex gl-max-w-15 gl-cursor-help gl-items-center gl-align-bottom gl-text-sm gl-text-gray-500" }).append( $("", { class: "gl-inline-block gl-truncate gl-font-bold" }).text(`🔄×${rollover}`) ).appendTo(inline); } function ensurePanelLayout() { const layout = document.querySelector("div.layout-page"); if (!layout) { return; } $(layout).css({ display: "flex" }); } function ensureSidePanel(panelName, url) { const buttonId = `close-${panelName.toLowerCase().replaceAll(" ", "-")}`; if (!document.querySelector(`#${buttonId}`)) { const topBar = document.querySelector(".top-bar-container"); if (!topBar) { return; } $(topBar).append( $("
    ` ); $description.find(".gl-button").on("click", async () => { const urlMatch = window.location.pathname.match( /\/(.+)\/-\/merge_requests\/(\d+)/ ); if (!urlMatch) { console.error("Could not parse MR URL"); return; } const projectPath = urlMatch[1]; const mrIid = urlMatch[2]; const title2 = getTitle(); await fetch( getApiUrl( `/projects/${encodeURIComponent(projectPath)}/merge_requests/${mrIid}` ), { method: "PUT", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, body: JSON.stringify({ title: `Draft: ${title2}` }) } ); window.location.reload(); }); $description.appendTo(reviewerPanel); } } async function enhanceIssueDetailPage() { waitForKeyElements( "#developmentitems > div.crud-body > div > ul > li", (mergeRequest) => { (async () => { console.debug( "inserting merge request meta to related merge requests", mergeRequest ); const statusBadge = mergeRequest.querySelector( "div.item-meta span.gl-badge-content" ); const mergeRequestStatus = (statusBadge == null ? void 0 : statusBadge.textContent) ?? "opened"; const mergeRequestAnchor = mergeRequest.querySelector(".item-title a"); const mergeRequestUrl = mergeRequestAnchor == null ? void 0 : mergeRequestAnchor.href; if (!mergeRequestUrl) { return; } switch (mergeRequestStatus == null ? void 0 : mergeRequestStatus.trim().toLowerCase()) { case "merged": { break; } case "closed": { $(mergeRequestAnchor).css({ "text-decoration": "line-through" }); $(mergeRequest).css({ filter: "grayscale(1)" }); return; } default: { $(mergeRequestAnchor).css({ color: "var(--primary)" }); } } const diffsMeta = await fetchGitLabData( `${mergeRequestUrl}/diffs_metadata.json` ); if (!diffsMeta) { return; } const metaDiv = mergeRequest.querySelector( ".item-meta .item-attributes-area" ); if (!metaDiv) { return; } createOpenModalButton(mergeRequestUrl).appendTo(metaDiv); if (mergeRequestStatus === "opened") { await addMergeRequestThreadMeta(metaDiv, mergeRequestUrl); await addMergeRequestDiffMeta(metaDiv, mergeRequestUrl); } $("").text(diffsMeta.project_path).prependTo(metaDiv); })(); }, true ); } function enhanceIssueList() { ensurePanelLayout(); waitForKeyElements("ul.issues-list > li", (issue) => { var _a; const issueUrl = (_a = issue.querySelector("a")) == null ? void 0 : _a.href; if (!issueUrl) { console.error("cannot find url for issue"); return; } $(issue).on("click", () => { ensureSidePanel("Issue Panel", issueUrl); }); return true; }); } const enhanceIssueCard = async (mutationList) => { var _a; for (const mutation of mutationList) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node instanceof Element && node.matches("li.board-card")) { const issueUrl = (_a = node.querySelector( "h3.board-card-title > a" )) == null ? void 0 : _a.href; const infoItems = node.querySelector( "span.board-info-items" ); if (!issueUrl || !infoItems) { continue; } const issue = await fetchGitLabData(`${issueUrl}.json`); if (!issue) { continue; } const relatedMergeRequest = await fetchGitLabData( getApiUrl( `/projects/${issue.project_id}/issues/${issue.iid}/related_merge_requests` ) ) ?? []; const total = relatedMergeRequest.length; const opened = relatedMergeRequest.filter( (mergeRequest) => mergeRequest.state === "opened" ).length; createIssueCardMergeRequestInfo(infoItems, opened, total); const iterationEvents = await fetchGitLabData( getApiUrl( `/projects/${issue.project_id}/issues/${issue.iid}/resource_iteration_events` ) ) ?? []; createIssueCardIterationInfo( infoItems, iterationEvents.filter((event) => event.action === "add").length - 1 ); } } } else if (mutation.type === "attributes") ; } return; }; const observer = new MutationObserver(enhanceIssueCard); const enhanceIssueBoard = () => { observer.disconnect(); const boardElement = document.querySelector(".boards-list"); if (!boardElement) { return; } observer.observe(boardElement, { attributes: false, childList: true, subtree: true }); }; const issueDetailRegex = /\/issues\/\d+/; const mergeRequestDetailRegex = /\/merge_requests\/(\d+)/; const mergeRequestListRegex = /\/merge_requests(?!\/\d+)/; const epicListRegex = /\/epics(?!\/\d+)/; const issueBoardRegex = /\/boards(?:\/\d+)?(?:\/)?(?:\?|$)/; const enhance = () => { if (mergeRequestListRegex.test(window.location.href)) { enhanceMergeRequestList(); } if (mergeRequestDetailRegex.test(window.location.href)) { enhanceMergeRequestDetailPage(); } if (issueDetailRegex.test(window.location.href)) { enhanceIssueDetailPage(); } if (epicListRegex.test(window.location.href)) { enhanceIssueList(); } if (issueBoardRegex.test(window.location.href)) { enhanceIssueBoard(); } }; window.onload = enhance; if (window.onurlchange === null) { window.addEventListener("urlchange", enhance); } })($);