From f123ab8d31d4f0116429dfbf4f9a793d908e954c Mon Sep 17 00:00:00 2001 From: Mark Carver Date: Mon, 29 Aug 2016 12:44:36 -0500 Subject: [PATCH] Issue #1673278 by markcarver: Implement User Enhancements on Drupal.org and port Dreditor features --- .../drupalorg_project.user_enhancements.inc | 93 ++++++++++++ .../user_enhancements/drupalorg.issue.clone.js | 68 +++++++++ .../user_enhancements/drupalorg.issue.js | 161 +++++++++++++++++++++ .../drupalorg.patch.filename.suggestion.js | 29 ++++ .../drupalorg.project.collapse.js | 41 ++++++ .../drupalorg.project.markasread.js | 122 ++++++++++++++++ 6 files changed, 514 insertions(+) create mode 100644 drupalorg_project/drupalorg_project.user_enhancements.inc create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.clone.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.issue.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.patch.filename.suggestion.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.project.collapse.js create mode 100644 drupalorg_project/user_enhancements/drupalorg.project.markasread.js diff --git a/drupalorg_project/drupalorg_project.user_enhancements.inc b/drupalorg_project/drupalorg_project.user_enhancements.inc new file mode 100644 index 0000000..924dcec --- /dev/null +++ b/drupalorg_project/drupalorg_project.user_enhancements.inc @@ -0,0 +1,93 @@ +Some of these user enhancements are a port from a browser extension previously known as "Dreditor". As such, some of these enhancements are JavaScript based in nature and require that JavaScript is enabled in your browser for you to use them. Over time, these user enhancements may become official features of the site or, quite possibly, replaced entirely with better solutions.

