Favro - Toggl Timer

Start Toggl Timer when viewing a Favro ticket

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

  1. // ==UserScript==
  2. // @name Favro - Toggl Timer
  3. // @namespace https://www.gotom.io/
  4. // @version 1.0
  5. // @license MIT
  6. // @author Mike Meier
  7. // @match https://favro.com/*
  8. // @match https://www.toggl.com/api/*
  9. // @grant GM.xmlHttpRequest
  10. // @grant GM.setValue
  11. // @grant GM.getValue
  12. // @require http://code.jquery.com/jquery-3.4.1.min.js
  13. // @description Start Toggl Timer when viewing a Favro ticket
  14. // ==/UserScript==
  15. (function ($) {
  16. const FAVRO_EMAIL_KEY_NAME = 'favro_email';
  17. const FAVRO_API_KEY_NAME = 'favro_api_key';
  18. const FAVRO_TICKET_PREFIX_KEY_NAME = 'favro_ticket_prefix';
  19. const FAVRO_ORGANIZATION_ID_KEY_NAME = 'favro_organization_id';
  20. const FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME = 'favro_pid_custom_field_id';
  21. const FAVRO_API_BASE_URL = 'https://favro.com/api/v1';
  22.  
  23. const TOGGL_API_KEY_NAME = 'toggl_api_key';
  24. const TOGGL_DEFAULT_PID_KEY_NAME = 'toggl_default_pid';
  25. const TOGGL_API_BASE_URL = 'https://www.toggl.com/api/v8';
  26.  
  27. const TICKET_NAME_SUFFIX = ' (Auto-Toggl)';
  28.  
  29. const ENV_VALUES = [
  30. FAVRO_EMAIL_KEY_NAME,
  31. FAVRO_API_KEY_NAME,
  32. FAVRO_TICKET_PREFIX_KEY_NAME,
  33. FAVRO_ORGANIZATION_ID_KEY_NAME,
  34. FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME,
  35.  
  36. TOGGL_API_KEY_NAME,
  37. TOGGL_DEFAULT_PID_KEY_NAME
  38. ];
  39.  
  40. const TIMEOUT_BEFORE_TRACKING = 5000;
  41.  
  42. function start() {
  43. $.noConflict();
  44. ensureEnvironmentVariables();
  45. window.setInterval(detectOpenCardChanges(onOpenCardChange($)), 1000);
  46. window.onbeforeunload = stopTimeEntry;
  47. }
  48.  
  49. function ensureEnvironmentVariables() {
  50. ENV_VALUES.forEach(async key => {
  51. await GM.getValue(key) || GM.setValue(key, prompt(key));
  52. });
  53. }
  54.  
  55. function detectOpenCardChanges(onChange) {
  56. let currentOpenCardId = null;
  57. return async () => {
  58. const ticketPrefix = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME);
  59. const regex = new RegExp('\\?card=' + ticketPrefix + '([\\d]+)', 'i');
  60. let search = new URL(location.href).search;
  61. let matches = regex.exec(search);
  62. let openCard = matches === null ? null : parseInt(matches[1]);
  63. if (openCard !== currentOpenCardId) {
  64. const oldValue = currentOpenCardId;
  65. currentOpenCardId = openCard;
  66. onChange(oldValue, openCard);
  67. }
  68. }
  69. }
  70.  
  71. function beforeSendFavro(favroToken, email, organizationId) {
  72. return (xhr) => {
  73. xhr.setRequestHeader('Authorization', 'Basic ' + btoa(email + ':' + favroToken));
  74. xhr.setRequestHeader('organizationId', organizationId);
  75. xhr.setRequestHeader('Content-Type', 'application/json');
  76. }
  77. }
  78.  
  79. function getTogglHeaders(togglToken) {
  80. return {
  81. 'Authorization': 'Basic ' + btoa(togglToken + ':api_token'),
  82. 'Content-Type': 'application/json'
  83. };
  84. }
  85.  
  86. function getTogglPid(customFields, pidCustomFieldId) {
  87. let pid = null;
  88. customFields.forEach(customField => {
  89. if (customField.customFieldId === pidCustomFieldId) {
  90. pid = customField.total;
  91. return true;
  92. }
  93. });
  94.  
  95. return pid;
  96. }
  97.  
  98. let timeEntryId = null;
  99. let timeEntryTimoutId = null;
  100.  
  101. function startTimeEntry(card) {
  102. stopTimeEntry();
  103.  
  104. timeEntryTimoutId = window.setTimeout(async () => {
  105. const description = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + card.sequentialId + ' / ' + card.name + TICKET_NAME_SUFFIX;
  106. const togglToken = await GM.getValue(TOGGL_API_KEY_NAME);
  107. const pidCustomFieldId = await GM.getValue(FAVRO_PID_CUSTOM_FIELD_ID_KEY_NAME);
  108. let pid = getTogglPid(card.customFields, pidCustomFieldId);
  109. if (!pid) {
  110. pid = await GM.getValue(TOGGL_DEFAULT_PID_KEY_NAME);
  111. }
  112. const data = JSON.stringify({time_entry: {pid: pid, description: description, created_with: 'tampermonkey'}});
  113. GM.xmlHttpRequest({
  114. method: 'POST',
  115. url: TOGGL_API_BASE_URL + '/time_entries/start',
  116. data: data,
  117. headers: getTogglHeaders(togglToken),
  118. onload: (res) => {
  119. timeEntryId = JSON.parse(res.response).data.id;
  120. const togglButtonId = 'button_' + timeEntryId;
  121. const togglButton = $('<button id="' + togglButtonId + '" type="button" style="border:none;background:none;cursor:pointer;">' +
  122. '<img src="https://web-assets.toggl.com/app/assets/images/favicon.b87d0d2d.ico" style="width:20px;height:20px;"></button>');
  123. togglButton.click(() => {
  124. stopTimeEntry();
  125. $('#' + togglButtonId).remove();
  126. });
  127. $('#' + card.cardId + '.cardeditor').find('.cardeditor-topbar .buttons').append(togglButton);
  128. }
  129. });
  130. }, TIMEOUT_BEFORE_TRACKING);
  131. }
  132.  
  133. async function stopTimeEntry() {
  134. if (timeEntryTimoutId) {
  135. window.clearTimeout(timeEntryTimoutId);
  136. }
  137.  
  138. if (!timeEntryId) {
  139. return;
  140. }
  141.  
  142. const currentTimeEntryId = timeEntryId;
  143. timeEntryId = null;
  144. const togglToken = await GM.getValue(TOGGL_API_KEY_NAME);
  145. GM.xmlHttpRequest({
  146. method: 'PUT',
  147. url: TOGGL_API_BASE_URL + '/time_entries/' + currentTimeEntryId + '/stop',
  148. headers: getTogglHeaders(togglToken),
  149. });
  150. }
  151.  
  152. function onOpenCardChange($) {
  153. return async (oldCard, newCard) => {
  154. if (!newCard) {
  155. await stopTimeEntry();
  156. return;
  157. }
  158. const sequentialId = await GM.getValue(FAVRO_TICKET_PREFIX_KEY_NAME) + newCard;
  159. const favroToken = await GM.getValue(FAVRO_API_KEY_NAME);
  160. const email = await GM.getValue(FAVRO_EMAIL_KEY_NAME);
  161. const organizationId = await GM.getValue(FAVRO_ORGANIZATION_ID_KEY_NAME);
  162. $.ajax({
  163. type: 'GET',
  164. url: FAVRO_API_BASE_URL + '/cards?cardSequentialId=' + sequentialId,
  165. beforeSend: beforeSendFavro(favroToken, email, organizationId),
  166. success: (res) => {
  167. const card = res.entities[0];
  168. if (!card) {
  169. console.error('No card found in favro for sequentialId' + sequentialId);
  170. return;
  171. }
  172. startTimeEntry(card);
  173. }
  174. });
  175. };
  176. }
  177.  
  178. start();
  179. })(window.jQuery);