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