', array( + '!1673278' => 'https://www.drupal.org/node/1673278', + '!2786361' => 'https://www.drupal.org/node/2786361', + )); + return array( + 'issues' => array('title' => t('Issues'), 'description' => $description), + 'patches' => array('title' => t('Patches'), 'description' => $description), + 'projects' => array('title' => t('Projects'), 'description' => $description), + ); +} + +/** + * Implements hook_user_enhancements_info(). + */ +function drupalorg_project_user_enhancements_info() { + // Issue enhancements. + $enhancements['drupalorg.issue'] = array( + 'dependencies' => array('user.enhancements'), + 'ui' => FALSE, + ); + $enhancements['drupalorg.issue.clone'] = array( + 'dependencies' => array('drupalorg.issue'), + 'group' => 'issues', + 'title' => t('Clone this issue'), + 'description' => t('Adds a "Clone this issue" button at the bottom of the issue meta data block in the sidebar. When clicked it will open a new tab/window to create a new issue in that project and populate the current issue values into that new tab/window.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + $enhancements['drupalorg.patch.filename.suggestion'] = array( + 'dependencies' => array('drupalorg.issue'), + 'group' => 'patches', + 'title' => t('Patch filename suggestion'), + 'description' => t('Adds a "Patch filename suggestion" button at the top of the "Files" fields on an issue. When clicked, it will suggest a patch filename based on the current state of the issue.'), + 'conditions' => array( + array('entity' => 'node:project_issue'), + ), + ); + + // Other enhancements. + $enhancements['drupalorg.project.collapse'] = array( + 'dependencies' => array('user.enhancements'), + 'group' => 'projects', + 'title' => t('Collapsible "Your projects"'), + 'description' => t('Adds a "Collapse projects" toggle button at the top of the "Your Projects" tab of your profile. When toggled, it will show/hide your list of projects. This is especially helpful if you have a lot of projects.'), + 'conditions' => array( + array('path' => 'project/user'), + ), + ); + $enhancements['drupalorg.project.markasread'] = array( + 'dependencies' => array('user.enhancements'), + 'group' => 'projects', + 'title' => t('Mark as read'), + 'description' => t('Makes the "new" marker next to links in tables and item lists clickable so it can be "Marked it as read" without actually having to view the link. It also adds "Mark all as read" button(s) relative to the table or item list that contains the markers.'), + ); + + return $enhancements; +} + +/** + * Implements hook_user_enhancements_js_settings_alter(). + */ +function drupalorg_project_user_enhancements_js_settings_alter(array &$settings) { + $node = menu_get_object(); + if (!$node || !$settings) { + return; + } + + foreach ($settings as $name => &$setting) { + switch ($name) { + // Fill in the current issue values. + case 'drupalorg.issue': + $setting['commentCount'] = isset($node->comment_count) ? (int) $node->comment_count : 0; + $setting['nid'] = (int) $node->nid; + if (isset($node->field_project[LANGUAGE_NONE][0]['entity'])) { + $machine_name = field_get_items('node', $node->field_project[LANGUAGE_NONE][0]['entity'], 'field_project_machine_name'); + $setting['project'] = $machine_name[0]['value']; + } + $setting['title'] = check_plain($node->title); + break; + } + } +} diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.clone.js b/drupalorg_project/user_enhancements/drupalorg.issue.clone.js new file mode 100644 index 0000000..2da19f5 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.clone.js @@ -0,0 +1,68 @@ +(function ($, Drupal, UserEnhancements, _window) { + 'use strict'; + + var selector = '.region-sidebar-first .issue-update'; + + UserEnhancements + .create('drupalorg.issue.clone', UserEnhancements.get('drupalorg.issue'), { + $wrapper: $(''), + $element: $('' + Drupal.t('Clone this issue') + '') + }) + .detachElements(selector) + .attachElements('append', selector, function () { + this.$originalForm = $('#project-issue-node-form'); + }) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Get the project. + this.project = this.getProject(); + this.parentNid = this.getNid(); + if (this.project) { + // Open a new window. + this.newWindow = _window.open('/node/add/project-issue/' + this.project + '#project-issue-node-form', '_blank'); + + // $(w).bind('load') does not actually bind to the new window "load" + // event. This may be on purpose or a bug with the currently used + // jQuery version on d.o (1.4.4). + this.newWindow.addEventListener('load', function () { + // Retrieve the DOM of the newly created window. + var $newDocument = $(this.newWindow.document); + $newDocument.ready(function () { + var $newForm = $newDocument.contents().find('#project-issue-node-form'); + var selector, selectors = [ + '#edit-title', + '#edit-body-und-0-value', + '#edit-field-issue-category-und', + '#edit-field-issue-priority-und', + '#edit-field-issue-status-und', + '#edit-field-issue-version-und', + '#edit-field-issue-component-und', + '#edit-field-issue-assigned-und', + '#edit-taxonomy-vocabulary-9-und' + ]; + for (selector in selectors) { + $newForm.find(selectors[selector]).val(this.$originalForm.find(selectors[selector]).val()); + } + + // Prepend body with "Follow-up to ..." line. + var $body = $newForm.find('#edit-body-und-0-value'); + $body.val('Follow-up to [#' + this.parentNid + ']\n\n' + $body.val()); + + // Add originating issue was parent issue relationship. + $newForm + .find('#edit-field-issue-parent-und-0-target-id') + .val(this.$originalForm.find('#edit-title').val() + ' (' + this.parentNid + ')'); + + // Ensure all fieldsets are expanded. + $newForm.find('.collapsed').removeClass('collapsed'); + + // Focus on the new issue title so users can enter it. + $newForm.find('#edit-title').focus(); + }.bind(this)); + }.bind(this), false); + } + }) + +})(jQuery, Drupal, Drupal.UserEnhancements, window); diff --git a/drupalorg_project/user_enhancements/drupalorg.issue.js b/drupalorg_project/user_enhancements/drupalorg.issue.js new file mode 100644 index 0000000..884745e --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.issue.js @@ -0,0 +1,161 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + UserEnhancements.create('drupalorg.issue', { + + /** + * Retrieve the node next comment number. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + */ + getCommentCount: function () { + return this.getSetting('commentCount', function () { + var lastCommentNumber = $('.comments .comment:last .permalink').text().match(/\d+$/); + return (lastCommentNumber ? parseInt(lastCommentNumber[0], 10) : 0); + }) + 1; + }, + + /** + * Retrieve the node identifier. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + */ + getNid: function () { + return this.getSetting('nid', function () { + var href = $('#tabs a:first').attr('href'); + if (href.length) { + return href.match(/(?:node|comment\/reply)\/(\d+)/)[1]; + } + return false; + }); + }, + + /** + * Retrieve the project short name. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + */ + getProject: function () { + return this.getSetting('project', function () { + // Retrieve project from breadcrumb. + var project = $('.breadcrumb a:eq(0)').attr('href'); + // @todo The comment preview page does not contain a breadcrumb and + // also does not expose the project name anywhere else. + // The Drupal (core) project breadcrumb does not contain a project page link. + return (project && project === '/project/issues/drupal' ? 'drupal' : project.substr(9)) || false; + }); + }, + + /** + * Retrieve the node title. + * + * Attempt to first use any passed JS settings value, falling back to + * extrapolating the value from somewhere in the DOM. + */ + getTitle: function () { + return this.getSetting('title', function () { + return $('#page-subtitle').text() || ''; + }); + }, + + getSelectedComponent: function () { + // Retrieve component from the comment form selected option label. + return $(':input[name*="issue_component"] :selected').text(); + }, + + /** + * Gets the selected version. + * + * Variations: + * 7.x + * 7.x-dev + * 7.x-alpha1 + * 7.20 + * 7.x-1.x + * 7.x-1.12 + * 7.x-1.x + * - 8.x issues - + * - Any - + * All-versions-4.x-dev + */ + getSelectedVersion: function () { + // Retrieve version from the comment form selected option label. + var version = $(':input[name*="issue_version"] :selected').text(); + return version; + }, + + /** + * Gets the selected core version. + * + * Variations: + * 7.x + * 7.20 + */ + getSelectedVersionCore: function () { + var version = Enhancements.issue.getSelectedVersion(); + var matches = version.match(/^(\d+\.[x\d]+)/); + if (matches) { + return matches[0]; + } + else { + return false; + } + }, + + /** + * Gets the selected contrib version. + * + * Variations: + * 1.x + * 1.2 + */ + getSelectedVersionContrib: function () { + var version = Enhancements.issue.getSelectedVersion(); + var matches = version.match(/^\d+\.x-(\d+\.[x\d]+)/); + if (matches) { + return matches[1]; + } + else { + return false; + } + }, + + /** + * Gets the selected core + contrib version. + * + * Variations: + * 7.x-1.x + * 7.x-1.2 + */ + getSelectedVersionCoreContrib: function () { + var version = Enhancements.issue.getSelectedVersion(); + var matches = version.match(/^(\d+\.x-\d+\.[x\d]+)/); + if (matches) { + return matches[0]; + } + else { + return false; + } + }, + + /** + * Truncates a string to a given length. + * + * @param {String} string + * @param {Number} length + * @param {Boolean} useWordBoundary + * + * @returns {string} + */ + truncateString: function (string, length, useWordBoundary) { + var toLong = string.length > length, + s_ = toLong ? string.substr(0, length - 1) : string; + return useWordBoundary && toLong ? s_.substr(0, s_.lastIndexOf(' ')) : s_; + } + + }); + +})(jQuery, Drupal, Drupal.UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.patch.filename.suggestion.js b/drupalorg_project/user_enhancements/drupalorg.patch.filename.suggestion.js new file mode 100644 index 0000000..5b70b8c --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.patch.filename.suggestion.js @@ -0,0 +1,29 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + var selector = '.field-name-field-issue-files .form-type-managed-file'; + + /** + * Create the "drupalorg.patch.filename.suggestion" user enhancement. + */ + UserEnhancements + .create('drupalorg.patch.filename.suggestion', UserEnhancements.get('drupalorg.issue'), { + $element: $('') + }) + .detachElements(selector) + .attachElements('prepend', selector) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + // Truncate and remove a heading/trailing underscore. + var title = this.truncateString(this.getTitle() || '', 25, true); + var patchName = title.replace(/[^a-zA-Z0-9]+/g, '_').replace(/(^_|_$)/, '').toLowerCase(); + var nid = this.getNid() || 0; + if (nid !== 0) { + patchName += (patchName.length ? '-' : '') + nid; + } + window.prompt("Please use this value", patchName + '-' + (this.getCommentCount() + 1) + '.patch'); + }) + +})(jQuery, Drupal, Drupal.UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.project.collapse.js b/drupalorg_project/user_enhancements/drupalorg.project.collapse.js new file mode 100644 index 0000000..5c869d1 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.project.collapse.js @@ -0,0 +1,41 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + var selector = '.view-project-issue-user-projects .view-header'; + + /** + * Attach collapsing behavior to user project tables. + */ + UserEnhancements + .create('drupalorg.project.collapse', { + $element: $(''), + state: true + }) + .detachElements(selector) + .attachElements('before', selector, function () { + this.trigger(this.storage.load('state') ? 'expand' : 'collapse', [false]); + }) + .on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + this.trigger('toggle'); + }) + .on('collapse', function (e, slide) { + slide = slide !== void 0 ? slide : true; + this.$element.val(Drupal.t('Expand projects')); + this.$selectors[slide ? 'slideUp' : 'hide'](); + this.state = false; + this.storage.save('state', this.state); + }) + .on('expand', function (e, slide) { + slide = slide !== void 0 ? slide : true; + this.$element.val(Drupal.t('Collapse projects')); + this.$selectors[slide ? 'slideDown' : 'show'](); + this.state = true; + this.storage.save('state', this.state); + }) + .on('toggle', function (e, slide) { + this.trigger(this.state ? 'collapse' : 'expand', [slide]); + }) + +})(jQuery, Drupal, Drupal.UserEnhancements); diff --git a/drupalorg_project/user_enhancements/drupalorg.project.markasread.js b/drupalorg_project/user_enhancements/drupalorg.project.markasread.js new file mode 100644 index 0000000..a33f187 --- /dev/null +++ b/drupalorg_project/user_enhancements/drupalorg.project.markasread.js @@ -0,0 +1,122 @@ +(function ($, Drupal, UserEnhancements) { + 'use strict'; + + // 'a + .marker' accounts for a d.o bug; the HTML markup contains two + // span.marker elements, the second being nested inside the first. + var selector = 'table:not(.sticky-header), .item-list > ul'; + + /** + * Attach collapsing behavior to user project tables. + */ + UserEnhancements + .create('drupalorg.project.markasread') + .detach(selector, function ($selectors) { + $selectors.each(function () { + var $container = $(this).parents('div').first(); + $container.find('input.mark-all').remove(); + $container.find('.marker.mark-as-read').off('click.mark-as-read').css('cursor', 'auto'); + }); + }) + .attach(selector, function ($selectors) { + var getParent = function ($marker) { + return $marker.parents('li, tr'); + }; + var getLink = function ($marker) { + // Usually a marker appears right after the link it is marking. + var $link = $marker.prev('a'); + + // In some cases, the marker is in a separate column of the view table. + if (!$link[0]) { + $link = getParent($marker).find('.views-field-title a'); + } + return $link; + }; + + $selectors.each(function () { + var $selector = $(this); + + // Filter markers. + var $markers = $selector.find('.marker').filter(function () { + // Usually a marker appears right after the link it is marking. + var $marker = $(this); + var $link = getLink($marker); + + // If no valid "node" link could be found, filter marker. + if (!$link[0]) { + return false; + } + + // Valid node link, but has no content (artifact node); hide it. + if ($link.is(':empty')) { + getParent($marker).hide(); + return false; + } + + return true; + }); + + // Immediately return if there are no markers. + if (!$markers[0]) { + return; + } + + var throbber = '
 
