Skip to content
This repository was archived by the owner on Aug 31, 2023. It is now read-only.

Commit 653231e

Browse files
authored
Merge pull request #6 from EpocDotFr/wip-toggle-button
WIP toggle button
2 parents fcd58e4 + 622e5a0 commit 653231e

File tree

7 files changed

+286
-66
lines changed

7 files changed

+286
-66
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A browser extension that enhance all Merge Requests lists on any instance of Git
88

99
## Features
1010

11+
- No configuration needed: install and it just works
1112
- Display source and target branches
1213
- Buttons allowing to easily copy these branches name (can be disabled in the extension preferences)
1314
- Button allowing to copy Merge Request information (useful when sharing the Merge Request on e.g instant messaging softwares)
@@ -18,8 +19,8 @@ A browser extension that enhance all Merge Requests lists on any instance of Git
1819
- Ticket ID is automatically detected in source branch name or Merge Request title
1920
- Base Jira URL is configured in extension preferences
2021
- The ticket ID or an icon can be displayed as the link label (configured in extension preferences)
22+
- WIP toggle button (can be disabled in the extension preferences)
2123
- Compatible with all GitLab editions (GitLab CE, GitLab EE, GitLab.com) (look at the prerequisites, though)
22-
- No configuration needed
2324

2425
## Prerequisites
2526

