NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Jira Tempo Worklog Extensions Cloud // @description ATTENTION: STILL IN TESTING MAY CAUSE PROBLEMS! Extends issue detail to set tempo worklog billable times to zero (for one or all worklogs of an issue). Also shows the total worktime/billable time. Tested with Jira Cloud // @author shindai // @copyright 2020, shindai (https://openuserjs.org/users/shindai) // @license MIT // @include https://app.tempo.io/timesheets/jira/issue-view-panel* // @match https://app.tempo.io/timesheets/jira/issue-view-panel* // @updateURL https://openuserjs.org/meta/shindai/Jira_Tempo_Worklog_Extensions_Cloud.meta.js // @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js // @version 0.0.4 // @grant none // ==/UserScript== //TODO: Check security is user has rights to set times to billable = 0 //TODO: Use GET/POST from Jira/Tempo, instead copying the code of headers //TODO: Check performance // PARAMETERS var showTempoTimeStatistics = true; // // initiatilize my specific jQuery and avoid conflicts with Jira one var $j = jQuery.noConflict(true); var doing_modifications = false; var ignore_dom_event = false; $j(document).ready(function () { AddStyling(); RegisterEvents(); InitLayout(); var timer = 0; var updateTimer = 0; $j(document).bind("DOMSubtreeModified", function () { if (timer) { window.clearTimeout(timer); } if (!doing_modifications) { timer = window.setTimeout(function () { InitLayout(); }, 100); } if (ignore_dom_event) { return; } ignore_dom_event = true; $j(document).trigger("update-worklog-stats"); var issueId = GetCurrentIssueId(); if (issueId != null) { console.log("update"); GetWorklogSumForIssue(issueId, () => { if (updateTimer) { window.clearTimeout(updateTimer); } updateTimer = window.setTimeout(function () { ignore_dom_event = false; }, 100); }, function (workflow) { $j(document).trigger('update-worklog-row', workflow); }); } }); }); // INIT METHODS function AddStyling() { $j('head').append('<style type="text/css">' + '.btn-billable-zero[data-billable-time="0"]{ opacity: 0.2;}' + '.sc-bZSQDF{margin-right: 0px;}' + '.btn-billable-zero{ height: 24px; }' + '.btn-billable-zero-all{ height: 32px; float: right; margin-top: 5px; }' + '.icon-time { width: 16px; height: 16px; background-image: url(//d2g6nngx86qj5q.cloudfront.net/atl-vertigo--shard-jira-prod-eu-16--2--jres.atlassian.net/s/-ipn83r/b/24/b80d0dad6eb3bb427b6dda8336a35d66e73f9102/_/download/resources/com.atlassian.jira.jira-atlaskit-plugin:overrides-general/images/time.svg); background-size: 16px 16px; }' + '.billable-stats-container{ color: #fff; background-color: #008da6; border-color: #008da6; border-radius: 3px; height: 28px; margin-top: 6px; padding: 4px; margin-right: 8px; text-align: left; word-wrap: break-word; overflow: hidden; text-overflow: ""; white-space: nowrap; }' + '</style>'); } function getBillableIcon() { return `<span class="sc-eHfQar"><span color="currentColor" height="20" width="20" data-testid="AtIcon" class="sc-bdfBwQ bdoyjv"><svg height='20' width='20' fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="-2 -1 27 27" x="0px" y="0px"><title>_</title><path d="M21,12a9.00984,9.00984,0,0,1-9,9V19.5A7.5,7.5,0,1,0,4.5,12H3a9,9,0,0,1,18,0ZM7.452,16.25555l-.69538-.099a.77781.77781,0,0,1-.41144-.16834.328.328,0,0,1-.09423-.23981c0-.30975.247-.49469.66064-.49469a1.39528,1.39528,0,0,1,1.03729.33533l.17572.16809.17286-.171.72851-.7204.1817-.17962-.18359-.17774a2.54873,2.54873,0,0,0-1.41242-.69848V13h-1.5v.87018A1.939,1.939,0,0,0,4.64,15.79089a1.81363,1.81363,0,0,0,.49622,1.33631,2.24482,2.24482,0,0,0,1.28345.56024l.71441.09949a.62554.62554,0,0,1,.38636.14178.41279.41279,0,0,1,.10235.31476c0,.11835,0,.47851-.79419.47851a1.67171,1.67171,0,0,1-1.24737-.42413l-.177-.178-.17725.1778-.74463.7467-.175.17554.17407.17651a2.7392,2.7392,0,0,0,1.63031.79767V21h1.5v-.84894A1.97194,1.97194,0,0,0,9.25,18.20911a1.89329,1.89329,0,0,0-.53265-1.4314A2.22463,2.22463,0,0,0,7.452,16.25555ZM11.25,6v6.75H16v-1.5H12.75V6Z"></path></svg></span></span>` } function getTimeIcon() { return `<span class="sc-eHfQar"><span color="currentColor" height="20" width="20" data-testid="AtIcon" class="sc-bdfBwQ bdoyjv"><svg viewBox="0 0 20 20" fill="#001c3d"><g fill-rule="evenodd"><path d="M4.5 10a5.5 5.5 0 1011 0 5.5 5.5 0 00-11 0zM3 10a7 7 0 1114 0 7 7 0 01-14 0z"></path><path d="M12.63 10.791a.75.75 0 01-.808 1.264l-2.226-1.423A.75.75 0 019.25 10V6.5a.75.75 0 011.5 0v3.09l1.88 1.201z"></path></g></svg></span></span>` } function getGraphIcon() { return `<span class="sc-eHfQar"><span color="currentColor" height="20" width="20" data-testid="AtIcon" class="sc-bdfBwQ bdoyjv"><svg viewBox="0 0 20 20" fill="#001c3d"><g fill-rule="evenodd"><rect x="13" y="9" width="3" height="6" rx="0.5"></rect><rect x="8.5" y="11" width="3" height="4" rx="0.5"></rect><rect x="4" y="5" width="3" height="10" rx="0.5"></rect></g></svg></span></span>` } function RegisterEvents() { $j(document).on("click", ".btn-billable-zero-all", function () { if (confirm("Are you sure?")) { var issueId = GetCurrentIssueId(); SetAllWorklogsForIssueToBillableZero(issueId, function (worklog) { $j(document).trigger("update-worklog-row", worklog); }, function (parameter) { alert("Worklogs edit complete: " + parameter.successCounter + "/" + parameter.total + " were successfully edited!"); $j(document).trigger("update-worklog-stats"); }); } }); $j(document).on("click", ".btn-billable-zero", function (element) { var $element; if ($j(element.target).hasClass('.btn-billable-zero')) { $element = $j(element.target); } else { $element = $j(element.target).closest('.btn-billable-zero'); } var issueId = $element.attr("data-issueid"); var worklogId = $element.attr("data-worklogid"); SetOneWorklogForIssueToBillableZero( issueId, worklogId, function (worklog) { $j(document).trigger("update-worklog-stats"); $j(document).trigger("update-worklog-row", worklog); alert("Worklog edit complete."); }, function () { alert("Worklog edit failed."); }, function () {} ); }); $j(document).on('update-worklog-row', function (element, worklog) { var button = $j(".btn-billable-zero[data-worklogid='" + worklog.originId + "']"); if (button.length !== 0) { button.attr("data-billable-time", worklog.billableSeconds) button.prop('disabled', false); if (worklog.billableSeconds <= 0) { button.prop('disabled', true); } } var worklogWorkLabel = $j("div[name='value_work_" + worklog.originId + "']"); if (worklogWorkLabel.length !== 0) { var billabelTimeLabel = worklogWorkLabel.parent().find('.billable-time-label'); if (billabelTimeLabel.length === 0) { billabelTimeLabel = $j("<div class='billable-time-label' title='billable time'></div>").insertAfter(worklogWorkLabel); } billabelTimeLabel.html(getBillableIcon() + (worklog.billableSeconds / 60 / 60) + "h"); } }); $j(document).on('update-worklog-stats', function () { var $billableStatsContainer = $j(".billable-stats-container"); if ($billableStatsContainer.length !== 0) { var issueId = GetCurrentIssueId(); if (issueId == null) { return; } GetWorklogSumForIssue(issueId, function (result) { $billableStatsContainer.html(getTimeIcon() + (result.totalTimeInSeconds / 60 / 60) + "h |" + getBillableIcon() + (result.totalTimeBillableInSeconds / 60 / 60) + "h<wbr> |" + getGraphIcon() + result.percentage + "%"); }, function (worklog) {}); } }); } function InitLayout() { try { doing_modifications = true; InitBillableInfoIfRequired(); InitAllBillableToZeroButtonIfRequired(); InitSingleBillableToZeroButtonsIfRequired(); } catch (exception) { doing_modifications = false; console.log("Error found on the page :" + exception); } doing_modifications = false; } function InitBillableInfoIfRequired() { if (showTempoTimeStatistics === false) { return; } var $tempoButtonRow = $j('#tempoIssueViewPanel .sc-hzMMCg'); if ($tempoButtonRow.length === 0) { return; } var $billableStatsContainer = $j('#tempoIssueViewPanel .sc-hzMMCg .billable-stats-container'); if ($billableStatsContainer.length !== 0) { return; } var issueId = GetCurrentIssueId(); if (issueId == null) { return; } GetWorklogSumForIssue(issueId, function (result) { if ($j('#tempoIssueViewPanel .sc-hzMMCg .billable-stats-container').length !== 0) { return; } $j("<div class=\"billable-stats-container \" style=\"display:inline-block;\">" + getTimeIcon() + (result.totalTimeInSeconds / 60 / 60) + "h |" + getBillableIcon() + (result.totalTimeBillableInSeconds / 60 / 60) + "h<wbr> |" + getGraphIcon() + result.percentage + "%</div>").prependTo($tempoButtonRow); }, function (workflow) { $j(document).trigger('update-worklog-row', workflow); }); } function InitAllBillableToZeroButtonIfRequired() { var $tempoButtonRow = $j('#tempoIssueViewPanel .sc-kYrkKh .sc-bZSQDF'); if ($tempoButtonRow.length === 0) { return; } var $billableZeroButton = $j('#tempoIssueViewPanel .sc-kYrkKh .sc-bZSQDF .btn-billable-zero-all'); if ($billableZeroButton.length !== 0) { return; } $j("<button class=\"sc-kEjbxe GjHin sc-hBEYos btn-billable-zero-all\" title=\"Set the billable time of all worklogs of this issue to 0.\"><span class=\"sc-gqjmRU ffarVT\" style=\"margin-top: 2px;\"><span class=\"sc-VigVT eAvoVS\">Set all worklogs to billable = 0</span></span></button>").appendTo($tempoButtonRow); } function InitSingleBillableToZeroButtonsIfRequired() { var $tempoWorklogsContainer = $j('#tempoIssueViewPanel #issuePanelTableWrapper'); if ($tempoWorklogsContainer.length === 0) { return; } var $rowActionContainers = $j('#tempoIssueViewPanel #issuePanelTableWrapper table tr .tempo-activity-actions-container'); if ($rowActionContainers.length === 0) { return; } var issueId = GetCurrentIssueId(); if (issueId == null) { return; } for (var i = 0; i < $rowActionContainers.length; i++) { var $rowActionContainer = $j($rowActionContainers[i]); if ($rowActionContainer.find(".btn-billable-zero").length > 0) { continue; } var $row = $rowActionContainer.closest('tr'); var selector = "div[name^=\"value_work_\"]"; var $hiddenField = $row.find(selector); if ($hiddenField.length !== 1) { continue; } var hiddenFieldNameAttr = $hiddenField.attr('name'); var worklogId = hiddenFieldNameAttr.replace("value_work_", ""); $j("<button class=\"sc-kEjbxe GjHin sc-hBEYos btn-billable-zero\" data-issueid=\"" + issueId + "\" data-worklogid=\"" + worklogId + "\" style=\"width: 49px;\" title=\"Set the billable time of this worklog to 0.\"><span class=\"sc-gqjmRU ffarVT\"><span class=\"sc-VigVT eAvoVS\">To 0</span></span></button>").appendTo($rowActionContainer); } } // API HELPER METHODS function GetCurrentIssueId() { if (window.INITIAL_STATE == null) { return null; } var issueId = window.INITIAL_STATE.issue_id; return issueId; } function GetWorklogSumForIssue(issueId, sumResultCallback, singleWorklogResultCallback) { GetWorklogsForIssue(issueId, function (data) { function ResolveAll(worklogIdList) { var promises = []; for (var i = 0; i < worklogIdList.length; i++) { var worklogId = worklogIdList[i]; promises.push(new Promise(function (resolve, reject) { GetWorklogById(worklogId, resolve, reject); })); } return Promise.all(promises); } var worklogIds = data.activities.map(x => x.id); var result = ResolveAll(worklogIds).then(function (results) { var totalTimeInSeconds = 0; var totalTimeBillableInSeconds = 0; for (var i = 0; i < results.length; i++) { var worklog = results[i]; totalTimeInSeconds += worklog.timeSpentSeconds; totalTimeBillableInSeconds += worklog.billableSeconds; singleWorklogResultCallback(worklog); } var percentage = Math.floor((totalTimeBillableInSeconds / totalTimeInSeconds) * 100, 2); var result = { percentage: percentage, totalTimeInSeconds: totalTimeInSeconds, totalTimeBillableInSeconds: totalTimeBillableInSeconds }; sumResultCallback(result); }, function (reason) { console.log('fetching worklogs for statistics failed -> ' + reason); }); }); } function SetAllWorklogsForIssueToBillableZero(issueId, singleWorklogUpdateCallback, finishedCallback) { var url = "/rest/tempo-time-activities/1/issue/" + issueId + "/?page=1&size=9999&activityType=work¤tUser=false"; $j.ajax({ headers: getTempoHeaders(), type: "GET", url: url }).done(function (data) { var worklogIds = data.activities.map(x => x.id); var counter = 0; var successCounter = 0; var failCounter = 0; function showResult() { if (counter === worklogIds.length) { var arg = { successCounter: successCounter, total: worklogIds.length } finishedCallback(arg); } } for (var i = 0; i < worklogIds.length; i++) { var worklogId = worklogIds[i]; SetOneWorklogForIssueToBillableZero( issueId, worklogId, function () { var worklog = arguments[1]; singleWorklogUpdateCallback(worklog); successCounter++; }, function () { failCounter++; }, function () { counter++; showResult(); } ); } }); } function SetOneWorklogForIssueToBillableZero(issueId, worklogId, successCallback, failCallback, alwaysCallback) { var pObj = { billableSeconds: 0, originId: worklogId, originTaskId: "" + issueId, remainingEstimate: null, endDate: null, includeNonWorkingDays: false }; $j.ajax({ headers: getTempoHeaders(), type: "PUT", url: "/rest/tempo-timesheets/4/worklogs/" + worklogId + "/", contentType: "application/json", data: JSON.stringify(pObj), }).done(successCallback) .fail(failCallback) .always(alwaysCallback); } function GetWorklogsForIssue(issueId, successCallback) { var url = "/rest/tempo-time-activities/1/issue/" + issueId + "/?page=1&size=9999&activityType=work¤tUser=false"; $j.ajax({ headers: getTempoHeaders(), type: "GET", url: url }).done(successCallback); } function GetWorklogById(worklogId, resolve, reject) { $j.ajax({ headers: getTempoHeaders(), type: "GET", url: "/rest/tempo-timesheets/4/worklogs/" + worklogId }).done(resolve) .fail(reject); } function getTempoHeaders() { return { Accept: 'application/json, application/vnd-ms-excel', 'Content-Type': 'application/json', Pragma: 'no-cache', 'x-atlassian-force-account-id': 'true', Authorization: `Tempo-Bearer ${window.INITIAL_STATE.token.token}`, 'Tempo-User-TimeZone': window.timezone || 'GMT', }; };