'; + + var $markAll = $(''); + $markAll.on('click.mark-as-read', function (e) { + e.preventDefault(); + e.stopPropagation(); + var $throbber = $(throbber); + $markAll.before($throbber); + $markAll.remove(); + $markAll = $throbber; + $markers.trigger('click.mark-as-read'); + }.bind(this)); + $selector.before($markAll); + + // Bind the click event to individual markers. + $markers.addClass('mark-as-read').css('cursor', 'pointer').on('click.mark-as-read', function () { + var $marker = $(this); + + var done = function () { + // Filter out the marker from the current list. + $markers = $markers.not($marker); + + // Remove any corresponding "X new" links. + var $new = getParent($marker).find('a[href$="#new"],a[href*="#comment-"]'); + if (/new/.test($new.text())) { + $new.remove(); + } + + // Remove the marker. + $marker.remove(); + + // Remove the "Mark All" button if there are no more. + if (!$markers.length) { + $markAll.remove(); + } + + }; + + var $link = getLink($marker); + if (!$link[0]) { + done(); + return; + } + + // Replace the marker with the throbber. + $marker.html(throbber); + + // The actual HTML page output is irrelevant, so denote that by + // using the appropriate HTTP method. + $.ajax({ + type: 'HEAD', + url: $link.attr('href'), + complete: done + }); + }); + }); + + }) + +})(jQuery, Drupal, Drupal.UserEnhancements); -- 2.8.3