IgorVodka / Velvica Jira Improvements

// ==UserScript==
// @name         Velvica Jira Improvements
// @namespace    https://jira.citilink.ru/
// @version      0.3.0
// @description  makes Jira better!
// @author       Velvica
// @match        https://jira.citilink.ru/*
// @match        https://github.com/rentsoft/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

if (window.location.hostname.includes('jira')) {
    const WATCHED_IDS = ['commentLevel-multi-select', 'assignee-single-select', 'customfield_15705-form'];
    const VELVICA_USER_SELECTOR_NAME = '_velvica_user_selector';

    const userMapping = [
        { velvicaName: 'abo', fullName: 'Александр Богданов', jiraName: 'albogdanov' },
        { velvicaName: 'dsi', fullName: 'Дмитрий Сизоненко', jiraName: 'sizonenko' },
        { velvicaName: 'dtk', fullName: 'Дмитрий Ткачук', jiraName: 'tkachuk.d' },
        { velvicaName: 'igv', fullName: 'Игорь Водка', jiraName: 'vodka.i' },
        { velvicaName: 'iik', fullName: 'Ильшат Иксанов', jiraName: 'iksanov.i' },
        { velvicaName: 'mzu', fullName: 'Маргарита Зуйкова', jiraName: 'zuykova.m' },
        { velvicaName: 'miv', fullName: 'Мария Иваненко', jiraName: 'ivanenko.m' },
        { velvicaName: 'mno', fullName: 'Марк Норенберг', jiraName: 'norenberg' },
        { velvicaName: 'mgr', fullName: 'Михаил Греков', jiraName: 'grekov.m' },
        { velvicaName: 'nku', fullName: 'Никита Куличков', jiraName: 'kulichkovn' },
        { velvicaName: 'nmi', fullName: 'Наталья Мистюкова', jiraName: 'mistyukova' },
        { velvicaName: 'rsv', fullName: 'Рустам Шаджалилов', jiraName: 'rustam.sh' },
        { velvicaName: 'tne', fullName: 'Татьяна Невская', jiraName: 'nevskaya.t' },
        { velvicaName: 'van', fullName: 'Вадим Андриенко', jiraName: 'andrienko' },
    ];

    (function() {
        'use strict';

        const jiraNode = document.getElementById('jira');
        const observerConfig = { childList: true, subtree: true };

        const callback = function(mutationsList, observer) {
            for(const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const nodes = Array.from(mutation.addedNodes);

                    if (nodes.some(node => WATCHED_IDS.includes(node.id))) {
                        // Jira is laggy. Wait just in case.
                        setTimeout(() => hookVelvicaUsers(), 300);
                    }
                }
            }
        };

        // Create an observer instance linked to the callback function
        const observer = new MutationObserver(callback);
        observer.observe(jiraNode, observerConfig);

        function hookVelvicaUsers() {
            const velvicaUserSelector = document.createElement('select');
            const usersField = document.getElementById('customfield_15705');

            if (usersField === null) {
                return;
            }

            if (Array.from(usersField.parentNode.childNodes).some(node => node.name === VELVICA_USER_SELECTOR_NAME)) {
                // Do not duplicate.
                return true;
            }

            velvicaUserSelector.onchange = function () {
                const foundUser = userMapping.find(user => user.velvicaName === this.value);
                if (foundUser === null) {
                    return true;
                }

                const { jiraName } = foundUser;
                const formerUsers = usersField.value.split(',')
                .map(userName => userName.trim())
                .filter(userName => userName.length > 0);

                const onlyUnique = (value, index, self) => self.indexOf(value) === index;
                usersField.value = [...formerUsers, jiraName].filter(onlyUnique).join(', ');
                velvicaUserSelector.selectedIndex = 0;
            };

            velvicaUserSelector.name = VELVICA_USER_SELECTOR_NAME;
            velvicaUserSelector.style.display = 'block';
            velvicaUserSelector.style.fontSize = '13px';
            velvicaUserSelector.style.fontFamily = 'monospace';
            velvicaUserSelector.style.padding = '5px 10px';
            velvicaUserSelector.style.backgroundImage = 'none';
            velvicaUserSelector.style.backgroundColor = '#ebecf0';
            velvicaUserSelector.style.border = 'none';
            velvicaUserSelector.style.borderRadius = '3px';
            velvicaUserSelector.style.marginBottom = '5px';

            const firstOption = document.createElement('option');
            firstOption.text = 'Добавить ответственного';
            velvicaUserSelector.add(firstOption);

            for (const user of userMapping) {
                const option = document.createElement('option');
                option.value = user.velvicaName;
                option.text = `${user.velvicaName} | ${user.fullName}`;
                velvicaUserSelector.add(option);
            }

            usersField.insertAdjacentElement('beforebegin', velvicaUserSelector);
        }
    })();
} else if (window.location.hostname.includes('github')) {
    // Must be idempotent.
    const apply = () => {
        const JIRA_LINK = 'https://jira.citilink.ru/browse/VEL-$1';
        const MERGE_WARNING_LABELS = ['not yet', 'not-yet', 'dont-merge'];

        // PR title
        const title = document.querySelector('.js-issue-title');
        if (title && !title.innerHTML.includes('<a')) {
            title.innerHTML = title.innerHTML.replaceAll(
                /VEL-([0-9]{5,6})/g,
                '<a target="_blank" title="Открыть задачу в Jira" href="' + JIRA_LINK + '">VEL-$1</a>'
            );
        }

        // PR labels
        const labels = [...document.querySelectorAll('.js-issue-labels a')].map(label => label.getAttribute('data-name'));
        const hasNoMergeLabels = labels.some(label => MERGE_WARNING_LABELS.includes(label.toLowerCase()));
        const mergeBox = document.querySelector('.js-merge-box');
        const addedWarning = () => mergeBox ? mergeBox.querySelector('.velvica-warning') : null;

        if (!addedWarning() && hasNoMergeLabels) {
            const retries = 10;
            for (let i = 0; i < retries; i++) {
                const timeout = setTimeout(() => {
                    if (mergeBox && !addedWarning()) {
                        mergeBox.innerHTML = '<p class="velvica-warning"><strong>⚠️ Этот PR не нужно мержить.</strong> Обратите внимание на метки.</p>'
                            + mergeBox.innerHTML;
                        clearTimeout(timeout);
                    }
                }, 1000 * i);
            }
        } else if (addedWarning() && !hasNoMergeLabels) {
            // If the label has been removed.
            document.querySelector('.velvica-warning').remove();
        }
    };

    apply();
    setInterval(apply, 2500);
}