@@ -45,8 +46,8 @@ You can also install this add-on manually by using one of the ZIP files on the [
4546
- **1.0** - Initial release (display Merge Request source and target branches name)
4647
- **1.1** - Copy source and target branches name
4748
- **1.2** - Copy Merge Request information (intended for sharing on e.g instant messaging softwares)
48-
- 👉 **1.3** - Direct Jira ticket link (automatic detection of ticket ID in source branch name or Merge Request title)
49-
- **1.4** - WIP / unWIP toggle button
49+
- **1.3** - Direct Jira ticket link (automatic detection of ticket ID in source branch name or Merge Request title)
50+
- 👉 **1.4** - WIP toggle button
5051
- **1.5**
5152
- New option: enable display Merge Request source and target branches
5253
- New options: enable copy source and target branches name button (one option for each branches)

html/options.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@
4848
<div class="browser-style man"><input type="radio" required name="jira_ticket_link_label_type" value="icon" id="jira_ticket_link_label_type_icon" class="man"> <label for="jira_ticket_link_label_type_icon">An icon</label></div>
4949
</div>
5050
</div>
51+
<div class="pts pbs row">
52+
<div class="w40p txt-center browser-style">
53+
<input type="checkbox" id="enable_button_to_toggle_wip_status">
54+
</div>
55+
<div>
56+
<label for="enable_button_to_toggle_wip_status">Enable button allowing to toggle WIP status</label>
57+
</div>
58+
</div>
5159
<div class="txt-center pts pbs"><button type="submit" class="browser-style">Save preferences</button></div>
5260
</form>
5361

js/content.js

Lines changed: 178 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* The GitLab API client used by the extension. No tokens or authentication needed as every requests are
77
* performed from inside the context of the page (GitLab allows API calls if they comes from the site).
88
*/
9-
constructor(baseUrl) {
9+
constructor(baseUrl, csrfToken) {
1010
this.baseUrl = baseUrl;
11+
this.csrfToken = csrfToken;
1112
}
1213

1314
/**
@@ -28,7 +29,7 @@
2829
/**
2930
* Sends an HTTP request to the GitLab API.
3031
*/
31-
sendRequest(callback, method, endpoint, queryStringParameters = null) {
32+
sendRequest(callback, method, endpoint, queryStringParameters = null, data = null) {
3233
let xhr = new XMLHttpRequest();
3334

3435
xhr.responseType = 'json';
@@ -40,7 +41,24 @@
4041
};
4142

4243
xhr.open(method, this.createEndpointUrl(endpoint, queryStringParameters));
43-
xhr.send();
44+
45+
if (['post', 'put', 'patch'].includes(method.toLowerCase())) {
46+
if (!this.csrfToken) {
47+
console.error('Cannot issue POST/PUT/PATCH requests without CSRF token');
48+
49+
return;
50+
}
51+
52+
xhr.setRequestHeader('X-CSRF-Token', this.csrfToken);
53+
}
54+
55+
if (data) {
56+
xhr.setRequestHeader('Content-Type', 'application/json');
57+
58+
data = JSON.stringify(data);
59+
}
60+
61+
xhr.send(data);
4462
}
4563

4664
/**
@@ -58,6 +76,26 @@
5876
queryStringParameters
5977
);
6078
}
79+
80+
/**
81+
* Update the given Merge Request Id in the given project ID.
82+
*/
83+
updateProjectMergeRequest(callback, projectId, mergeRequestId, data) {
84+
let dataToSend = {
85+
id: parseInt(projectId, 10),
86+
merge_request_iid: parseInt(mergeRequestId, 10)
87+
};
88+
89+
Object.assign(dataToSend, data);
90+
91+
this.sendRequest(
92+
callback,
93+
'PUT',
94+
'projects/' + projectId + '/merge_requests/' + mergeRequestId,
95+
null,
96+
dataToSend
97+
);
98+
}
6199
}
62100

63101
class ContentScript {
@@ -85,7 +123,7 @@
85123

86124
this.baseUrl = location.protocol + '//' + location.host;
87125
this.baseApiUrl = this.baseUrl + '/api/v4/';
88-
this.apiClient = new GitLabApiClient(this.baseApiUrl);
126+
this.apiClient = new GitLabApiClient(this.baseApiUrl, this.getCsrfToken());
89127

90128
let currentMergeRequestIds = this.getCurrentMergeRequestIds();
91129
let preferencesManager = new globals.Gmrle.PreferencesManager();
@@ -120,6 +158,15 @@
120158
return link ? link.getAttribute('href') : null;
121159
}
122160

161+
/**
162+
* Get the current CSRF token that should be sent in any subsequent POST or PUT requests to the Gitlab API.
163+
*/
164+
getCsrfToken() {
165+
let meta = document.querySelector('meta[name="csrf-token"]');
166+
167+
return meta ? meta.getAttribute('content') : null;
168+
}
169+
123170
/**
124171
* Gets all Merge Requests IDs that are currently displayed.
125172
*/
@@ -151,10 +198,14 @@
151198
if (self.preferences.enable_button_to_copy_mr_info) {
152199
self.attachClickEventToCopyMergeRequestInfoButtons();
153200
}
154-
} else {
155-
alert('Got error from GitLab, check console for more information.');
156201

202+
if (self.preferences.enable_button_to_toggle_wip_status) {
203+
self.attachClickEventToToggleWipStatusButtons();
204+
}
205+
} else {
157206
console.error('Got error from GitLab:', this.status, this.response);
207+
208+
alert('Got error from GitLab, check console for more information.');
158209
}
159210
},
160211
this.currentProjectId,
@@ -203,43 +254,68 @@
203254
});
204255
}
205256

