Favro - Toggl Timer

Start Toggl Timer when viewing a Favro ticket

当前为 2020-03-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Favro - Toggl Timer
// @namespace    https://www.gotom.io/
// @version      0.7
// @license      MIT
// @author       Mike Meier
// @match        https://favro.com/*
// @match        https://www.toggl.com/api/*
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// @description Start Toggl Timer when viewing a Favro ticket
// ==/UserScript==
(function ($) {
    const FAVRO_EMAIL_KEY_NAME = 'favro_email';
    const FAVRO_API_KEY_NAME = 'favro_api_key';
    const FAVRO_TICKET_PREFIX_KEY_NAME = 'favro_ticket_prefix';
    const FAVRO_ORGANIZATION_ID_KEY_NAME = 'favro_organization_id';
    const FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME = 'favro_pid_custom_field_id';

    const TOGGL_API_KEY_NAME = 'toggl_api_key';
    const TOGGL_DEFAULT_PID_KEY_NAME = 'toggl_default_pid';

    const ENV_VALUES = [
        FAVRO_EMAIL_KEY_NAME,
        FAVRO_API_KEY_NAME,
        FAVRO_TICKET_PREFIX_KEY_NAME,
        FAVRO_ORGANIZATION_ID_KEY_NAME,
        FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME,

        TOGGL_API_KEY_NAME,
        TOGGL_DEFAULT_PID_KEY_NAME
    ];

    const TIMEOUT_BEFORE_TRACKING = 5000;

    function start() {
        $.noConflict();
        ensureEnvironmentVariables();
        window.setInterval(detectOpenCardChanges(onOpenCardChange($)), 1000);
        window.onbeforeunload = stopTimeEntry;
    }

    function ensureEnvironmentVariables() {
        ENV_VALUES.forEach(async key => {
            await GM.getValue(key) || GM.setValue(key, prompt(key));
        });
    }

    function detectOpenCardChanges(onChange) {
        let currentOpenCardId = null;
        return async () => {
            const ticketPrefix = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME);
            const regex = new RegExp('\\?card=' + ticketPrefix + '([\\d]+)', 'i');
            let search = new URL(location.href).search;
            let matches = regex.exec(search);
            let openCard = matches === null ? null : parseInt(matches[1]);
            if (openCard !== currentOpenCardId) {
                const oldValue = currentOpenCardId;
                currentOpenCardId = openCard;
                onChange(oldValue, openCard);
            }
        }
    }

    function beforeSendFavro(favroToken, email, organizationId) {
        return (xhr) => {
            xhr.setRequestHeader('Authorization', 'Basic ' + btoa(email + ':' + favroToken));
            xhr.setRequestHeader('organizationId', organizationId);
            xhr.setRequestHeader('Content-Type', 'application/json');
        }
    }

    function getTogglHeaders(togglToken) {
        return {
            'Authorization': 'Basic ' + btoa(togglToken + ':api_token'),
            'Content-Type': 'application/json'
        };
    }

    function getTogglPid(customFields, pidCustomFieldId) {
        let pid = null;
        customFields.forEach(customField => {
            if (customField.customFieldId === pidCustomFieldId) {
                pid = customField.total;
                return true;
            }
        });

        return pid;
    }

    let timeEntryId = null;
    let timeEntryTimoutId = null;

    function startTimeEntry(card) {
        stopTimeEntry();

        timeEntryTimoutId = window.setTimeout(async () => {
            const description = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + card.sequentialId + ' / ' + card.name + ' (Auto-Toggl)';
            const togglToken = await GM.getValue(TOGGL_API_KEY_NAME);
            const pidCustomFieldId = await GM.getValue(FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME);
            let pid = getTogglPid(card.customFields, pidCustomFieldId);
            if (!pid) {
                pid = await GM.getValue(TOGGL_DEFAULT_PID_KEY_NAME);
            }
            const data = JSON.stringify({time_entry: {pid: pid, description: description, created_with: 'tampermonkey'}});
            GM.xmlHttpRequest({
                method: 'POST',
                url: 'https://www.toggl.com/api/v8/time_entries/start',
                data: data,
                headers: getTogglHeaders(togglToken),
                onload: (res) => {
                    timeEntryId = JSON.parse(res.response).data.id;
                    const togglButtonId = 'button_' + timeEntryId;
                    const togglButton = $('<button id="' + togglButtonId + '" type="button" style="border:none;background:none;cursor:pointer;">' +
                        '<img src="https://web-assets.toggl.com/app/assets/images/favicon.b87d0d2d.ico" style="width:20px;height:20px;"></button>');
                    togglButton.click(() => {
                        stopTimeEntry();
                        $('#' + togglButtonId).remove();
                    });
                    $('#' + card.cardId + '.cardeditor').find('.cardeditor-topbar .buttons').append(togglButton);
                }
            });
        }, TIMEOUT_BEFORE_TRACKING);
    }

    async function stopTimeEntry() {
        if (timeEntryTimoutId) {
            window.clearTimeout(timeEntryTimoutId);
        }

        if (!timeEntryId) {
            return;
        }

        const currentTimeEntryId = timeEntryId;
        timeEntryId = null;
        const togglToken = await GM.getValue(TOGGL_API_KEY_NAME);
        GM.xmlHttpRequest({
            method: 'PUT',
            url: 'https://www.toggl.com/api/v8/time_entries/' + currentTimeEntryId + '/stop',
            headers: getTogglHeaders(togglToken),
        });
    }

    function onOpenCardChange($) {
        return async (oldCard, newCard) => {
            if (!newCard) {
                await stopTimeEntry();
                return;
            }
            const sequentialId = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + newCard;
            const favroToken = await GM.getValue(FAVRO_API_KEY_NAME);
            const email = await GM.getValue(FAVRO_EMAIL_KEY_NAME);
            const organizationId = await GM.getValue(FAVRO_ORGANIZATION_ID_KEY_NAME);
            $.ajax({
                type: 'GET',
                url: 'https://favro.com/api/v1/cards?cardSequentialId=' + sequentialId,
                beforeSend: beforeSendFavro(favroToken, email, organizationId),
                success: (res) => {
                    const card = res.entities[0];
                    if (!card) {
                        console.error('No card found in favro for sequentialId' + sequentialId);
                        return;
                    }
                    startTimeEntry(card);
                }
            });
        };
    }

    start();
})(window.jQuery);