diff --git a/README.md b/README.md index b096e9b..8ecfa46 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ A browser extension that enhance all Merge Requests lists on any instance of Git - Button allowing to copy Merge Request information (useful when sharing the Merge Request on e.g instant messaging softwares) - Can be disabled in the extension preferences - Text format is customizable (with support of placeholders) + - Direct Jira ticket link + - Can be enabled/disabled in the extension preferences + - Ticket ID is automatically detected in source branch name or Merge Request title + - Base Jira URL is configured in extension preferences + - The ticket ID or an icon can be displayed as the link label (configured in extension preferences) - Compatible with all GitLab editions (GitLab CE, GitLab EE, GitLab.com) (look at the prerequisites, though) - No configuration needed @@ -37,11 +42,14 @@ You can also install this add-on manually by using one of the ZIP files on the [ 👉 = current version - - **1.0** - Initial release (display Merge Request source and target branches) + - **1.0** - Initial release (display Merge Request source and target branches name) - **1.1** - Copy source and target branches name - - 👉 **1.2** - Copy Merge Request information (intended for sharing on e.g instant messaging softwares) - - **1.3** - Direct Jira ticket link (automatic detection of ticket ID in branch name or Merge Request title) + - **1.2** - Copy Merge Request information (intended for sharing on e.g instant messaging softwares) + - 👉 **1.3** - Direct Jira ticket link (automatic detection of ticket ID in source branch name or Merge Request title) - **1.4** - WIP / unWIP toggle button + - **1.5** + - New option: enable display Merge Request source and target branches + - New options: enable copy source and target branches name button (one option for each branches) ## License diff --git a/css/options.css b/css/options.css index 6ce4e48..4bcc9ca 100644 --- a/css/options.css +++ b/css/options.css @@ -1,19 +1,48 @@ /************************************************************************ - * Global styles */ + * Browser-specific styles */ + +/* Firefox *************************************************************/ + +/* Dark theme styles override */ +@media (prefers-color-scheme: dark) { + body.is-firefox { + background-color: #202023; + color: rgb(249, 249, 250); + } + + .is-firefox input[type="url"], + .is-firefox textarea { + background-color: #2a2a2e; + color: #fff; + border-color: rgba(249, 249, 250, 0.2); + border-width: 1px; + } +} + +/* Chrome **************************************************************/ + +body.is-chrome { + width: 660px; +} + +/* Dark theme styles override */ +@media (prefers-color-scheme: dark) { + body.is-chrome { + background-color: #292a2d; + color: rgb(232, 234, 237); + } -/* - * Firefox for Mac: use a dark theme if the OS/browser also uses one. - * This doesn't affects Firefox for Windows (as for now), as dark/light theme detection isn't implemented for this OS. - */ -@supports (-moz-appearance:none) { - @media (prefers-color-scheme: dark) { - body { - background-color: #202023; - color: rgb(249, 249, 250); - } + .is-chrome input[type="url"], + .is-chrome textarea { + background-color: #202023; + color: rgb(232, 234, 237); + border-color: rgba(255, 255, 255, 0.1); } } +/************************************************************************ + * Global styles */ + .is-hidden { display: none; } @@ -29,6 +58,10 @@ font-family: monospace; } +.txt-muted { + opacity: 0.8; +} + /************************************************************************ * Sizing styles */ @@ -36,10 +69,18 @@ width: 100%; } +.w50 { + width: 50%; +} + .w40p { width: 40px; } +.w300p { + width: 300px; +} + /************************************************************************ * Layout styles */ @@ -63,14 +104,42 @@ padding-top: 5px; } +.ptm { + padding-top: 10px; +} + .pbs { padding-bottom: 5px; } +.pbm { + padding-bottom: 10px; +} + +.prm { + padding-right: 10px; +} + .pll { padding-left: 40px; } +.mlm { + margin-left: 10px; +} + .man { margin: 0; +} + +.mtn { + margin-top: 0; +} + +.mbn { + margin-bottom: 0; +} + +.mrn { + margin-right: 0; } \ No newline at end of file diff --git a/html/options.html b/html/options.html index dce733a..98d09af 100644 --- a/html/options.html +++ b/html/options.html @@ -6,7 +6,7 @@
-
+
@@ -14,7 +14,7 @@
-
+
@@ -23,14 +23,32 @@
- Useful when sharing the Merge Request on e.g instant messaging softwares + Useful when sharing the Merge Request on e.g instant messaging softwares
-
+
-
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}
-
+
+
+ +
+
+ +
+
+ +
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.',