AzDO PR dashboard improvements

Adds sorting and categorization to the PR dashboard.

Versione datata 13/04/2019. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==

// @name         AzDO PR dashboard improvements
// @version      2.7.2
// @author       National Instruments
// @description  Adds sorting and categorization to the PR dashboard.
// @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/underscore.js/1.9.1/underscore-min.js#sha256-G7A4JrJjJlFqP0yamznwPjAApIKPkadeHfyIwiaa9e0=

// ==/UserScript==

// Update if we notice new elements being inserted into the DOM. This happens when AzDO loads the PR dashboard. Debounce new elements by a short time, in case they are being added in a batch.
$(document).bind('DOMNodeInserted', _.debounce(() => {
    // If we're on a pull request page, attempt to sort it.
    if(/\/(_pulls|pullrequests)/i.test(window.location.pathname)) {
        sortPullRequestDashboard();
    }
}, 500));

function sortPullRequestDashboard() {
    // Find the reviews section for this user.
    var myReviews = $("[aria-label='Assigned to me'][role='region']");
    if (myReviews.length == 0) {
         // We're on the overall dashboard (e.g. https://dev.azure.com/*/_pulls) which has a different HTML layout...
         myReviews = $("[aria-label='Assigned to me']").parent();
    }
    if (myReviews.length == 0) {
        // We are not on a page that has a PR dashboard.
        console.log("No PR dashboard found at: " + window.location);
        return;
    }

    // Don't update if we see evidence of us having run.
    if (myReviews.attr('data-reviews-sorted') == 'true') {
        return;
    }
    myReviews.attr('data-reviews-sorted', 'true');

    // Sort the reviews in reverse; aka. show oldest reviews first then newer reviews.
    myReviews.append(myReviews.find("[role='listitem']").get().reverse());

    // Create review sections with counters.
    myReviews.append("<details class='reviews-incomplete-blocked' style='display: none; margin: 10px 30px' open><summary style='padding: 10px; cursor: pointer; color: var(--text-secondary-color)'>Incomplete but blocked (<span class='review-subsection-counter'>0</span>)</summary></details>");
    myReviews.append("<details class='reviews-drafts' style='display: none; margin: 10px 30px' open><summary style='padding: 10px; cursor: pointer; color: var(--text-secondary-color)'>Drafts (<span class='review-subsection-counter'>0</span>)</summary></details>");
    myReviews.append("<details class='reviews-waiting' style='display: none; margin: 10px 30px'><summary style='padding: 10px; cursor: pointer; color: var(--text-secondary-color)'>Completed as Waiting on Author (<span class='review-subsection-counter'>0</span>)</summary></details>");
    myReviews.append("<details class='reviews-rejected' style='display: none; margin: 10px 30px'><summary style='padding: 10px; cursor: pointer; color: var(--text-secondary-color)'>Completed as Rejected (<span class='review-subsection-counter'>0</span>)</summary></details>");
    myReviews.append("<details class='reviews-approved' style='display: none; margin: 10px 30px'><summary style='padding: 10px; cursor: pointer; color: var(--text-secondary-color)'>Completed as Approved / Approved with Suggestions (<span class='review-subsection-counter'>0</span>)</summary></details>");

    // If we have browser local storage, we can save the open/closed setting of these subsections.
    if (localStorage) {
        // Load the subsection open/closed setting if it exists.
        myReviews.children("details").each((index, item) => {
            var detailsElement = $(item);
            var isSubsectionOpen = localStorage.getItem(`userscript/azdo-pr-dashboard/is-subsection-open/${detailsElement.attr('class')}`);
            if (isSubsectionOpen == 1) {
                detailsElement.attr('open', 'open');
            } else if (isSubsectionOpen == 0) {
                detailsElement.removeAttr('open');
            }
        });

        // Save the subsection open/closed setting on toggle.
        myReviews.children("details").on("toggle", (e) => {
            var detailsElement = $(e.target);
            localStorage.setItem(`userscript/azdo-pr-dashboard/is-subsection-open/${detailsElement.attr('class')}`, detailsElement.attr('open') == 'open' ? 1 : 0);
        });
    }

    // Because of CORS, we need to make sure we're querying the same hostname for our AzDO APIs.
    var apiUrlPrefix;
    if (window.location.hostname == 'dev.azure.com') {
        apiUrlPrefix = `https://${window.location.hostname}${window.location.pathname.match(/^\/.*?\//ig)[0]}`;
    } else {
        apiUrlPrefix = `https://${window.location.hostname}`;
    }

    // Find the user's name.
    var me = $(".vss-Persona").attr("aria-label");

    // Loop through the PRs that we've voted on.
    $(myReviews).find(`[role="listitem"]`).each((index, item) => {
        var row = $(item);
        if (row.length == 0) {
            return;
        }

        // Get the PR id.
        var pullRequestUrl = row.find("a[href*='/pullrequest/']").attr('href');
        if (pullRequestUrl == undefined) {
            return;
        }
        var pullRequestId = pullRequestUrl.substring(pullRequestUrl.lastIndexOf('/') + 1);

        // Hide the row while we are updating it.
        row.hide(150);

        // Get complete information about the PR.
        // See: https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull%20requests/get%20pull%20request%20by%20id?view=azure-devops-rest-5.0
        $.ajax({
            url: `${apiUrlPrefix}/_apis/git/pullrequests/${pullRequestId}?api-version=5.0`,
            type: 'GET',
            cache: false,
            success: (pullRequestInfo) => {
                // AzDO has returned with info on this PR.

                var missingVotes = 0;
                var waitingOrRejectedVotes = 0;
                var neededVotes = 0;
                var myVote = 0;

                // Count the number of votes.
                $.each(pullRequestInfo.reviewers, function(i, reviewer) {
                    neededVotes++;
                    if (reviewer.displayName == me) {
                        myVote = reviewer.vote;
                    }
                    if (reviewer.vote == 0) {
                        missingVotes++;
                    }
                    if (reviewer.vote < 0) {
                        waitingOrRejectedVotes++;
                    }
                });

                // See what section this PR should be filed under and style the row, if necessary.
                var subsection = "";
                if (pullRequestInfo.isDraft) {
                    subsection = '.reviews-drafts';
                } else if (myVote == -5) {
                    subsection = '.reviews-waiting';
                } else if (myVote < 0) {
                    subsection = '.reviews-rejected';
                } else if (myVote > 0) {
                    subsection = '.reviews-approved';
                } else {
                    if (waitingOrRejectedVotes > 0) {
                        subsection = '.reviews-incomplete-blocked';
                    } else if (missingVotes == 1) {
                        row.css('background', 'rgba(256, 0, 0, 0.3)');
                    }
                }

                // If we identified a section, move the row.
                if (subsection) {
                    var completedSection = myReviews.children(subsection);
                    completedSection.find('.review-subsection-counter').text(function(i, value) { return +value + 1 });
                    completedSection.find('.review-subsection-counter').removeClass('empty');
                    completedSection.css('display', 'block');
                    completedSection.append(row);
                }
            },
            error: (jqXHR, exception) => {
                console.log(`Error at PR ${pullRequestId}: ${jqXHR.responseText}`);
            },
            complete: (jqXHR, status) => {
                // Show the row when we're done processing it, whether it resulting in an error or not.
                row.show(150);
            }
        });
    });
}