// ==UserScript== // @name AzDO Pull Request Improvements // @version 2.17.0 // @author National Instruments // @description Adds sorting and categorization to the PR dashboard. Also adds minor improvements to the PR diff experience, such as a base update selector and per-file checkboxes. // @license MIT // @namespace https://ni.com // @homepageURL https://github.com/alejandro5042/azdo-userscripts // @supportURL https://github.com/alejandro5042/azdo-userscripts // @contributionURL https://github.com/alejandro5042/azdo-userscripts // @include https://dev.azure.com/* // @include https://*.visualstudio.com/* // @run-at document-start // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js#sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8= // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-once/2.2.3/jquery.once.min.js#sha256-HaeXVMzafCQfVtWoLtN3wzhLWNs8cY2cH9OIQ8R9jfM= // @require https://cdnjs.cloudflare.com/ajax/libs/lscache/1.3.0/lscache.js#sha256-QVvX22TtfzD4pclw/4yxR0G1/db2GZMYG9+gxRM9v30= // @require https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js#sha256-wCBClaCr6pJ7sGU5kfb3gQMOOcIZNzaWpWcj/lD9Vfk= // @require https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js#sha256-7/yoZS3548fXSRXqc/xYzjsmuW3sFKzuvOCHd06Pmps= // @downloadURL none // ==/UserScript== (function () { 'use strict'; // Set a namespace for our local storage items. lscache.setBucket('acb-azdo/'); // Call our event handler if we notice new elements being inserted into the DOM. This happens as the page is loading or updating dynamically based on user activity. We throttle new element events to avoid using up CPU when AzDO is adding a lot of elements during a short time (like on page load). document.addEventListener('DOMNodeInserted', _.throttle(onPageDOMNodeInserted, 400)); // This is "main()" for this script. Runs periodically when the page updates. function onPageDOMNodeInserted(event) { // The page may not have refreshed when moving between URLs--sometimes AzDO acts as a single-page application. So we must always check where we are and act accordingly. if (/\/(pullrequest)\//i.test(window.location.pathname)) { addCheckboxesToFiles(); addBaseUpdateSelector(); } else if (/\/(_pulls|pullrequests)/i.test(window.location.pathname)) { sortPullRequestDashboard(); } } // The func we'll call to continuously add checkboxes to the PR file listing, once initialization is over. let addCheckboxesToNewFilesFunc = () => { }; // If we're on specific PR, add checkboxes to the file listing. function addCheckboxesToFiles() { $('.vc-sparse-files-tree').once('add-checkbox-support').each(async function () { addCheckboxesToNewFilesFunc = () => { }; const filesTree = $(this); addStyleOnce('pr-file-checbox-support-css', ` button.file-complete-checkbox { /* Make a checkbox out of a button. */ cursor: pointer; width: 15px; height: 15px; line-height: 15px; margin: -3px 8px 0px 0px; padding: 0px; background: var(--palette-black-alpha-6); border-radius: 3px; border: 1px solid var(--palette-black-alpha-10); vertical-align: middle; display: inline-block; font-size: 0.75em; text-align: center; color: var(--text-primary-color); } button.file-complete-checkbox:hover { /* Make a checkbox out of a button. */ background: var(--palette-black-alpha-10); } button.file-complete-checkbox.checked:after { /* Make a checkbox out of a button. */ content: "✔"; }`); // Get the current iteration of the PR. const iterations = await getPullRequestIterations(); const currentPullRequestIteration = iterations.length; // Get the current checkbox state for the PR at this URL. const checkboxStateId = `pr-file-iteration6/${window.location.pathname}`; // Stores the checkbox state for the current page. A map of files => iteration it was checked. const filesToIterationReviewed = lscache.get(checkboxStateId) || {}; // Handle clicking on file checkboxes. filesTree.on('click', 'button.file-complete-checkbox', function (event) { const checkbox = $(this); // Toggle the look of the checkbox. checkbox.toggleClass('checked'); // Save the iteration number the file was checked in our map. To save space, if it is unchecked, simply remove the entry. if (checkbox.hasClass('checked')) { filesToIterationReviewed[checkbox.attr('name')] = currentPullRequestIteration; } else { delete filesToIterationReviewed[checkbox.attr('name')]; } // Save the current checkbox state to local storage. lscache.set(checkboxStateId, filesToIterationReviewed, 60 * 24 * 21); // Stop the click event here to avoid the checkbox click from selecting the PR row underneath, which changes the active diff in the right panel. event.stopPropagation(); }); addCheckboxesToNewFilesFunc = () => $('.vc-sparse-files-tree .vc-tree-cell').once('add-complete-checkbox').each(function () { const fileCell = $(this); const fileRow = fileCell.closest('.tree-row'); const typeIcon = fileRow.find('.type-icon'); // Don't put checkboxes on rows that don't represent files. if (!/bowtie-file\b/i.test(typeIcon.attr('class'))) { return; } const name = fileCell.attr('content'); // The 'content' attribute contains the file operation; e.g. "/src/file.cs [edit]". const iteration = filesToIterationReviewed[name] || 0; // Create the checkbox before the type icon. $('