+
-
Available placeholders: {MR_TITLE}
, {MR_ID}
, {MR_URL}
, {MR_DIFFS_URL}
, {MR_AUTHOR_NAME}
, {MR_STATUS}
, {MR_SOURCE_BRANCH_NAME}
, {MR_TARGET_BRANCH_NAME}
+
Available placeholders: {MR_TITLE}
, {MR_ID}
, {MR_URL}
, {MR_DIFFS_URL}
, {MR_AUTHOR_NAME}
, {MR_STATUS}
, {MR_SOURCE_BRANCH_NAME}
, {MR_TARGET_BRANCH_NAME}
, {MR_JIRA_TICKET_ID}
, {MR_JIRA_TICKET_URL}
-
+
+
+
+
+
+
+
+
+
+
The ticket ID is automatically searched for in the source branch name and in the Merge Request title. The position of the ticket ID in these locations doesn't matter. Only the first uppercase ticket ID occurence will be matched.
+
+
+
What should be displayed as the label of the Jira ticket link?
+
+
+
diff --git a/js/content.js b/js/content.js
index 0aac928..6f2c783 100644
--- a/js/content.js
+++ b/js/content.js
@@ -212,6 +212,39 @@
this.setDataAttributesToMergeRequestContainer(mergeRequestContainer, mergeRequest);
+ // -----------------------------------------------
+ // Jira ticket link (data attributes are set in setDataAttributesToMergeRequestContainer, above)
+
+ if (('jiraTicketId' in mergeRequestContainer.dataset) && ('jiraTicketUrl' in mergeRequestContainer.dataset)) {
+ let jiraTicketLinkLabel = null;
+
+ switch (this.preferences.jira_ticket_link_label_type) {
+ case 'ticket_id':
+ jiraTicketLinkLabel = mergeRequestContainer.dataset.jiraTicketId;
+
+ break;
+ case 'icon':
+ jiraTicketLinkLabel = '
';
+
+ break;
+ default:
+ console.error('Invalid link label type ' + this.preferences.jira_ticket_link_label_type);
+ }
+
+ if (jiraTicketLinkLabel) {
+ let jiraTicketLink = '
' +
+ jiraTicketLinkLabel +
+ ' ';
+
+ this.parseHtmlAndPrepend(
+ mergeRequestContainer.querySelector('.merge-request-title'),
+ jiraTicketLink
+ );
+ }
+ }
+
// -----------------------------------------------
// Copy MR info button
@@ -276,6 +309,55 @@
mergeRequestContainer.dataset.status = mergeRequest.state;
mergeRequestContainer.dataset.sourceBranchName = mergeRequest.source_branch;
mergeRequestContainer.dataset.targetBranchName = mergeRequest.target_branch;
+
+ if (this.preferences.enable_jira_ticket_link) {
+ let jiraTicketId = this.findFirstJiraTicketId(mergeRequest);
+
+ if (jiraTicketId) {
+ mergeRequestContainer.dataset.jiraTicketId = jiraTicketId;
+ mergeRequestContainer.dataset.jiraTicketUrl = this.createJiraTicketUrl(jiraTicketId);
+ }
+ }
+ }
+
+ /**
+ * Finds a Jira ticket ID in the given Merge Request object. It first tris in the source branch name, then
+ * fallbacks to the Merge Request title.
+ */
+ findFirstJiraTicketId(mergeRequest) {
+ let jiraTicketIdRegex = new RegExp('[A-Z]{1,10}-\\d+');
+
+ // First try in the source branch name
+ let results = jiraTicketIdRegex.exec(mergeRequest.source_branch);
+
+ if (results) {
+ return results[0];
+ }
+
+ // Fallback to the Merge Request title if none found in the source branch name
+ results = jiraTicketIdRegex.exec(mergeRequest.title);
+
+ if (results) {
+ return results[0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates an URL to a given Jira ticket ID, pointing to the Jira base URL the user has defined in its
+ * preferences.
+ */
+ createJiraTicketUrl(jiraTicketId) {
+ let baseJiraUrl = new URL(this.preferences.base_jira_url);
+
+ if (!baseJiraUrl.pathname.endsWith('/')) {
+ baseJiraUrl.pathname += '/';
+ }
+
+ baseJiraUrl.pathname += 'browse/' + jiraTicketId;
+
+ return baseJiraUrl.toString();
}
/**
@@ -324,21 +406,23 @@
*/
buildMergeRequestInfoText(mergeRequestContainer) {
let placeholders = {
- 'MR_TITLE': mergeRequestContainer.dataset.title,
- 'MR_ID': mergeRequestContainer.dataset.iid,
- 'MR_URL': mergeRequestContainer.dataset.url,
- 'MR_DIFFS_URL': mergeRequestContainer.dataset.diffsUrl,
- 'MR_AUTHOR_NAME': mergeRequestContainer.dataset.authorName,
- 'MR_STATUS': mergeRequestContainer.dataset.status,
- 'MR_SOURCE_BRANCH_NAME': mergeRequestContainer.dataset.sourceBranchName,
- 'MR_TARGET_BRANCH_NAME': mergeRequestContainer.dataset.targetBranchName
+ MR_TITLE: mergeRequestContainer.dataset.title,
+ MR_ID: mergeRequestContainer.dataset.iid,
+ MR_URL: mergeRequestContainer.dataset.url,
+ MR_DIFFS_URL: mergeRequestContainer.dataset.diffsUrl,
+ MR_AUTHOR_NAME: mergeRequestContainer.dataset.authorName,
+ MR_STATUS: mergeRequestContainer.dataset.status,
+ MR_SOURCE_BRANCH_NAME: mergeRequestContainer.dataset.sourceBranchName,
+ MR_TARGET_BRANCH_NAME: mergeRequestContainer.dataset.targetBranchName,
+ MR_JIRA_TICKET_ID: ('jiraTicketId' in mergeRequestContainer.dataset) ? mergeRequestContainer.dataset.jiraTicketId : '',
+ MR_JIRA_TICKET_URL: ('jiraTicketUrl' in mergeRequestContainer.dataset) ? mergeRequestContainer.dataset.jiraTicketUrl : ''
};
let placeholdersReplaceRegex = new RegExp('{(' + Object.keys(placeholders).join('|') + ')}', 'g');
return this.preferences.copy_mr_info_format.replace(placeholdersReplaceRegex, function(_, placeholder) {
return placeholders[placeholder];
- });
+ }).trim();
}
}
diff --git a/js/options.js b/js/options.js
index 99d7157..462925a 100644
--- a/js/options.js
+++ b/js/options.js
@@ -7,6 +7,8 @@
* the browser's local storage.
*/
constructor() {
+ this.addBrowserDiscriminatingClassToBody();
+
this.preferencesManager = new globals.Gmrle.PreferencesManager();
this.getDomNodes();
@@ -25,6 +27,11 @@
this.copyMrInfoOptionsDiv = document.querySelector('div#copy-mr-info-options');
this.enableButtonToCopyMrInfoCheckbox = document.querySelector('input#enable_button_to_copy_mr_info');
this.copyMrInfoFormatTextarea = document.querySelector('textarea#copy_mr_info_format');
+
+ this.jiraTicketLinkOptionsDiv = document.querySelector('div#jira-ticket-link-options');
+ this.enableJiraTicketLinkCheckbox = document.querySelector('input#enable_jira_ticket_link');
+ this.baseJiraUrlInput = document.querySelector('input#base_jira_url');
+ this.jiraTicketLinkLabelTypeRadioButtons = Array.from(document.querySelectorAll('input[name="jira_ticket_link_label_type"]'));
}
/**
@@ -40,6 +47,15 @@
self.enableButtonToCopyMrInfoCheckbox.dispatchEvent(new CustomEvent('change'));
self.copyMrInfoFormatTextarea.value = preferences.copy_mr_info_format;
+
+ self.enableJiraTicketLinkCheckbox.checked = preferences.enable_jira_ticket_link;
+ self.enableJiraTicketLinkCheckbox.dispatchEvent(new CustomEvent('change'));
+
+ self.baseJiraUrlInput.value = preferences.base_jira_url;
+
+ self.jiraTicketLinkLabelTypeRadioButtons.find(function(el) {
+ return el.value == preferences.jira_ticket_link_label_type;
+ }).checked = true;
});
}
@@ -59,18 +75,69 @@
self.copyMrInfoOptionsDiv.classList.toggle('is-hidden', !this.checked);
self.copyMrInfoFormatTextarea.toggleAttribute('required', this.checked);
});
+
+ this.enableJiraTicketLinkCheckbox.addEventListener('change', function() {
+ self.jiraTicketLinkOptionsDiv.classList.toggle('is-hidden', !this.checked);
+ self.baseJiraUrlInput.toggleAttribute('required', this.checked);
+
+ self.jiraTicketLinkLabelTypeRadioButtons.forEach(function(el) {
+ el.toggleAttribute('required', this.checked);
+ }, this);
+ });
}
/**
* Take all DOM nodes values and persist them in the local storage.
*/
saveOptionsToStorage() {
+ let jira_ticket_link_label_type = this.jiraTicketLinkLabelTypeRadioButtons.find(function(el) {
+ return el.checked;
+ }).value;
+
this.preferencesManager.setAll({
enable_buttons_to_copy_source_and_target_branches_name: this.enableButtonsToCopySourceAndTargetBranchesNameCheckbox.checked,
enable_button_to_copy_mr_info: this.enableButtonToCopyMrInfoCheckbox.checked,
- copy_mr_info_format: this.copyMrInfoFormatTextarea.value
+ copy_mr_info_format: this.copyMrInfoFormatTextarea.value,
+ enable_jira_ticket_link: this.enableJiraTicketLinkCheckbox.checked,
+ base_jira_url: this.baseJiraUrlInput.value,
+ jira_ticket_link_label_type: jira_ticket_link_label_type
});
}
+
+ /**
+ * Returns the browser name the extension is currently running on.
+ */
+ getCurrentBrowserName() {
+ let ua = navigator.userAgent;
+
+ if (ua.includes('Firefox') && !ua.includes('Seamonkey')) {
+ return 'firefox';
+ } else if (ua.includes('Chrome') && !ua.includes('Chromium')) {
+ return 'chrome';
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a CSS class name to the tag identifying the browser the extension is currently running on.
+ */
+ addBrowserDiscriminatingClassToBody() {
+ let currentBrowserName = this.getCurrentBrowserName();
+
+ if (!currentBrowserName) {
+ return;
+ }
+
+ let body = document.querySelector('body');
+
+ if (!body) {
+ return;
+ }
+
+ body.classList.add('is-' + currentBrowserName);
+ }
+
}
document.addEventListener('DOMContentLoaded', function() {
diff --git a/js/preferences.js b/js/preferences.js
index 7b0ccfd..f562a1b 100644
--- a/js/preferences.js
+++ b/js/preferences.js
@@ -8,7 +8,10 @@
return {
enable_buttons_to_copy_source_and_target_branches_name: true,
enable_button_to_copy_mr_info: true,
- copy_mr_info_format: 'MR {MR_ID} (from {MR_AUTHOR_NAME}): {MR_TITLE}\n{MR_URL}'
+ copy_mr_info_format: 'MR {MR_ID} (from {MR_AUTHOR_NAME}): {MR_TITLE}\n{MR_URL}',
+ enable_jira_ticket_link: false,
+ base_jira_url: '',
+ jira_ticket_link_label_type: 'ticket_id'
};
}
diff --git a/scripts/settings.py b/scripts/settings.py
index 8002a20..3f52f65 100644
--- a/scripts/settings.py
+++ b/scripts/settings.py
@@ -1,7 +1,7 @@
MANIFEST_FILE = {
'manifest_version': 2,
'name': 'GitLab Merge Requests lists enhancer',
- 'version': '1.2.0',
+ 'version': '1.3.0',
'description': 'An extension that enhance all Merge Requests lists on any instance of Gitlab and GitLab.com.',
'homepage_url': 'https://github.com/EpocDotFr/gitlab-merge-requests-lists-enhancer',
'author': 'Maxime \'Epoc\' G.',