Azure DevOps Pull Request Filter

Filter pull Requests in Azure DevOps by tags

// ==UserScript==
// @name         Azure DevOps Pull Request Filter
// @namespace    https://github.com/calne-ca
// @version      0.2.1
// @description  Filter pull Requests in Azure DevOps by tags
// @author       Joscha Düringer
// @license      MIT
// @supportURL   https://github.com/calne-ca/tampermonkey-azure-devops-pull-request-tag-filter
// @match        https://dev.azure.com/*/pullrequests*
// @icon         https://www.google.com/s2/favicons?domain=dev.azure.com
// @grant        none
// ==/UserScript==

function removePullRequest(pullRequest){
    console.log("Removing pull request: " + pullRequest);
    pullRequest.remove();
}

function adjustRowSpacers() {
    const rowSpacers = document.getElementsByClassName('bolt-list-cell-spacer');

    for(const rowSpacer of rowSpacers){
        rowSpacer.style.height = 0;
    }
}

function stringMatchesFilter(str, filter) {
    return filter.some(filterStr=> new RegExp("^" + filterStr + "$").test(str));
}

function pullRequestHasMatchingTag(pullRequest, filteredTags, showUntagged) {
    if(filteredTags.length === 0) {
        return true;
    }

    const labels = pullRequest.querySelectorAll('.bolt-pill-overflow .text-ellipsis');
    const pullRequestLabels = []

    if(labels.length === 0 && showUntagged) {
        return true;
    }

    for (const label of labels) {
        pullRequestLabels.push(label.textContent);
    }

    return pullRequestLabels.some(tag=> stringMatchesFilter(tag, filteredTags));
}

function pullRequestHasMatchingTitle(pullRequest, filteredTitles) {
    if(filteredTitles.length === 0){
        return true;
    }

    const title = pullRequest.querySelector('.body-l').textContent;

    return stringMatchesFilter(title, filteredTitles);
}

function pullRequestHasAssignee(pullRequest){
    const assignees = pullRequest.querySelectorAll('.bolt-table-cell-content .flex-center img');
    return assignees.length > 0;
}

function pullRequestShouldBeRemoved(pullRequest, filteredTags, showUntagged, filteredTitles, unassignedOnly) {
    return !pullRequestHasMatchingTag(pullRequest, filteredTags, showUntagged) ||
        !pullRequestHasMatchingTitle(pullRequest, filteredTitles) ||
        (unassignedOnly && pullRequestHasAssignee(pullRequest));
}

function filterPullRequests(pullRequests, filteredTags, showUntagged, filteredTitles, unassignedOnly){
    for (const pullRequest of pullRequests) {
        if (pullRequestShouldBeRemoved(pullRequest, filteredTags, showUntagged, filteredTitles, unassignedOnly)) {
            removePullRequest(pullRequest)
        }
    }

    adjustRowSpacers();
}

(function() {
    'use strict';

    const urlParams = new URLSearchParams(window.location.search);
    const tagFilterParam = urlParams.get('tagFilter');
    const titleFilterParam = urlParams.get('titleFilter');
    const showUntaggedParam = urlParams.get('showUntagged');
    const unassignedOnlyParam = urlParams.get('unassignedOnly');
    const refreshInterval = 200;

    const filteredTags = tagFilterParam ? tagFilterParam.split(',') : [];
    const filteredTitles = titleFilterParam ? titleFilterParam.split(',') : [];
    const showUntagged = showUntaggedParam === 'true'
    const unassignedOnly = unassignedOnlyParam === 'true'

    if(filteredTags.length === 0 && filteredTitles.length === 0 && !unassignedOnly) {
        return;
    }

    console.log("Filtering pull requests with filteredTags='" + filteredTags + "', showUntagged='" + showUntagged + "', filteredTitles='" + filteredTitles + "', unassignedOnly='" + unassignedOnly + "'");

    setInterval(function() {
        const path = window.location.pathname.replace(/\/$/, "");

        if(!path.endsWith('pullrequests')) {
            return;
        }

        const pullRequests = document.getElementsByClassName('bolt-list-row');

        if (pullRequests) {
            filterPullRequests(pullRequests, filteredTags, showUntagged, filteredTitles, unassignedOnly);
        }
    }, refreshInterval);
})();