257+
/**
258+
* Inserts the given HTML string before the given child target node.
259+
*/
260+
parseHtmlAndInsertBefore(targetNode, html) {
261+
this.parseHtml(html, function(node) {
262+
targetNode.parentNode.insertBefore(node, targetNode);
263+
});
264+
}
265+
206266
/**
207267
* Actually updates the UI by altering the DOM by adding our stuff.
208268
*/
209-
updateMergeRequestsNodes(mergeRequestsDetails) {
210-
mergeRequestsDetails.forEach(function(mergeRequest) {
211-
let mergeRequestContainer = document.querySelector('.mr-list .merge-request[data-id="' + mergeRequest.id + '"]');
269+
updateMergeRequestsNodes(mergeRequests) {
270+
mergeRequests.forEach(function(mergeRequest) {
271+
let mergeRequestNode = document.querySelector('.mr-list .merge-request[data-id="' + mergeRequest.id + '"]');
272+
273+
this.setDataAttributesToMergeRequestNode(mergeRequestNode, mergeRequest);
212274

213-
this.setDataAttributesToMergeRequestContainer(mergeRequestContainer, mergeRequest);
275+
// -----------------------------------------------
276+
// Toggle WIP status button
277+
278+
if (this.preferences.enable_button_to_toggle_wip_status) {
279+
let toggleWipStatusButton = '<button class="btn btn-secondary btn-md btn-default btn-transparent btn-clipboard has-tooltip gmrle-toggle-wip-status" title="Toggle WIP status" style="padding-left: 0">' +
280+
'<i class="fa fa-wrench" aria-hidden="true"></i>' +
281+
'</button> ';
282+
283+
this.parseHtmlAndPrepend(
284+
mergeRequestNode.querySelector('.merge-request-title'),
285+
toggleWipStatusButton
286+
);
287+
}
214288

215289
// -----------------------------------------------
216-
// Jira ticket link (data attributes are set in setDataAttributesToMergeRequestContainer, above)
290+
// Jira ticket link (data attributes are set in setDataAttributesToNode, above)
217291

218-
if (('jiraTicketId' in mergeRequestContainer.dataset) && ('jiraTicketUrl' in mergeRequestContainer.dataset)) {
292+
if (('jiraTicketId' in mergeRequestNode.dataset) && ('jiraTicketUrl' in mergeRequestNode.dataset)) {
293+
let jiraTicketLinkToolip = null;
219294
let jiraTicketLinkLabel = null;
220295

221296
switch (this.preferences.jira_ticket_link_label_type) {
222297
case 'ticket_id':
223-
jiraTicketLinkLabel = mergeRequestContainer.dataset.jiraTicketId;
298+
jiraTicketLinkLabel = mergeRequestNode.dataset.jiraTicketId;
224299

225300
break;
226301
case 'icon':
227-
jiraTicketLinkLabel = '<button class="btn btn-secondary btn-md btn-default btn-transparent btn-clipboard has-tooltip" title="Jira ticket ' + mergeRequestContainer.dataset.jiraTicketId + '">' +
228-
'<i class="fa fa-ticket" aria-hidden="true"></i>' +
229-
'</button>';
302+
jiraTicketLinkLabel = '<i class="fa fa-ticket" aria-hidden="true"></i>';
303+
jiraTicketLinkToolip = 'Jira ticket ' + mergeRequestNode.dataset.jiraTicketId;
230304

231305
break;
232306
default:
233307
console.error('Invalid link label type ' + this.preferences.jira_ticket_link_label_type);
234308
}
235309

236310
if (jiraTicketLinkLabel) {
237-
let jiraTicketLink = '<a href="' + mergeRequestContainer.dataset.jiraTicketUrl + '" class="issuable-milestone">' +
311+
let jiraTicketLink = '<a href="' + mergeRequestNode.dataset.jiraTicketUrl + '" ' +
312+
'class="issuable-milestone ' + (jiraTicketLinkToolip ? 'has-tooltip' : '') + '" ' +
313+
(jiraTicketLinkToolip ? 'title="' + jiraTicketLinkToolip + '"' : '') + '>' +
238314
jiraTicketLinkLabel +
239315
'</a> ';
240316

241-
this.parseHtmlAndPrepend(
242-
mergeRequestContainer.querySelector('.merge-request-title'),
317+
this.parseHtmlAndInsertBefore(
318+
mergeRequestNode.querySelector('.merge-request-title-text'),
243319
jiraTicketLink
244320
);
245321
}
@@ -249,12 +325,12 @@
249325
// Copy MR info button
250326

251327
if (this.preferences.enable_button_to_copy_mr_info) {
252-
let copyMrInfoButton = '<button class="btn btn-secondary btn-md btn-default btn-transparent btn-clipboard has-tooltip gmrle-copy-mr-info" title="Copy Merge Request info">' +
328+
let copyMrInfoButton = '<button class="btn btn-secondary btn-md btn-default btn-transparent btn-clipboard has-tooltip gmrle-copy-mr-info" title="Copy Merge Request info" style="padding-left: 0">' +
253329
'<i class="fa fa-share-square-o" aria-hidden="true"></i>' +
254330
'</button> ';
255331

256332
this.parseHtmlAndPrepend(
257-
mergeRequestContainer.querySelector('.issuable-info'),
333+
mergeRequestNode.querySelector('.issuable-info'),
258334
copyMrInfoButton
259335
);
260336
}
@@ -291,7 +367,7 @@
291367
newInfoLineToInject += '</div>';
292368

293369
this.parseHtmlAndAppend(
294-
mergeRequestContainer.querySelector('.issuable-main-info'),
370+
mergeRequestNode.querySelector('.issuable-main-info'),
295371
newInfoLineToInject
296372
);
297373
}, this);
@@ -300,22 +376,23 @@
300376
/**
301377
* Sets several data-* attributes on a DOM node representing a Merge Request so these values may be used later.
302378
*/
303-
setDataAttributesToMergeRequestContainer(mergeRequestContainer, mergeRequest) {
304-
mergeRequestContainer.dataset.title = mergeRequest.title;
305-
mergeRequestContainer.dataset.iid = mergeRequest.iid;
306-
mergeRequestContainer.dataset.url = mergeRequest.web_url;
307-
mergeRequestContainer.dataset.diffsUrl = mergeRequest.web_url + '/diffs';
308-
mergeRequestContainer.dataset.authorName = mergeRequest.author.name;
309-
mergeRequestContainer.dataset.status = mergeRequest.state;
310-
mergeRequestContainer.dataset.sourceBranchName = mergeRequest.source_branch;
311-
mergeRequestContainer.dataset.targetBranchName = mergeRequest.target_branch;
379+
setDataAttributesToMergeRequestNode(mergeRequestNode, mergeRequest) {
380+
mergeRequestNode.dataset.title = mergeRequest.title;
381+
mergeRequestNode.dataset.iid = mergeRequest.iid;
382+
mergeRequestNode.dataset.url = mergeRequest.web_url;
383+
mergeRequestNode.dataset.diffsUrl = mergeRequest.web_url + '/diffs';
384+
mergeRequestNode.dataset.authorName = mergeRequest.author.name;
385+
mergeRequestNode.dataset.status = mergeRequest.state;
386+
mergeRequestNode.dataset.sourceBranchName = mergeRequest.source_branch;
387+
mergeRequestNode.dataset.targetBranchName = mergeRequest.target_branch;
388+
mergeRequestNode.dataset.isWip = mergeRequest.work_in_progress;
312389

313390
if (this.preferences.enable_jira_ticket_link) {
314391
let jiraTicketId = this.findFirstJiraTicketId(mergeRequest);
315392

316393
if (jiraTicketId) {
317-
mergeRequestContainer.dataset.jiraTicketId = jiraTicketId;
318-
mergeRequestContainer.dataset.jiraTicketUrl = this.createJiraTicketUrl(jiraTicketId);
394+
mergeRequestNode.dataset.jiraTicketId = jiraTicketId;
395+
mergeRequestNode.dataset.jiraTicketUrl = this.createJiraTicketUrl(jiraTicketId);
319396
}
320397
}
321398
}
@@ -401,21 +478,78 @@
401478
});
402479
}
403480

481+
/**
482+
* Attach a click event to all buttons inserted by the extension allowing to toggle Merge Request WIP status.
483+
*/
484+
attachClickEventToToggleWipStatusButtons() {
485+
let self = this;
486+
487+
document.querySelectorAll('button.gmrle-toggle-wip-status').forEach(function(el) {
488+
el.addEventListener('click', function(e) {
489+
e.preventDefault();
490+
491+
self.toggleMergeRequestWipStatus(this.closest('.merge-request'), this);
492+
});
493+
});
494+
}
495+
496+
/**
497+
* Actually toggle a given Merge Request WIP status.
498+
*/
499+
toggleMergeRequestWipStatus(mergeRequestNode, toggleButton) {
500+
toggleButton.disabled = true;
501+
toggleButton.firstChild.classList.remove('fa-wrench');
502+
toggleButton.firstChild.classList.add('fa-spinner', 'fa-spin');
503+
504+
let isWip = mergeRequestNode.dataset.isWip == 'true';
505+
let newTitle = '';
506+
507+
if (isWip) {
508+
newTitle = mergeRequestNode.dataset.title.replace(new RegExp('^WIP:'), '').trim();
509+
} else {
510+
newTitle = 'WIP: ' + mergeRequestNode.dataset.title.trim();
511+
}
512+
513+
this.apiClient.updateProjectMergeRequest(
514+
function() {
515+
if (this.status == 200) {
516+
mergeRequestNode.dataset.isWip = this.response.work_in_progress;
517+
mergeRequestNode.dataset.title = this.response.title;
518+
519+
mergeRequestNode.querySelector('.merge-request-title-text a').textContent = this.response.title;
520+
} else {
521+
console.error('Got error from GitLab:', this.status, this.response);
522+
523+
alert('Got error from GitLab, check console for more information.');
524+
}
525+
526+
toggleButton.disabled = false;
527+
toggleButton.firstChild.classList.add('fa-wrench');
528+
toggleButton.firstChild.classList.remove('fa-spinner', 'fa-spin');
529+
},
530+
this.currentProjectId,
531+
mergeRequestNode.dataset.iid,
532+
{
533+
title: newTitle
534+
}
535+
);
536+
}
537+
404538
/**
405539
* Creates the Merge Request info text from a Merge Request container DOM node.
406540
*/
407-
buildMergeRequestInfoText(mergeRequestContainer) {
541+
buildMergeRequestInfoText(mergeRequestNode) {
408542
let placeholders = {
409-
MR_TITLE: mergeRequestContainer.dataset.title,
410-
MR_ID: mergeRequestContainer.dataset.iid,
411-
MR_URL: mergeRequestContainer.dataset.url,
412-
MR_DIFFS_URL: mergeRequestContainer.dataset.diffsUrl,
413-
MR_AUTHOR_NAME: mergeRequestContainer.dataset.authorName,
414-
MR_STATUS: mergeRequestContainer.dataset.status,
415-
MR_SOURCE_BRANCH_NAME: mergeRequestContainer.dataset.sourceBranchName,
416-
MR_TARGET_BRANCH_NAME: mergeRequestContainer.dataset.targetBranchName,
417-
MR_JIRA_TICKET_ID: ('jiraTicketId' in mergeRequestContainer.dataset) ? mergeRequestContainer.dataset.jiraTicketId : '',
418-
MR_JIRA_TICKET_URL: ('jiraTicketUrl' in mergeRequestContainer.dataset) ? mergeRequestContainer.dataset.jiraTicketUrl : ''
543+
MR_TITLE: mergeRequestNode.dataset.title,
544+
MR_ID: mergeRequestNode.dataset.iid,
545+
MR_URL: mergeRequestNode.dataset.url,
546+
MR_DIFFS_URL: mergeRequestNode.dataset.diffsUrl,
547+
MR_AUTHOR_NAME: mergeRequestNode.dataset.authorName,
548+
MR_STATUS: mergeRequestNode.dataset.status,
549+
MR_SOURCE_BRANCH_NAME: mergeRequestNode.dataset.sourceBranchName,
550+
MR_TARGET_BRANCH_NAME: mergeRequestNode.dataset.targetBranchName,
551+
MR_JIRA_TICKET_ID: ('jiraTicketId' in mergeRequestNode.dataset) ? mergeRequestNode.dataset.jiraTicketId : '',
552+
MR_JIRA_TICKET_URL: ('jiraTicketUrl' in mergeRequestNode.dataset) ? mergeRequestNode.dataset.jiraTicketUrl : ''
419553
};
420554

421555
let placeholdersReplaceRegex = new RegExp('{(' + Object.keys(placeholders).join('|') + ')}', 'g');

0 commit comments

Comments
 (0)