TGithub

Adds some features on github.com & gitlab.com to integrate it with tecnativa.com

< Feedback on TGithub

Review: Good - script works

§
Posted: 05. 04. 2026

Can you review and update with this code?

// ==UserScript==
// @name           TGithub
// @author         Alexandre Díaz, Carlos Roca, Carlos Dauden
// @version        1.18.0
// @grant          none
// @run-at         document-idle
// @namespace      tecnativa
// @icon           https://www.tecnativa.com/web/image/website/1/favicon/
// @match          *://github.com/*
// @match          *://gitlab.tecnativa.com/*
// @description    Adds some features on github.com & gitlab.com to integrate it with tecnativa.com
// @downloadURL https://update.greasyfork.org/scripts/469159/TGithub.user.js
// @updateURL https://update.greasyfork.org/scripts/469159/TGithub.meta.js
// ==/UserScript==

(function (window) {
    "use strict";

    const TGithub = {
        ODOO_SERVER: 'https://www.tecnativa.com',
        COMPANY_NAME: 'Tecnativa',

        REGEX_TEMPLATES: {},

        initialized: false,
        observer: null,
        navbarObserver: null,

        init() {
            if (this.initialized) return;
            this.initialized = true;

            this._addRegexTemplate(
                'TT',
                /\bTT(\d+)/gi,
                `<a target="_blank" href="${this.ODOO_SERVER}/web#id=$1&model=project.task&view_type=form">${this.COMPANY_NAME}-Task #$1</a>`
            );

            this._replaceTask();

            if (this._isLocationHost('github')) {
                this._ensureGhNavbarButton();
                this._observeNavbar();
            }
        },

        /* ================= CORE ================= */

        _isLocationHost(host) {
            return document.location.host.toLowerCase().includes(host);
        },

        _addRegexTemplate(name, regex, html) {
            this.REGEX_TEMPLATES[name] = { regex, html };
        },

        _executeRegexReplace(templateName, text) {
            const tpl = this.REGEX_TEMPLATES[templateName];
            if (!tpl) return false;

            const result = text.replace(tpl.regex, tpl.html);
            return result !== text ? result : false;
        },

        /* ================= TASK REPLACEMENT ================= */

        _replaceTask() {
            const processNode = (elm) => {
                if (elm.dataset.tgithubProcessed) return;

                const newHTML = this._executeRegexReplace('TT', elm.innerHTML);
                if (newHTML) {
                    elm.innerHTML = newHTML;
                }

                elm.dataset.tgithubProcessed = "1";
            };

            const scan = () => {
                document.querySelectorAll(
                    '.comment-body, .note-text, .description, .commit-description, .js-comment-body, .js-issue-title, .markdown-body'
                ).forEach(processNode);
            };

            // Optimized observer with RAF debounce
            if (!this.observer) {
                let scheduled = false;

                this.observer = new MutationObserver(() => {
                    if (scheduled) return;
                    scheduled = true;

                    requestAnimationFrame(() => {
                        scan();
                        scheduled = false;
                    });
                });

                this.observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            }

            // Initial scan
            scan();
        },

        /* ================= GITHUB NAVBAR ================= */

        _findGhHeaderTarget() {
            return (
                document.querySelector('.AppHeader-globalBar-end') ||
                document.querySelector('div[data-testid="top-bar-actions"]')
            );
        },

        _ensureGhNavbarButton() {
            const target = this._findGhHeaderTarget();
            if (!target) return false;

            if (target.querySelector('#pulls-tecnativa')) return true;

            const menuItem = document.createElement('a');
            menuItem.id = 'pulls-tecnativa';
            menuItem.textContent = this.COMPANY_NAME;
            menuItem.href = `/pulls?q=is%3Aopen+is%3Apr+archived%3Afalse+involves%3A${this.COMPANY_NAME}`;

            const ref =
                target.querySelector("a[href$='/pulls']") ||
                target.querySelector("a[href*='/pulls']") ||
                target.querySelector('a');

            if (ref) {
                menuItem.className = ref.className;
                menuItem.style.cssText = (ref.style.cssText || '') + 'width:auto;';
            } else {
                menuItem.style.cssText = 'margin-right:8px; display:inline-flex; align-items:center;';
            }

            target.insertAdjacentElement('afterbegin', menuItem);
            return true;
        },

        _observeNavbar() {
            if (this.navbarObserver) return;

            this.navbarObserver = new MutationObserver(() => {
                const target = this._findGhHeaderTarget();
                if (!target) return;

                if (!target.querySelector('#pulls-tecnativa')) {
                    this._ensureGhNavbarButton();
                }
            });

            this.navbarObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
        },

        /* ================= SPA HANDLING ================= */

        attachNavigationEvents() {
            // GitHub Turbo navigation (clave)
            document.addEventListener('turbo:load', () => {
                this.initialized = false;
                this.init();
            });

            // Fallback genérico
            window.addEventListener('popstate', () => {
                this.initialized = false;
                this.init();
            });
        }
    };

    /* ================= BOOT ================= */

    const ready = (cb) => {
        if (document.readyState !== 'loading') {
            cb();
        } else {
            document.addEventListener('DOMContentLoaded', cb);
        }
    };

    ready(() => {
        TGithub.init();
        TGithub.attachNavigationEvents();
    });

})(window);

Post reply

Sign in to post a reply.