-
Notifications
You must be signed in to change notification settings - Fork 24
/
parser.js
293 lines (248 loc) · 12.9 KB
/
parser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
var logs = [];
var unloggableTogglEntries = 0;
var config = {};
var myIdentity = {};
$(document).ready(function () {
// Retrieve the stored options
chrome.storage.sync.get({
url: 'https://jira.atlassian.net',
togglApiToken: '',
mergeEntriesBy: 'no-merge',
useTogglDescription: true,
comment: 'Updated via toggl-to-jira http://tiny.cc/t2j',
jumpToToday: false,
roundMinutes: 0,
}, function (items) {
config = items;
// If this is a new user, direct them to the Options page straight away
if(config.togglApiToken == '') window.location = "options.html";
console.log('Fetching toggl entries for today.', 'Jira url: ', config.url, config);
// Configure AJAX for Jira requests that we are intercepting
$.ajaxSetup({
contentType: 'application/json',
headers: {
'forgeJira': 'true',
'X-Atlassian-Token': 'nocheck',
'Access-Control-Allow-Origin': '*'
},
xhrFields: {
withCredentials: true
}
});
// The browser datepicker will display a date in UTC if given a local datetime (stupid)
// So we need to convert the local datetime into a local date string
var startString = localStorage.getItem('toggl-to-jira.last-date');
var startDate = (config.jumpToToday || !startString) ? dateTimeHelpers.localDateISOString() : dateTimeHelpers.localDateISOString(new Date(startString));
$('#start-picker').val(startDate);
var endString = localStorage.getItem('toggl-to-jira.last-end-date');
var endDate = (config.jumpToToday || !endString) ? dateTimeHelpers.localDateISOString(new Date(Date.now() + (3600 * 24 * 1000))) : dateTimeHelpers.localDateISOString(new Date(endString));
$('#end-picker').val(endDate);
// Try to connect to both services first - from identity.js
identity.Connect(config.url, config.togglApiToken).done(function (res) {
myIdentity = res;
$('#connectionDetails').addClass('success').removeClass('error')
.html('Toggl: ' + res.togglUserName + ' (' + res.togglEmailAddress + ') << connected >> JIRA: ' + res.jiraUserName + ' (' + res.jiraEmailAddress + ')');
// Finally fetch the Toggl entries
fetchEntries();
}).fail(function () {
$('#connectionDetails').addClass('error').removeClass('success')
.html('Connecting to Toggl or JIRA failed. Check your configuration options.');
});
// Add event handlers
$('#start-picker').on('change', fetchEntries);
$('#end-picker').on('change', fetchEntries);
$('#submit').on('click', submitEntries);
// Shortcut buttons for moving between days
$('#previous-day').on('click', function () {
$('#start-picker').val(dateTimeHelpers.localDateISOString(dateTimeHelpers.addDays(document.getElementById('start-picker').valueAsDate, -1)));
$('#end-picker').val(dateTimeHelpers.localDateISOString(dateTimeHelpers.addDays(document.getElementById('end-picker').valueAsDate, -1)));
fetchEntries();
});
$('#next-day').on('click', function () {
$('#start-picker').val(dateTimeHelpers.localDateISOString(dateTimeHelpers.addDays(document.getElementById('start-picker').valueAsDate, 1)));
$('#end-picker').val(dateTimeHelpers.localDateISOString(dateTimeHelpers.addDays(document.getElementById('end-picker').valueAsDate, 1)));
fetchEntries();
});
});
});
function submitEntries() {
// log time for each jira ticket
var timeout = 500;
logs.forEach(function (log) {
if (!log.submit) return;
$('#result-' + log.id).text('Pending...').addClass('info');
setTimeout(() => {
// comment to go with work log (this is called Work Description in Jira UI)
var comment = $("#comment-" + log.id).val() || '';
var workDescription = "";
if(config.useTogglDescription) {
workDescription = (comment.length > 0) ? (log.description + ' - ' + comment) : log.description;
} else {
workDescription = comment;
}
// Api body to send
var body = JSON.stringify({
timeSpent: log.timeSpent,
comment: workDescription,
started: log.started
});
// Post to the Api
$.post(config.url + '/rest/api/latest/issue/' + log.issue + '/worklog', body,
function success(response) {
console.log('success', response);
log.submit = false;
$('#result-' + log.id).text('OK').addClass('success').removeClass('info');
$('#input-' + log.id).removeAttr('checked').attr('disabled', 'disabled');
$("#comment-" + log.id).attr('disabled', 'disabled');
}).fail(function error(error, message) {
console.log(error, message);
var e = error.responseText || JSON.stringify(error);
console.log(e);
$('p#error').text(e + "\n" + message).addClass('error');
})
}, timeout);
timeout += 500;
});
}
// log entry checkbox toggled
function selectEntry() {
var id = this.id.split('input-')[1];
logs.forEach(function (log) {
if (log.id === id) {
log.submit = this.checked;
}
}.bind(this));
}
function fetchEntries() {
// toISOString gives us UTC midnight of the selected date
// eg; "2020-05-19T00:00:00.000Z"
var startDate = document.getElementById('start-picker').valueAsDate.toISOString();
var endDate = document.getElementById('end-picker').valueAsDate.toISOString();
// We will store these as simple ISO dates to easily retrieve and set
localStorage.setItem('toggl-to-jira.last-date', startDate);
localStorage.setItem('toggl-to-jira.last-end-date', endDate);
// Toggl is expecting dates in ISO 8601 https://en.wikipedia.org/wiki/ISO_8601 with timezone offset
// eg; "2020-05-20T04:51:50+00:00"
// Because of timezones we want to slice off the Z from the ISO string and add the local offset
// This gives us an offset from midnight of the date (eg in NZ a date of 20/05/20 should actually be to 12pm on the 19th UTC)
var startDateWithTimezoneOffset = startDate.slice(0, -1) + dateTimeHelpers.timeZoneOffset();
var endDateWithTimezoneOffset = endDate.slice(0, -1) + dateTimeHelpers.timeZoneOffset();
$('p#error').text("").removeClass('error');
// Encode the start and end times
var dateQuery = '?start_date=' + encodeURIComponent(startDateWithTimezoneOffset) + '&end_date=' + encodeURIComponent(endDateWithTimezoneOffset);
// Toggl API call with token authorisation header
// https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md
$.get({
url: 'https://api.track.toggl.com/api/v9/me/time_entries' + dateQuery,
headers: {
"Authorization": "Basic " + btoa(config.togglApiToken + ':api_token')
}
}, function (entries) {
// Reset on each fetch
logs = [];
unloggableTogglEntries = 0;
entries.forEach(function (entry) {
entry.description = entry.description || 'no-description';
var issue = entry.description.split(' ')[0];
// Validate the issue, if it's not in correct format, don't add to the table should be LETTERS-NUMBERS (XX-##)
if(!issue.includes('-') || !(Number(issue.split('-')[1]) > 0)) {
unloggableTogglEntries++;
return;
}
entry.description = entry.description.slice(issue.length + 1); // slice off the JIRA issue identifier
// from dateTimeHelpers.js
var togglTime = dateTimeHelpers.roundUpTogglDuration(entry.duration, config.roundMinutes);
var dateString = dateTimeHelpers.toJiraWhateverDateTime(entry.start); // this means the Jira work log entry will have a matching start time to the Toggl entry
var dateKey = dateTimeHelpers.createDateKey(entry.start);
var log = _.find(logs, function (log) {
if (config.mergeEntriesBy === 'issue-and-date') {
return log.issue === issue && log.dateKey === dateKey;
} else {
return log.issue === issue;
}
});
// merge toggl entries by ticket ?
if (log && config.mergeEntriesBy !== 'no-merge') {
log.timeSpentInt = log.timeSpentInt + togglTime;
log.timeSpent = log.timeSpentInt > 0 ? log.timeSpentInt.toString().toHHMM() : 'still running...';
} else {
log = {
id: entry.id.toString(),
issue: issue,
description: entry.description,
submit: (togglTime > 0),
timeSpentInt: togglTime,
timeSpent: togglTime > 0 ? togglTime.toString().toHHMM() : 'still running...',
comment: config.comment, // default comment
started: dateString,
dateKey: dateKey,
};
logs.push(log);
}
});
renderList();
});
}
function renderList() {
var list = $('#toggle-entries');
list.children().remove();
var totalTime = 0;
logs.forEach(function (log) {
var url = config.url + '/browse/' + log.issue;
var dom = '<tr><td>';
// checkbox
if (log.timeSpentInt > 0) dom += '<input id="input-' + log.id + '" type="checkbox" checked/>';
dom += '</td>';
// link to jira ticket
dom += '<td><a href="' + url + '" target="_blank">' + log.issue + '</a></td>';
dom += '<td>' + log.description.limit(35) + '</td>';
dom += '<td>' + log.started.toMMMDD() + '</td>';
if (log.timeSpentInt > 0) {
dom += '<td>' + log.timeSpentInt.toString().toHH_MM() + '</td>';
dom += '<td><input id="comment-' + log.id + '" type="text" value="' + log.comment + '" /></td>';
dom += '<td id="result-' + log.id + '"></td>';
} else {
dom += '<td colspan="3" style="text-align:center;">still running...</td>'
}
dom += '</tr>';
totalTime += (log.timeSpentInt > 0 && log.timeSpentInt) || 0;
list.append(dom);
if (log.timeSpentInt > 0) {
$('#input-' + log.id).on('click', selectEntry);
}
})
// Total Time for displayed tickets and count of unloggable Toggl entries
var totalRow = '<tr><td></td><td></td><td>';
if(unloggableTogglEntries > 0) totalRow += '<i class="warning">+' + unloggableTogglEntries + ' Toggl entries with no valid Jira issues</i>';
totalRow += '</td><td><b>TOTAL</b></td><td>' + totalTime.toString().toHHMM() + '</td></tr>';
list.append(totalRow);
// check if entry was already logged
logs.forEach(function (log) {
$.get(config.url + '/rest/api/latest/issue/' + log.issue + '/worklog',
function success(response) {
var worklogs = response.worklogs;
worklogs.forEach(function (worklog) {
// If the entry isn't for us we can skip it
if (!!myIdentity.jiraEmailAddress && !!worklog.author && worklog.author.emailAddress !== myIdentity.jiraEmailAddress) { return; }
// Try to match on worklog start date time
var dateTimeMatch = false;
if(config.mergeEntriesBy == 'no-merge') { // we need to match on each specific entry by day/month/start time
dateTimeMatch = (worklog.started.toMMMDDHHMM() === log.started.toMMMDDHHMM());
} else { // we can match on just day/month
dateTimeMatch = (worklog.started.toMMMDD() === log.started.toMMMDD());
}
// Try to match on worklog duration
var diff = Math.floor(worklog.timeSpentSeconds / 60) - Math.floor(log.timeSpentInt / 60);
// if duration is within 4 minutes because JIRA is rounding worklog minutes :facepalm:
var durationMatch = (diff < 4 && diff > -4);
// Matching entries are not able to be logged again
if (dateTimeMatch && durationMatch) {
$('#result-' + log.id).text('OK').addClass('success').removeClass('info');
$('#input-' + log.id).removeAttr('checked').attr('disabled', 'disabled');
$("#comment-" + log.id).val(worklog.comment || '').attr('disabled', 'disabled');
log.submit = false;
}
})
});
});
}