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