diff --git a/README.md b/README.md index 0dc7379..588d2fa 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A browser extension that enhance all Merge Requests lists on any instance of Git - 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) - - WIP toggle button (can be enabled/disabled in the extension preferences) + - Draft toggle button (can be enabled/disabled in the extension preferences) - Show an indicator when there's unresolved discussions left on Merge Requests - Can be enabled/disabled in the extension preferences - Note the **All discussions must be resolved** option must be enabled for this feature to be working as expected. This option is enabled per project and is located in **Settings > General > Merge Requests > Merge checks** diff --git a/html/options.html b/html/options.html index 4a9c33f..5a7f7ee 100644 --- a/html/options.html +++ b/html/options.html @@ -53,10 +53,10 @@
- +
- +
@@ -76,6 +76,7 @@
+ diff --git a/js/background.js b/js/background.js new file mode 100644 index 0000000..671792e --- /dev/null +++ b/js/background.js @@ -0,0 +1,37 @@ +(function(globals) { + 'use strict'; + + class BackgroundScript { + /** + * The background script of the extension. + */ + constructor() { + if ('chrome' in globals && globals.chrome) { // Firefox and Edge uses `browser`, Chrome and Opera uses `chrome` + globals.browser = globals.chrome; + } + + if (!('browser' in globals) || !globals.browser) { + console.error('Unsupported browser'); + } + + this.listenToExtensionUpdates(); + } + + /** + * Attach a callback to the `onInstalled` runtime event, which handles stuff related to extension updates. + */ + listenToExtensionUpdates() { + let self = this; + + globals.browser.runtime.onInstalled.addListener(function(details) { + if (!('reason' in details) || details.reason != 'update') { + return; + } + + // TODO enable_button_to_toggle_wip_status => enable_button_to_toggle_draft_status + }); + } + } + + let bs = new BackgroundScript(); +}(this)); \ No newline at end of file diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..87b9c25 --- /dev/null +++ b/js/common.js @@ -0,0 +1,123 @@ +(function(globals) { + 'use strict'; + + globals.Gmrle = globals.Gmrle || {}; + + // This function has been stolen from https://github.com/locutusjs/locutus/blob/master/src/php/info/version_compare.js + globals.Gmrle.versionCompare = function(v1, v2, operator) { + // discuss at: https://locutus.io/php/version_compare/ + // original by: Philippe Jausions (https://pear.php.net/user/jausions) + // original by: Aidan Lister (https://aidanlister.com/) + // reimplemented by: Kankrelune (https://www.webfaktory.info/) + // improved by: Brett Zamir (https://brett-zamir.me) + // improved by: Scott Baker + // improved by: Theriault (https://github.com/Theriault) + // example 1: version_compare('8.2.5rc', '8.2.5a') + // returns 1: 1 + // example 2: version_compare('8.2.50', '8.2.52', '<') + // returns 2: true + // example 3: version_compare('5.3.0-dev', '5.3.0') + // returns 3: -1 + // example 4: version_compare('4.1.0.52','4.01.0.51') + // returns 4: 1 + + // Important: compare must be initialized at 0. + let i + let x + let compare = 0 + + // vm maps textual PHP versions to negatives so they're less than 0. + // PHP currently defines these as CASE-SENSITIVE. It is important to + // leave these as negatives so that they can come before numerical versions + // and as if no letters were there to begin with. + // (1alpha is < 1 and < 1.1 but > 1dev1) + // If a non-numerical value can't be mapped to this table, it receives + // -7 as its value. + const vm = { + dev: -6, + alpha: -5, + a: -5, + beta: -4, + b: -4, + RC: -3, + rc: -3, + '#': -2, + p: 1, + pl: 1 + } + + // This function will be called to prepare each version argument. + // It replaces every _, -, and + with a dot. + // It surrounds any nonsequence of numbers/dots with dots. + // It replaces sequences of dots with a single dot. + // version_compare('4..0', '4.0') === 0 + // Important: A string of 0 length needs to be converted into a value + // even less than an unexisting value in vm (-7), hence [-8]. + // It's also important to not strip spaces because of this. + // version_compare('', ' ') === 1 + const _prepVersion = function (v) { + v = ('' + v).replace(/[_\-+]/g, '.') + v = v.replace(/([^.\d]+)/g, '.$1.').replace(/\.{2,}/g, '.') + return (!v.length ? [-8] : v.split('.')) + } + // This converts a version component to a number. + // Empty component becomes 0. + // Non-numerical component becomes a negative number. + // Numerical component becomes itself as an integer. + const _numVersion = function (v) { + return !v ? 0 : (isNaN(v) ? vm[v] || -7 : parseInt(v, 10)) + } + + v1 = _prepVersion(v1) + v2 = _prepVersion(v2) + x = Math.max(v1.length, v2.length) + + for (i = 0; i < x; i++) { + if (v1[i] === v2[i]) { + continue + } + v1[i] = _numVersion(v1[i]) + v2[i] = _numVersion(v2[i]) + if (v1[i] < v2[i]) { + compare = -1 + break + } else if (v1[i] > v2[i]) { + compare = 1 + break + } + } + + if (!operator) { + return compare + } + + // Important: operator is CASE-SENSITIVE. + // "No operator" seems to be treated as "<." + // Any other values seem to make the function return null. + switch (operator) { + case '>': + case 'gt': + return (compare > 0) + case '>=': + case 'ge': + return (compare >= 0) + case '<=': + case 'le': + return (compare <= 0) + case '===': + case '=': + case 'eq': + return (compare === 0) + case '<>': + case '!==': + case 'ne': + return (compare !== 0) + case '': + case '<': + case 'lt': + return (compare < 0) + default: + return null + } + }; +}(this)); \ No newline at end of file diff --git a/js/content.js b/js/content.js index d5d29fe..938064f 100644 --- a/js/content.js +++ b/js/content.js @@ -250,8 +250,8 @@ self.attachClickEventToCopyMergeRequestInfoButtons(); } - if (self.userAuthenticated && self.preferences.enable_button_to_toggle_wip_status) { - self.attachClickEventToToggleWipStatusButtons(); + if (self.userAuthenticated && self.preferences.enable_button_to_toggle_draft_status) { + self.attachClickEventToToggleDraftStatusButtons(); } }); } @@ -316,16 +316,16 @@ this.setDataAttributesToMergeRequestNode(mergeRequestNode, mergeRequest); // ----------------------------------------------- - // Toggle WIP status button + // Toggle Draft status button - if (this.userAuthenticated && this.preferences.enable_button_to_toggle_wip_status) { - let toggleWipStatusButton = ' '; this.parseHtmlAndPrepend( mergeRequestNode.querySelector('.merge-request-title'), - toggleWipStatusButton + toggleDraftStatusButton ); } @@ -443,7 +443,7 @@ mergeRequestNode.dataset.status = mergeRequest.state; mergeRequestNode.dataset.sourceBranchName = mergeRequest.source_branch; mergeRequestNode.dataset.targetBranchName = mergeRequest.target_branch; - mergeRequestNode.dataset.isWip = mergeRequest.work_in_progress; + mergeRequestNode.dataset.isDraft = ('draft' in mergeRequest) ? mergeRequest.draft : mergeRequest.work_in_progress; if (this.preferences.enable_jira_ticket_link) { let jiraTicketId = this.findFirstJiraTicketId(mergeRequest); @@ -537,33 +537,33 @@ } /** - * Attach a click event to all buttons inserted by the extension allowing to toggle Merge Request WIP status. + * Attach a click event to all buttons inserted by the extension allowing to toggle Merge Request Draft status. */ - attachClickEventToToggleWipStatusButtons() { + attachClickEventToToggleDraftStatusButtons() { let self = this; - document.querySelectorAll('button.gmrle-toggle-wip-status').forEach(function(el) { + document.querySelectorAll('button.gmrle-toggle-draft-status').forEach(function(el) { el.addEventListener('click', function(e) { e.preventDefault(); - self.toggleMergeRequestWipStatus(this.closest('.merge-request'), this); + self.toggleMergeRequestDraftStatus(this.closest('.merge-request'), this); }); }); } /** - * Actually toggle a given Merge Request WIP status. + * Actually toggle a given Merge Request Draft status. */ - toggleMergeRequestWipStatus(mergeRequestNode, toggleButton) { + toggleMergeRequestDraftStatus(mergeRequestNode, toggleButton) { toggleButton.disabled = true; - let isWip = mergeRequestNode.dataset.isWip == 'true'; + let isDraft = mergeRequestNode.dataset.isDraft == 'true'; let newTitle = ''; - if (isWip) { - newTitle = mergeRequestNode.dataset.title.replace(new RegExp('^WIP:'), '').trim(); + if (isDraft) { + newTitle = mergeRequestNode.dataset.title.replace(new RegExp('^(Draft|WIP):', 'i'), '').trim(); } else { - newTitle = 'WIP: ' + mergeRequestNode.dataset.title.trim(); + newTitle = 'Draft: ' + mergeRequestNode.dataset.title.trim(); } this.apiClient.updateProjectMergeRequest( @@ -573,7 +573,7 @@ title: newTitle } ).then(function(responseData) { - mergeRequestNode.dataset.isWip = responseData.work_in_progress; + mergeRequestNode.dataset.isDraft = ('draft' in responseData) ? responseData.draft : responseData.work_in_progress; mergeRequestNode.dataset.title = responseData.title; mergeRequestNode.querySelector('.merge-request-title-text a').textContent = responseData.title; diff --git a/js/options.js b/js/options.js index 96fd9c5..1402d00 100644 --- a/js/options.js +++ b/js/options.js @@ -46,7 +46,7 @@ this.baseJiraUrlInput = document.querySelector('input#base_jira_url'); this.jiraTicketLinkLabelTypeRadioButtons = Array.from(document.querySelectorAll('input[name="jira_ticket_link_label_type"]')); - this.enableButtonToToggleWipStatusCheckbox = document.querySelector('input#enable_button_to_toggle_wip_status'); + this.enableButtonToToggleDraftStatusCheckbox = document.querySelector('input#enable_button_to_toggle_draft_status'); this.enableUnresolvedDiscussionsIndicatorCheckbox = document.querySelector('input#enable_unresolved_discussions_indicator'); } @@ -77,8 +77,8 @@ return el.value == preferences.jira_ticket_link_label_type; }).checked = true; - self.enableButtonToToggleWipStatusCheckbox.checked = preferences.enable_button_to_toggle_wip_status; - self.enableButtonToToggleWipStatusCheckbox.dispatchEvent(new CustomEvent('change')); + self.enableButtonToToggleDraftStatusCheckbox.checked = preferences.enable_button_to_toggle_draft_status; + self.enableButtonToToggleDraftStatusCheckbox.dispatchEvent(new CustomEvent('change')); self.enableUnresolvedDiscussionsIndicatorCheckbox.checked = preferences.enable_unresolved_discussions_indicator; self.enableUnresolvedDiscussionsIndicatorCheckbox.dispatchEvent(new CustomEvent('change')); @@ -129,7 +129,7 @@ self.forceUserToEnableAtLeastOneFeatureIfNecessarily(); }); - this.enableButtonToToggleWipStatusCheckbox.addEventListener('change', function() { + this.enableButtonToToggleDraftStatusCheckbox.addEventListener('change', function() { self.forceUserToEnableAtLeastOneFeatureIfNecessarily(); }); @@ -157,7 +157,7 @@ enable_jira_ticket_link: this.enableJiraTicketLinkCheckbox.checked, base_jira_url: this.baseJiraUrlInput.value, jira_ticket_link_label_type: jira_ticket_link_label_type, - enable_button_to_toggle_wip_status: this.enableButtonToToggleWipStatusCheckbox.checked, + enable_button_to_toggle_draft_status: this.enableButtonToToggleDraftStatusCheckbox.checked, enable_unresolved_discussions_indicator: this.enableUnresolvedDiscussionsIndicatorCheckbox.checked }, function() { @@ -193,7 +193,7 @@ return !this.displaySourceAndTargetBranchesCheckbox.checked && !this.enableButtonToCopyMrInfoCheckbox.checked && !this.enableJiraTicketLinkCheckbox.checked - && !this.enableButtonToToggleWipStatusCheckbox.checked + && !this.enableButtonToToggleDraftStatusCheckbox.checked && !this.enableUnresolvedDiscussionsIndicatorCheckbox.checked; } @@ -246,21 +246,17 @@ body.classList.add('is-' + currentBrowserName); - this.addBrowserVersionsComparisonsClasses(currentBrowserName, currentBrowserVersion, body); - } - - addBrowserVersionsComparisonsClasses(currentBrowserName, currentBrowserVersion, el) { if (!currentBrowserName || !currentBrowserVersion || !(currentBrowserName in this.browser_versions_to_compare)) { return; } this.browser_versions_to_compare[currentBrowserName].forEach(function(targetBrowserVersion) { ['gt', 'ge', 'lt', 'le', 'eq', 'ne'].forEach(function(operator) { - if (this.versionCompare(currentBrowserVersion, targetBrowserVersion, operator)) { - el.classList.add(operator + '-' + targetBrowserVersion.replace(new RegExp('\\.'), '-')); + if (globals.Gmrle.versionCompare(currentBrowserVersion, targetBrowserVersion, operator)) { + body.classList.add(operator + '-' + targetBrowserVersion.replace(new RegExp('\\.'), '-')); } - }, this); - }, this); + }); + }); } /** @@ -315,126 +311,6 @@ delete this.submitButtonInOptionsForm.dataset.originalTextContent; } - - /** - * This function has been stolen from https://github.com/locutusjs/locutus/blob/master/src/php/info/version_compare.js - */ - versionCompare(v1, v2, operator) { // eslint-disable-line camelcase - // discuss at: https://locutus.io/php/version_compare/ - // original by: Philippe Jausions (https://pear.php.net/user/jausions) - // original by: Aidan Lister (https://aidanlister.com/) - // reimplemented by: Kankrelune (https://www.webfaktory.info/) - // improved by: Brett Zamir (https://brett-zamir.me) - // improved by: Scott Baker - // improved by: Theriault (https://github.com/Theriault) - // example 1: version_compare('8.2.5rc', '8.2.5a') - // returns 1: 1 - // example 2: version_compare('8.2.50', '8.2.52', '<') - // returns 2: true - // example 3: version_compare('5.3.0-dev', '5.3.0') - // returns 3: -1 - // example 4: version_compare('4.1.0.52','4.01.0.51') - // returns 4: 1 - - // Important: compare must be initialized at 0. - let i - let x - let compare = 0 - - // vm maps textual PHP versions to negatives so they're less than 0. - // PHP currently defines these as CASE-SENSITIVE. It is important to - // leave these as negatives so that they can come before numerical versions - // and as if no letters were there to begin with. - // (1alpha is < 1 and < 1.1 but > 1dev1) - // If a non-numerical value can't be mapped to this table, it receives - // -7 as its value. - const vm = { - dev: -6, - alpha: -5, - a: -5, - beta: -4, - b: -4, - RC: -3, - rc: -3, - '#': -2, - p: 1, - pl: 1 - } - - // This function will be called to prepare each version argument. - // It replaces every _, -, and + with a dot. - // It surrounds any nonsequence of numbers/dots with dots. - // It replaces sequences of dots with a single dot. - // version_compare('4..0', '4.0') === 0 - // Important: A string of 0 length needs to be converted into a value - // even less than an unexisting value in vm (-7), hence [-8]. - // It's also important to not strip spaces because of this. - // version_compare('', ' ') === 1 - const _prepVersion = function (v) { - v = ('' + v).replace(/[_\-+]/g, '.') - v = v.replace(/([^.\d]+)/g, '.$1.').replace(/\.{2,}/g, '.') - return (!v.length ? [-8] : v.split('.')) - } - // This converts a version component to a number. - // Empty component becomes 0. - // Non-numerical component becomes a negative number. - // Numerical component becomes itself as an integer. - const _numVersion = function (v) { - return !v ? 0 : (isNaN(v) ? vm[v] || -7 : parseInt(v, 10)) - } - - v1 = _prepVersion(v1) - v2 = _prepVersion(v2) - x = Math.max(v1.length, v2.length) - - for (i = 0; i < x; i++) { - if (v1[i] === v2[i]) { - continue - } - v1[i] = _numVersion(v1[i]) - v2[i] = _numVersion(v2[i]) - if (v1[i] < v2[i]) { - compare = -1 - break - } else if (v1[i] > v2[i]) { - compare = 1 - break - } - } - - if (!operator) { - return compare - } - - // Important: operator is CASE-SENSITIVE. - // "No operator" seems to be treated as "<." - // Any other values seem to make the function return null. - switch (operator) { - case '>': - case 'gt': - return (compare > 0) - case '>=': - case 'ge': - return (compare >= 0) - case '<=': - case 'le': - return (compare <= 0) - case '===': - case '=': - case 'eq': - return (compare === 0) - case '<>': - case '!==': - case 'ne': - return (compare !== 0) - case '': - case '<': - case 'lt': - return (compare < 0) - default: - return null - } - } } document.addEventListener('DOMContentLoaded', function() { diff --git a/js/preferences.js b/js/preferences.js index a82a325..41f0218 100644 --- a/js/preferences.js +++ b/js/preferences.js @@ -13,7 +13,7 @@ enable_jira_ticket_link: false, base_jira_url: '', jira_ticket_link_label_type: 'ticket_id', - enable_button_to_toggle_wip_status: true, + enable_button_to_toggle_draft_status: true, enable_unresolved_discussions_indicator: true }; } @@ -22,11 +22,13 @@ * This class holds all the logic related to user preferences persistance. */ constructor() { - if (globals.browser) { // Firefox and Edge uses `browser`, Chrome and Opera uses `chrome` + if ('browser' in globals && globals.browser) { // Firefox and Edge uses `browser`, Chrome and Opera uses `chrome` this.getAll = this.getAllBrowser; + this.get = this.getBrowser; this.setAll = this.setAllBrowser; - } else if (globals.chrome) { + } else if ('chrome' in globals && globals.chrome) { this.getAll = this.getAllChrome; + this.get = this.getChrome; this.setAll = this.setAllChrome; } else { console.error('Unsupported browser'); @@ -39,7 +41,16 @@ * Used as `getAll` if the current browser is Firefox or Edge. */ getAllBrowser(successCallback) { - browser.storage.local.get(this.defaults).then(successCallback, function() { + this.getBrowser(this.defaults, successCallback); + } + + /** + * Get one or several user's preferences. + * + * Used as `get` if the current browser is Firefox or Edge. + */ + getBrowser(prefs, successCallback) { + globals.browser.storage.local.get(prefs).then(successCallback, function() { alert('Error retrieving extension preferences.'); }); } @@ -50,7 +61,7 @@ * Used as `setAll` if the current browser is Firefox or Edge. */ setAllBrowser(preferences, successCallback, errorCallback) { - browser.storage.local.set(preferences).then(successCallback, function() { + globals.browser.storage.local.set(preferences).then(successCallback, function() { errorCallback(); alert('Error saving extension preferences.'); @@ -63,7 +74,16 @@ * Used as `getAll` if the current browser is Chrome or Opera. */ getAllChrome(successCallback) { - chrome.storage.local.get(this.defaults, function(preferences) { + this.getChrome(this.defaults, successCallback); + } + + /** + * Get one or several user's preferences. + * + * Used as `get` if the current browser is Chrome or Opera. + */ + getChrome(prefs, successCallback) { + globals.chrome.storage.local.get(prefs, function(preferences) { if (chrome.runtime.lastError) { alert('Error retrieving extension preferences, check console for more information.'); @@ -80,7 +100,7 @@ * Used as `setAll` if the current browser is Chrome or Opera. */ setAllChrome(preferences, successCallback, errorCallback) { - chrome.storage.local.set(preferences, function() { + globals.chrome.storage.local.set(preferences, function() { if (chrome.runtime.lastError) { errorCallback(); diff --git a/screenshot.png b/screenshot.png index 9f61302..bf52089 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/scripts/settings.py b/scripts/settings.py index b8268d2..15c6bf7 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.6.0', + 'version': '1.7.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.', @@ -17,6 +17,9 @@ 'js': ['js/preferences.js', 'js/content.js'] } ], + 'background': { + 'scripts': ['js/common.js', 'js/preferences.js', 'js/background.js'] + }, 'options_ui': { 'page': 'html/options.html', 'open_in_tab': False