Merge "RCFilters: Change `What's this?` i18n based on user testing"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 18 Jul 2017 23:17:41 +0000 (23:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 18 Jul 2017 23:17:41 +0000 (23:17 +0000)
54 files changed:
includes/exception/MWExceptionRenderer.php
includes/htmlform/fields/HTMLSelectAndOtherField.php
includes/htmlform/fields/HTMLSelectOrOtherField.php
includes/jobqueue/JobQueueDB.php
includes/specialpage/ChangesListSpecialPage.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/deleteDefaultMessages.php
mw-config/config.js
package.json
resources/src/jquery/jquery.badge.js
resources/src/jquery/jquery.colorUtil.js
resources/src/jquery/jquery.makeCollapsible.js
resources/src/jquery/jquery.mwExtension.js
resources/src/jquery/jquery.suggestions.js
resources/src/jquery/jquery.tablesorter.js
resources/src/mediawiki.action/mediawiki.action.edit.preview.js
resources/src/mediawiki.action/mediawiki.action.history.js
resources/src/mediawiki.action/mediawiki.action.view.metadata.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuSectionOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js
resources/src/mediawiki.special/mediawiki.special.preferences.js
resources/src/mediawiki.special/mediawiki.special.search.commonsInterwikiWidget.js
resources/src/mediawiki.widgets.datetime/DateTimeFormatter.js
resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js
resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.js
resources/src/mediawiki/api/rollback.js
resources/src/mediawiki/api/upload.js
resources/src/mediawiki/htmlform/hide-if.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js
resources/src/mediawiki/mediawiki.RegExp.js
resources/src/mediawiki/mediawiki.Title.js
resources/src/mediawiki/mediawiki.debug.js
resources/src/mediawiki/mediawiki.feedback.js
resources/src/mediawiki/mediawiki.jqueryMsg.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.notification.js
resources/src/mediawiki/mediawiki.requestIdleCallback.js
resources/src/mediawiki/mediawiki.searchSuggest.js
resources/src/mediawiki/mediawiki.toc.js
resources/src/mediawiki/page/patrol.ajax.js
resources/src/mediawiki/page/rollback.js
tests/phpunit/includes/specialpage/ChangesListSpecialPageTest.php
tests/qunit/suites/resources/jquery/jquery.byteLimit.test.js
tests/qunit/suites/resources/jquery/jquery.makeCollapsible.test.js
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
tests/selenium/wdio.conf.js

index 2eb821a..60d760f 100644 (file)
@@ -211,7 +211,7 @@ class MWExceptionRenderer {
                                "\nBacktrace:\n" .
                                MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
                } else {
-                       return self::getShowBacktraceError( $e );
+                       return self::getShowBacktraceError( $e ) . "\n";
                }
        }
 
@@ -242,7 +242,7 @@ class MWExceptionRenderer {
                        $vars[] = '$wgShowDBErrorBacktrace = true;';
                }
                $vars = implode( ' and ', $vars );
-               return "Set $vars at the bottom of LocalSettings.php to show detailed debugging information\n";
+               return "Set $vars at the bottom of LocalSettings.php to show detailed debugging information.";
        }
 
        /**
index 9af60e5..38b487a 100644 (file)
@@ -63,8 +63,70 @@ class HTMLSelectAndOtherField extends HTMLSelectField {
                return "$select<br />\n$textbox";
        }
 
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.SelectWithInputWidget' ];
+       }
+
        public function getInputOOUI( $value ) {
-               return false;
+               $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SelectWithInputWidget.styles' );
+
+               # TextInput
+               $textAttribs = [
+                       'id' => $this->mID . '-other',
+                       'name' => $this->mName . '-other',
+                       'size' => $this->getSize(),
+                       'class' => [ 'mw-htmlform-select-and-other-field' ],
+                       'data-id-select' => $this->mID,
+                       'value' => $value[2],
+               ];
+
+               $allowedParams = [
+                       'required',
+                       'autofocus',
+                       'multiple',
+                       'disabled',
+                       'tabindex',
+                       'maxlength',
+               ];
+
+               $textAttribs += OOUI\Element::configFromHtmlAttributes(
+                       $this->getAttributes( $allowedParams )
+               );
+
+               if ( $this->mClass !== '' ) {
+                       $textAttribs['classes'] = [ $this->mClass ];
+               }
+
+               # DropdownInput
+               $dropdownInputAttribs = [
+                       'name' => $this->mName,
+                       'id' => $this->mID,
+                       'options' => $this->getOptionsOOUI(),
+                       'value' => $value[1],
+               ];
+
+               $allowedParams = [
+                       'tabindex',
+                       'disabled',
+               ];
+
+               $dropdownInputAttribs += OOUI\Element::configFromHtmlAttributes(
+                       $this->getAttributes( $allowedParams )
+               );
+
+               if ( $this->mClass !== '' ) {
+                       $dropdownInputAttribs['classes'] = [ $this->mClass ];
+               }
+
+               return $this->getInputWidget( [
+                       'textinput' => $textAttribs,
+                       'dropdowninput' => $dropdownInputAttribs,
+                       'or' => false,
+               ] );
+       }
+
+       public function getInputWidget( $params ) {
+               return new Mediawiki\Widget\SelectWithInputWidget( $params );
        }
 
        /**
index bb41079..a009b28 100644 (file)
@@ -64,8 +64,80 @@ class HTMLSelectOrOtherField extends HTMLTextField {
                return "$select<br />\n$textbox";
        }
 
+       protected function shouldInfuseOOUI() {
+               return true;
+       }
+
+       protected function getOOUIModules() {
+               return [ 'mediawiki.widgets.SelectWithInputWidget' ];
+       }
+
        public function getInputOOUI( $value ) {
-               return false;
+               $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.SelectWithInputWidget.styles' );
+
+               $valInSelect = false;
+               if ( $value !== false ) {
+                       $value = strval( $value );
+                       $valInSelect = in_array(
+                               $value, HTMLFormField::flattenOptions( $this->getOptions() ), true
+                       );
+               }
+
+               # DropdownInput
+               $dropdownAttribs = [
+                       'id' => $this->mID,
+                       'name' => $this->mName,
+                       'options' => $this->getOptionsOOUI(),
+                       'value' => $valInSelect ? $value : 'other',
+                       'class' => [ 'mw-htmlform-select-or-other' ],
+               ];
+
+               $allowedParams = [
+                       'disabled',
+                       'tabindex',
+               ];
+
+               $dropdownAttribs += OOUI\Element::configFromHtmlAttributes(
+                       $this->getAttributes( $allowedParams )
+               );
+
+               # TextInput
+               $textAttribs = [
+                       'id' => $this->mID . '-other',
+                       'name' => $this->mName . '-other',
+                       'size' => $this->getSize(),
+                       'value' => $valInSelect ? '' : $value,
+               ];
+
+               $allowedParams = [
+                       'required',
+                       'autofocus',
+                       'multiple',
+                       'disabled',
+                       'tabindex',
+                       'maxlength',
+               ];
+
+               $textAttribs += OOUI\Element::configFromHtmlAttributes(
+                       $this->getAttributes( $allowedParams )
+               );
+
+               if ( $this->mClass !== '' ) {
+                       $textAttribs['classes'] = [ $this->mClass ];
+               }
+               if ( $this->mPlaceholder !== '' ) {
+                       $textAttribs['placeholder'] = $this->mPlaceholder;
+               }
+
+               return $this->getInputWidget( [
+                       'textinput' => $textAttribs,
+                       'dropdowninput' => $dropdownAttribs,
+                       'or' => true,
+               ] );
+       }
+
+       public function getInputWidget( $params ) {
+               return new Mediawiki\Widget\SelectWithInputWidget( $params );
        }
 
        /**
index cefe74d..b7cc133 100644 (file)
@@ -768,7 +768,7 @@ class JobQueueDB extends JobQueue {
        protected function getDB( $index ) {
                $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
                $lb = ( $this->cluster !== false )
-                       ? $lbFactory->getExternalLB( $this->cluster, $this->wiki )
+                       ? $lbFactory->getExternalLB( $this->cluster )
                        : $lbFactory->getMainLB( $this->wiki );
 
                return $lb->getConnectionRef( $index, [], $this->wiki );
index b85d272..645fbb2 100644 (file)
@@ -86,8 +86,6 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'filters' => [
                                        [
                                                'name' => 'hideliu',
-                                               'label' => 'rcfilters-filter-registered-label',
-                                               'description' => 'rcfilters-filter-registered-description',
                                                // rcshowhideliu-show, rcshowhideliu-hide,
                                                // wlshowhideliu
                                                'showHideSuffix' => 'showhideliu',
@@ -97,16 +95,11 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                ) {
                                                        $conds[] = 'rc_user = 0';
                                                },
-                                               'cssClassSuffix' => 'liu',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return $rc->getAttribute( 'rc_user' );
-                                               },
+                                               'isReplacedInStructuredUi' => true,
 
                                        ],
                                        [
                                                'name' => 'hideanons',
-                                               'label' => 'rcfilters-filter-unregistered-label',
-                                               'description' => 'rcfilters-filter-unregistered-description',
                                                // rcshowhideanons-show, rcshowhideanons-hide,
                                                // wlshowhideanons
                                                'showHideSuffix' => 'showhideanons',
@@ -116,10 +109,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                                ) {
                                                        $conds[] = 'rc_user != 0';
                                                },
-                                               'cssClassSuffix' => 'anon',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return !$rc->getAttribute( 'rc_user' );
-                                               },
+                                               'isReplacedInStructuredUi' => true,
                                        ]
                                ],
                        ],
@@ -128,9 +118,26 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'name' => 'userExpLevel',
                                'title' => 'rcfilters-filtergroup-userExpLevel',
                                'class' => ChangesListStringOptionsFilterGroup::class,
-                               // Excludes unregistered users
-                               'isFullCoverage' => false,
+                               'isFullCoverage' => true,
                                'filters' => [
+                                       [
+                                               'name' => 'unregistered',
+                                               'label' => 'rcfilters-filter-user-experience-level-unregistered-label',
+                                               'description' => 'rcfilters-filter-user-experience-level-unregistered-description',
+                                               'cssClassSuffix' => 'user-unregistered',
+                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                                       return !$rc->getAttribute( 'rc_user' );
+                                               }
+                                       ],
+                                       [
+                                               'name' => 'registered',
+                                               'label' => 'rcfilters-filter-user-experience-level-registered-label',
+                                               'description' => 'rcfilters-filter-user-experience-level-registered-description',
+                                               'cssClassSuffix' => 'user-registered',
+                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                                       return $rc->getAttribute( 'rc_user' );
+                                               }
+                                       ],
                                        [
                                                'name' => 'newcomer',
                                                'label' => 'rcfilters-filter-user-experience-level-newcomer-label',
@@ -632,19 +639,10 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                $this->registerFiltersFromDefinitions( [ $unstructuredGroupDefinition ] );
 
                $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' );
-
-               $registration = $this->getFilterGroup( 'registration' );
-               $anons = $registration->getFilter( 'hideanons' );
-
-               // This means there is a conflict between any item in user experience level
-               // being checked and only anons being *shown* (hideliu=1&hideanons=0 in the
-               // URL, or equivalent).
-               $userExperienceLevel->conflictsWith(
-                       $anons,
-                       'rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global',
-                       'rcfilters-filtergroup-user-experience-level-conflicts-unregistered',
-                       'rcfilters-filter-unregistered-conflicts-user-experience-level'
-               );
+               $registered = $userExperienceLevel->getFilter( 'registered' );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) );
+               $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) );
 
                $categoryFilter = $changeTypeGroup->getFilter( 'hidecategorization' );
                $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' );
@@ -1337,15 +1335,35 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        $wgLearnerMemberSince,
                        $wgExperiencedUserMemberSince;
 
-               $LEVEL_COUNT = 3;
+               $LEVEL_COUNT = 5;
 
-               // If all levels are selected, all logged-in users are included (but no
-               // anons), so we can short-circuit.
+               // If all levels are selected, don't filter
                if ( count( $selectedExpLevels ) === $LEVEL_COUNT ) {
+                       return;
+               }
+
+               // both 'registered' and 'unregistered', experience levels, if any, are included in 'registered'
+               if (
+                       in_array( 'registered', $selectedExpLevels ) &&
+                       in_array( 'unregistered', $selectedExpLevels )
+               ) {
+                       return;
+               }
+
+               // 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
+               if (
+                       in_array( 'registered', $selectedExpLevels ) &&
+                       !in_array( 'unregistered', $selectedExpLevels )
+               ) {
                        $conds[] = 'rc_user != 0';
                        return;
                }
 
+               if ( $selectedExpLevels === [ 'unregistered' ] ) {
+                       $conds[] = 'rc_user = 0';
+                       return;
+               }
+
                $tables[] = 'user';
                $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
 
@@ -1373,24 +1391,39 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        IDatabase::LIST_AND
                );
 
+               $conditions = [];
+
+               if ( in_array( 'unregistered', $selectedExpLevels ) ) {
+                       $selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
+                       $conditions[] = 'rc_user = 0';
+               }
+
                if ( $selectedExpLevels === [ 'newcomer' ] ) {
-                       $conds[] = "NOT ( $aboveNewcomer )";
+                       $conditions[] = "NOT ( $aboveNewcomer )";
                } elseif ( $selectedExpLevels === [ 'learner' ] ) {
-                       $conds[] = $dbr->makeList(
+                       $conditions[] = $dbr->makeList(
                                [ $aboveNewcomer, "NOT ( $aboveLearner )" ],
                                IDatabase::LIST_AND
                        );
                } elseif ( $selectedExpLevels === [ 'experienced' ] ) {
-                       $conds[] = $aboveLearner;
+                       $conditions[] = $aboveLearner;
                } elseif ( $selectedExpLevels === [ 'learner', 'newcomer' ] ) {
-                       $conds[] = "NOT ( $aboveLearner )";
+                       $conditions[] = "NOT ( $aboveLearner )";
                } elseif ( $selectedExpLevels === [ 'experienced', 'newcomer' ] ) {
-                       $conds[] = $dbr->makeList(
+                       $conditions[] = $dbr->makeList(
                                [ "NOT ( $aboveNewcomer )", $aboveLearner ],
                                IDatabase::LIST_OR
                        );
                } elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
-                       $conds[] = $aboveNewcomer;
+                       $conditions[] = $aboveNewcomer;
+               } elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
+                       $conditions[] = 'rc_user != 0';
+               }
+
+               if ( count( $conditions ) > 1 ) {
+                       $conds[] = $dbr->makeList( $conditions, IDatabase::LIST_OR );
+               } elseif ( count( $conditions ) === 1 ) {
+                       $conds[] = reset( $conditions );
                }
        }
 }
index a76eafc..263a1c1 100644 (file)
        "rcfilters-noresults-conflict": "No results found because the search criteria are in conflict",
        "rcfilters-state-message-subset": "This filter has no effect because its results are included with those of the following, broader {{PLURAL:$2|filter|filters}} (try highlighting to distinguish it): $1",
        "rcfilters-state-message-fullcoverage": "Selecting all filters in a group is the same as selecting none, so this filter has no effect. Group includes: $1",
-       "rcfilters-filtergroup-registration": "User registration",
-       "rcfilters-filter-registered-label": "Registered",
-       "rcfilters-filter-registered-description": "Logged-in editors.",
-       "rcfilters-filter-unregistered-label": "Unregistered",
-       "rcfilters-filter-unregistered-description": "Editors who aren’t logged in.",
-       "rcfilters-filter-unregistered-conflicts-user-experience-level": "This filter conflicts with the following Experience {{PLURAL:$2|filter|filters}}, which {{PLURAL:$2|finds|find}} only registered users: $1",
        "rcfilters-filtergroup-authorship": "Contribution authorship",
        "rcfilters-filter-editsbyself-label": "Changes by you",
        "rcfilters-filter-editsbyself-description": "Your own contributions.",
        "rcfilters-filter-editsbyother-label": "Changes by others",
        "rcfilters-filter-editsbyother-description": "All changes except your own.",
-       "rcfilters-filtergroup-userExpLevel": "Experience level (for registered users only)",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered": "Experience filters find only registered users, so this filter conflicts with the “Unregistered” filter.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global": "The \"Unregistered\" filter conflicts with one or more Experience filters, which find registered users only. The conflicting filters are marked in the Active Filters area, above.",
+       "rcfilters-filtergroup-userExpLevel": "Experience registration and experience",
+       "rcfilters-filter-user-experience-level-registered-label": "Registered",
+       "rcfilters-filter-user-experience-level-registered-description": "Logged-in editors.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Unregistered",
+       "rcfilters-filter-user-experience-level-unregistered-description": "Editors who aren't logged-in.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Newcomers",
-       "rcfilters-filter-user-experience-level-newcomer-description": "Fewer than 10 edits and 4 days of activity.",
+       "rcfilters-filter-user-experience-level-newcomer-description": "Registered editors with fewer than 10 edits and 4 days of activity.",
        "rcfilters-filter-user-experience-level-learner-label": "Learners",
-       "rcfilters-filter-user-experience-level-learner-description": "More experience than \"Newcomers\" but less than \"Experienced users\".",
+       "rcfilters-filter-user-experience-level-learner-description": "Registered editors whose experience falls between \"Newcomers\" and \"Experienced users.\"",
        "rcfilters-filter-user-experience-level-experienced-label": "Experienced users",
-       "rcfilters-filter-user-experience-level-experienced-description": "More than 30 days of activity and 500 edits.",
+       "rcfilters-filter-user-experience-level-experienced-description": "Registered editors with more than 500 edits and 30 days of activity.",
        "rcfilters-filtergroup-automated": "Automated contributions",
        "rcfilters-filter-bots-label": "Bot",
        "rcfilters-filter-bots-description": "Edits made by automated tools.",
index c8f0e06..182b910 100644 (file)
        "rcfilters-noresults-conflict": "A message displayed in the results area when no results found because there are filters in conflict with one another.",
        "rcfilters-state-message-subset": "Tooltip shown when hovering over a filter tag when one or more broader filters that contain the hovered filter are also selected. This indicates that the hovered filter has no effect because all the results it matches are also matched by the broader filter(s).  Parameters:\n* $1 - Comma-separated string of selected broader filters that this filter is a subset of\n* $2 - Count of filters in $1, for PLURAL",
        "rcfilters-state-message-fullcoverage": "Tooltip shown when hovering over a filter tag when all the filters in its group are selected. This indicates that the hovered filter has no effect because the selected filters in the group cover all changes. Parameters:\n* $1 - Comma-separated string of selected filters in the group\n* $2 - Count of filters in $1, for PLURAL",
-       "rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
-       "rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
-       "rcfilters-filter-registered-description": "Description for the filter for showing edits made by logged-in users.",
-       "rcfilters-filter-unregistered-label": "Label for the filter for showing edits made by logged-out users.\n{{Identical|Unregistered}}",
-       "rcfilters-filter-unregistered-description": "Description for the filter for showing edits made by logged-out users.",
-       "rcfilters-filter-unregistered-conflicts-user-experience-level": "Tooltip shown when hovering over a Unregistered filter tag, when a User Experience Level filter is also selected.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.\n\n\"Experience\" is based on {{msg-mw|Rcfilters-filtergroup-userExpLevel}}.\n\nThis indicates that no results will be shown, because users matched by the User Experience Level groups are never unregistered.  Parameters:\n* $1 - Comma-separated string of selected User Experience Level filters, e.g. \"Newcomer, Experienced\"\n* $2 - Count of selected User Experience Level filters, for PLURAL",
        "rcfilters-filtergroup-authorship": "Title for the filter group for edit authorship. This filter group allows the user to choose between \"Your own edits\" and \"Edits by others\". More info: https://phabricator.wikimedia.org/T149859",
        "rcfilters-filter-editsbyself-label": "Label for the filter for showing edits made by the current user.",
        "rcfilters-filter-editsbyself-description": "Description for the filter for showing edits made by the current user.",
        "rcfilters-filter-editsbyother-label": "Label for the filter for showing edits made by anyone other than the current user.",
        "rcfilters-filter-editsbyother-description": "Description for the filter for showing edits made by anyone other than the current user.",
        "rcfilters-filtergroup-userExpLevel": "Title for the filter group for user experience levels.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered": "Tooltip shown when hovering over a User Experience Level filter tag, when only Unregistered users are being shown.  This indicates that no results will be shown, because users matched by the User Experience Level groups are never unregistered.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.",
-       "rcfilters-filtergroup-user-experience-level-conflicts-unregistered-global": "Message shown in the result area when both a User Experience Level filter and the Unregistered filter are selected.  This indicates that no results will be shown because users selected by the User Experience Filter are never unregistered.\n\n\"Unregistered\" is {{msg-mw|Rcfilters-filter-unregistered-label}}.\n\n\"Experience\" is based on {{msg-mw|Rcfilters-filtergroup-userExpLevel}}.",
+       "rcfilters-filter-user-experience-level-registered-label": "Label for the filter for showing edits made by logged-in editors.",
+       "rcfilters-filter-user-experience-level-registered-description": "Description for the filter for showing edits made by logged-in editors.",
+       "rcfilters-filter-user-experience-level-unregistered-label": "Label for the filter for showing edits made by anonymous editors.",
+       "rcfilters-filter-user-experience-level-unregistered-description": "Description for the filter for showing edits made by anonymous editors.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Label for the filter for showing edits made by new editors.",
        "rcfilters-filter-user-experience-level-newcomer-description": "Description for the filter for showing edits made by new editors.",
        "rcfilters-filter-user-experience-level-learner-label": "Label for the filter for showing edits made by learning editors.",
index 69f4f89..ba8662a 100644 (file)
@@ -35,6 +35,7 @@ class DeleteDefaultMessages extends Maintenance {
                parent::__construct();
                $this->addDescription( 'Deletes all pages in the MediaWiki namespace' .
                        ' which were last edited by "MediaWiki default"' );
+               $this->addOption( 'dry-run', 'Perform a dry run, delete nothing' );
        }
 
        public function execute() {
@@ -52,14 +53,23 @@ class DeleteDefaultMessages extends Maintenance {
                );
 
                if ( $dbr->numRows( $res ) == 0 ) {
-                       # No more messages left
+                       // No more messages left
                        $this->output( "done.\n" );
+                       return;
+               }
 
+               $dryrun = $this->hasOption( 'dry-run' );
+               if ( $dryrun ) {
+                       foreach ( $res as $row ) {
+                               $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               $this->output( "\n* [[$title]]" );
+                       }
+                       $this->output( "\n\nRun again without --dry-run to delete these pages.\n" );
                        return;
                }
 
-               # Deletions will be made by $user temporarly added to the bot group
-               # in order to hide it in RecentChanges.
+               // Deletions will be made by $user temporarly added to the bot group
+               // in order to hide it in RecentChanges.
                $user = User::newFromName( 'MediaWiki default' );
                if ( !$user ) {
                        $this->error( "Invalid username", true );
@@ -67,7 +77,7 @@ class DeleteDefaultMessages extends Maintenance {
                $user->addGroup( 'bot' );
                $wgUser = $user;
 
-               # Handle deletion
+               // Handle deletion
                $this->output( "\n...deleting old default messages (this may take a long time!)...", 'msg' );
                $dbw = $this->getDB( DB_MASTER );
 
index 8b2d6e5..c745ce4 100644 (file)
@@ -4,7 +4,7 @@
 
                function syncText() {
                        var value = $( this ).val()
-                               .replace( /[\[\]\{\}|#<>%+? ]/g, '_' )
+                               .replace( /[\[\]{}|#<>%+? ]/g, '_' ) // eslint-disable-line no-useless-escape
                                .replace( /&/, '&amp;' )
                                .replace( /__+/g, '_' )
                                .replace( /^_+/, '' )
                }
 
                // Set up the help system
-               $( '.config-help-field-data' )
-                       .hide()
-                       .closest( '.config-help-field-container' )
-                               .find( '.config-help-field-hint' )
-                                       .show()
-                                       .click( function () {
-                                               $( this )
-                                                       .closest( '.config-help-field-container' )
-                                                               .find( '.config-help-field-data' )
-                                                                       .slideToggle( 'fast' );
-                                       } );
+               $( '.config-help-field-data' ).hide()
+                       .closest( '.config-help-field-container' ).find( '.config-help-field-hint' )
+                       .show()
+                       .click( function () {
+                               $( this ).closest( '.config-help-field-container' ).find( '.config-help-field-data' )
+                                       .slideToggle( 'fast' );
+                       } );
 
                // Show/hide code for DB-specific options
                // FIXME: Do we want slow, fast, or even non-animated (instantaneous) showing/hiding here?
index e91f58b..8507238 100644 (file)
@@ -14,7 +14,7 @@
     "grunt-banana-checker": "0.6.0",
     "grunt-contrib-copy": "1.0.0",
     "grunt-contrib-watch": "1.0.0",
-    "grunt-eslint": "19.0.0",
+    "grunt-eslint": "20.0.0",
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
     "grunt-stylelint": "0.8.0",
index 7773866..40b3baf 100644 (file)
@@ -64,8 +64,7 @@
                        if ( $badge.length ) {
                                $badge
                                        .toggleClass( 'mw-badge-important', isImportant )
-                                       .find( '.mw-badge-content' )
-                                               .text( text );
+                                       .find( '.mw-badge-content' ).text( text );
                        } else {
                                // Otherwise, create a new badge with the specified text and style
                                $badge = $( '<div class="mw-badge"></div>' )
index 2be1dba..a5b136d 100644 (file)
@@ -42,7 +42,7 @@
 
                        // Look for rgb(num%,num%,num%)
                        // eslint-disable-next-line no-cond-assign
-                       if ( result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec( color ) ) {
+                       if ( result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*\)/.exec( color ) ) {
                                return [
                                        parseFloat( result[ 1 ] ) * 2.55,
                                        parseFloat( result[ 2 ] ) * 2.55,
index 5ce9b1f..aa76d6d 100644 (file)
                        buildDefaultToggleLink = function () {
                                return $( '<a class="mw-collapsible-text"></a>' )
                                        .text( collapseText )
-                                       .wrap( '<span class="mw-collapsible-toggle"></span>' ).parent()
-                                               .attr( {
-                                                       role: 'button',
-                                                       tabindex: 0
-                                               } )
-                                               .prepend( '<span>[</span>' )
-                                               .append( '<span>]</span>' )
-                                               .on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
+                                       .wrap( '<span class="mw-collapsible-toggle"></span>' )
+                                       .parent()
+                                       .attr( {
+                                               role: 'button',
+                                               tabindex: 0
+                                       } )
+                                       .prepend( '<span>[</span>' )
+                                       .append( '<span>]</span>' )
+                                       .on( 'click.mw-collapsible keypress.mw-collapsible', actionHandler );
                        };
 
                        // Check if this element has a custom position for the toggle link
index 6d478bd..4bcccdd 100644 (file)
@@ -11,7 +11,7 @@
                },
                trimRight: function ( str ) {
                        return str === null ?
-                                       '' : str.toString().replace( /\s+$/, '' );
+                               '' : str.toString().replace( /\s+$/, '' );
                },
                ucFirst: function ( str ) {
                        return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
        } );
 
        mw.log.deprecate( $, 'escapeRE', function ( str ) {
-               return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+               return str.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ); // eslint-disable-line no-useless-escape
        }, 'Use mediawiki.RegExp instead.' );
 
 }( jQuery, mediaWiki ) );
index 75f1ba6..4f4edc9 100644 (file)
@@ -19,7 +19,6 @@
  * @class jQuery.plugin.suggestions
  */
 
- // jscs:disable checkParamNames
 /**
  * @method suggestions
  * @chainable
@@ -94,7 +93,6 @@
  * @param {boolean} [options.highlightInput=false] Whether to highlight matched portions of the
  *  input or not.
  */
- // jscs:enable checkParamNames
 
 ( function ( $, mw ) {
 
                                                        27, // escape
                                                        13, // enter
                                                        46, // delete
-                                                       8   // backspace
+                                                       8 //   backspace
                                                ];
                                                if ( context.data.keypressedCount === 0 &&
                                                        e.which === context.data.keypressed &&
index 8d019e5..ec91773 100644 (file)
 
                // Build RegEx
                // Any date formated with . , ' - or /
-               ts.dateRegex[ 0 ] = new RegExp( /^\s*(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{1,2})[\,\.\-\/'\s]{1,2}(\d{2,4})\s*?/i );
+               ts.dateRegex[ 0 ] = new RegExp( /^\s*(\d{1,2})[,.\-/'\s]{1,2}(\d{1,2})[,.\-/'\s]{1,2}(\d{2,4})\s*?/i );
 
                // Written Month name, dmy
                ts.dateRegex[ 1 ] = new RegExp(
                        }
 
                        columnToCell = [];
-                       cellsInRow = ( $row[ 0 ].cells.length ) || 0;  // all cells in this row
+                       cellsInRow = ( $row[ 0 ].cells.length ) || 0; // all cells in this row
                        index = 0; // real cell index in this row
                        for ( j = 0; j < columns; index++ ) {
                                if ( index === cellsInRow ) {
                }
                ts.rgx = {
                        IPAddress: [
-                               new RegExp( /^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/ )
+                               new RegExp( /^\d{1,3}[.]\d{1,3}[.]\d{1,3}[.]\d{1,3}$/ )
                        ],
                        currency: [
                                new RegExp( /(^[£$€¥]|[£$€¥]$)/ ),
                                new RegExp( /(https?|ftp|file):\/\// )
                        ],
                        isoDate: [
-                               new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)([T\s]((([01]\d|2[0-3])(:?[0-5]\d)?|24:?00)?(:?([0-5]\d|60))?([.,]\d+)?)([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ),
+                               new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)([T\s]((([01]\d|2[0-3])(:?[0-5]\d)?|24:?00)?(:?([0-5]\d|60))?([.,]\d+)?)([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?/ ),
                                new RegExp( /^([-+]?\d{1,4})-([01]\d)-([0-3]\d)/ )
                        ],
                        usLongDate: [
                        return getParserById( id );
                },
 
-               getParsers: function () {  // for table diagnosis
+               getParsers: function () { // for table diagnosis
                        return parsers;
                }
        };
index 53c1fbb..2b6fc9d 100644 (file)
                                        role: 'navigation',
                                        'aria-labelledby': 'p-lang-label'
                                } )
-                               .append( $( '<h3>' ).attr( 'id', 'p-lang-label' ).text( mw.msg( 'otherlanguages' ) ) )
-                               .append( $( '<div>' ).addClass( 'body' ).append( '<ul>' ) )
+                                       .append( $( '<h3>' ).attr( 'id', 'p-lang-label' ).text( mw.msg( 'otherlanguages' ) ) )
+                                       .append( $( '<div>' ).addClass( 'body' ).append( '<ul>' ) )
                        );
                }
 
index b3b0af2..6f49fa6 100644 (file)
@@ -23,8 +23,7 @@ jQuery( function ( $ ) {
                        return true;
                }
 
-               $lis
-               .each( function () {
+               $lis.each( function () {
                        $li = $( this );
                        $inputs = $li.find( 'input[type="radio"]' );
                        $oldidRadio = $inputs.filter( '[name="oldid"]' ).eq( 0 );
@@ -97,8 +96,7 @@ jQuery( function ( $ ) {
                        // Also remove potentially conflicting id attributes that we don't need anyway
                        $copyForm
                                .css( 'display', 'none' )
-                               .find( '[id]' )
-                                       .removeAttr( 'id' )
+                               .find( '[id]' ).removeAttr( 'id' )
                                .end()
                                .insertAfter( $historyCompareForm )
                                .submit();
index a3a82d5..0d000c9 100644 (file)
                $col = $( '<td colspan="2"></td>' );
 
                $link = $( '<a>' )
-               .text( showText )
-               .attr( {
-                       role: 'button',
-                       tabindex: 0
-               } )
-               .on( 'click keypress', function ( e ) {
-                       if (
-                               e.type === 'click' ||
-                               e.type === 'keypress' && e.which === 13
-                       ) {
-                               if ( $table.hasClass( 'collapsed' ) ) {
-                                       $( this ).text( hideText );
-                               } else {
-                                       $( this ).text( showText );
+                       .text( showText )
+                       .attr( {
+                               role: 'button',
+                               tabindex: 0
+                       } )
+                       .on( 'click keypress', function ( e ) {
+                               if (
+                                       e.type === 'click' ||
+                                       e.type === 'keypress' && e.which === 13
+                               ) {
+                                       if ( $table.hasClass( 'collapsed' ) ) {
+                                               $( this ).text( hideText );
+                                       } else {
+                                               $( this ).text( showText );
+                                       }
+                                       $table.toggleClass( 'expanded collapsed' );
                                }
-                               $table.toggleClass( 'expanded collapsed' );
-                       }
-               } );
+                       } );
 
                $col.append( $link );
                $row.append( $col );
index 2307f30..4915803 100644 (file)
 
                        // Go over the items and define the correct values
                        $.each( filterRepresentation, function ( name, value ) {
+                               // We must store all parameter values as strings '0' or '1'
                                result[ filterParamNames[ name ] ] = areAnySelected ?
-                                       // We must store all parameter values as strings '0' or '1'
                                        String( Number( !value ) ) :
                                        '0';
                        } );
                        $.each( paramRepresentation, function ( paramName, paramValue ) {
                                var filterItem = paramToFilterMap[ paramName ];
 
+                               // Flip the definition between the parameter
+                               // state and the filter state
+                               // This is what the 'toggleSelected' value of the filter is
                                result[ filterItem.getName() ] = areAnySelected ?
-                                       // Flip the definition between the parameter
-                                       // state and the filter state
-                                       // This is what the 'toggleSelected' value of the filter is
                                        !Number( paramValue ) :
                                        // Otherwise, there are no selected items in the
                                        // group, which means the state is false
                        );
                        // Translate the parameter values into a filter selection state
                        this.getItems().forEach( function ( filterItem ) {
+                               // All true (either because all values are written or the term 'all' is written)
+                               // is the same as all filters set to true
                                result[ filterItem.getName() ] = (
-                                               // If it is the word 'all'
-                                               paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
-                                               // All values are written
-                                               paramValues.length === model.getItemCount()
-                                       ) ?
-                                       // All true (either because all values are written or the term 'all' is written)
-                                       // is the same as all filters set to true
+                                       // If it is the word 'all'
+                                       paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
+                                       // All values are written
+                                       paramValues.length === model.getItemCount()
+                               ) ?
                                        true :
                                        // Otherwise, the filter is selected only if it appears in the parameter values
                                        paramValues.indexOf( filterItem.getParamName() ) > -1;
index 75431d9..06fa0aa 100644 (file)
                } );
        };
 
+       /**
+        * Get all selected items
+        *
+        * @return {mw.rcfilters.dm.FilterItem[]} Selected items
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedItems = function () {
+               var allSelected = [];
+
+               $.each( this.getFilterGroups(), function ( groupName, groupModel ) {
+                       allSelected = allSelected.concat( groupModel.getSelectedItems() );
+               } );
+
+               return allSelected;
+       };
        /**
         * Switch the current view
         *
index 5858566..a9283b9 100644 (file)
@@ -15,6 +15,8 @@
                this.baseFilterState = {};
                this.uriProcessor = null;
                this.initializing = false;
+
+               this.prevLoggedItems = [];
        };
 
        /* Initialization */
                        this.filtersModel.toggleFilterSelected( filterName, false );
                        this.updateChangesList();
                        this.filtersModel.reassessFilterInteractions( filterItem );
+
+                       // Log filter grouping
+                       this.trackFilterGroupings( 'removefilter' );
                }
 
                if ( isHighlighted ) {
                        this.filtersModel.reassessFilterInteractions();
 
                        this.updateChangesList();
+
+                       // Log filter grouping
+                       this.trackFilterGroupings( 'savedfilters' );
                }
        };
 
                );
        };
 
+       /**
+        * Track filter grouping usage
+        *
+        * @param {string} action Action taken
+        */
+       mw.rcfilters.Controller.prototype.trackFilterGroupings = function ( action ) {
+               var controller = this,
+                       rightNow = new Date().getTime(),
+                       randomIdentifier = String( mw.user.sessionId() ) + String( rightNow ) + String( Math.random() ),
+                       // Get all current filters
+                       filters = this.filtersModel.getSelectedItems().map( function ( item ) {
+                               return item.getName();
+                       } );
+
+               action = action || 'filtermenu';
+
+               // Check if these filters were the ones we just logged previously
+               // (Don't log the same grouping twice, in case the user opens/closes)
+               // the menu without action, or with the same result
+               if (
+                       // Only log if the two arrays are different in size
+                       filters.length !== this.prevLoggedItems.length ||
+                       // Or if any filters are not the same as the cached filters
+                       filters.some( function ( filterName ) {
+                               return controller.prevLoggedItems.indexOf( filterName ) === -1;
+                       } ) ||
+                       // Or if any cached filters are not the same as given filters
+                       this.prevLoggedItems.some( function ( filterName ) {
+                               return filters.indexOf( filterName ) === -1;
+                       } )
+               ) {
+                       filters.forEach( function ( filterName ) {
+                               mw.track(
+                                       'event.ChangesListFilterGrouping',
+                                       {
+                                               action: action,
+                                               groupIdentifier: randomIdentifier,
+                                               filter: filterName,
+                                               userId: mw.user.getId()
+                                       }
+                               );
+                       } );
+
+                       // Cache the filter names
+                       this.prevLoggedItems = filters;
+               }
+       };
 }( mediaWiki, jQuery ) );
index 9f41712..88479c3 100644 (file)
@@ -72,7 +72,6 @@
                                classes: [ 'mw-rcfilters-ui-filterMenuSectionOptionWidget-whatsThisButton' ],
                                flags: [ 'progressive' ],
                                popup: {
-                                       $autoCloseIgnore: this.$element.add( this.$overlay ),
                                        padded: false,
                                        align: 'center',
                                        position: 'above',
index dcada85..305456a 100644 (file)
                                // Clear the input
                                this.input.setValue( '' );
                        }
+
+                       // Log filter grouping
+                       this.controller.trackFilterGroupings( 'filtermenu' );
                }
 
                this.input.setIcon( isVisible ? 'search' : 'menu' );
index 64b9ac9..07d4506 100644 (file)
@@ -34,8 +34,7 @@
 
                this.inputValue = '';
                this.$overlay = config.$overlay || this.$element;
-               this.$body = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
+               this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
                this.footers = [];
 
                // Parent
index 84a9a96..63f2d98 100644 (file)
@@ -21,8 +21,8 @@
                                'aria-labelledby': labelFunc
                        } );
                $fieldsets.not( '#mw-prefsection-personal' )
-                               .hide()
-                               .attr( 'aria-hidden', 'true' );
+                       .hide()
+                       .attr( 'aria-hidden', 'true' );
 
                // T115692: The following is kept for backwards compatibility with older skins
                $preferences.addClass( 'jsprefs' );
@@ -72,8 +72,7 @@
                                $tab.attr( {
                                        tabIndex: 0,
                                        'aria-selected': 'true'
-                               } )
-                               .focus()
+                               } ).focus()
                                        .parent().addClass( 'selected' );
 
                                $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
                function detectHash() {
                        var hash = location.hash,
                                matchedElement, parentSection;
-                       if ( hash.match( /^#mw-prefsection-[\w\-]+/ ) ) {
+                       if ( hash.match( /^#mw-prefsection-[\w-]+/ ) ) {
                                mw.storage.session.remove( 'mwpreferences-prevTab' );
                                switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
-                       } else if ( hash.match( /^#mw-[\w\-]+/ ) ) {
+                       } else if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                matchedElement = document.getElementById( hash.slice( 1 ) );
                                parentSection = $( matchedElement ).closest( '.prefsection' );
                                if ( parentSection.length ) {
                ) {
                        $( window ).on( 'hashchange', function () {
                                var hash = location.hash;
-                               if ( hash.match( /^#mw-[\w\-]+/ ) ) {
+                               if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                        detectHash();
                                } else if ( hash === '' ) {
                                        switchPrefTab( 'personal', 'noHash' );
                                }
                        } )
-                       // Run the function immediately to select the proper tab on startup.
-                       .trigger( 'hashchange' );
+                               // Run the function immediately to select the proper tab on startup.
+                               .trigger( 'hashchange' );
                // In older browsers we'll bind a click handler as fallback.
                // We must not have onhashchange *and* the click handlers, otherwise
                // the click handler calls switchPrefTab() which sets the hash value,
index 9518283..648bf67 100644 (file)
@@ -56,8 +56,7 @@
                piprop: 'thumbnail',
                pithumbsize: 300,
                formatversion: 2
-       } )
-       .done( function ( resp ) {
+       } ).done( function ( resp ) {
                var results = ( resp.query && resp.query.pages ) ? resp.query.pages : false,
                        multimediaWidgetTemplate;
 
index ad49a42..a7a3bd3 100644 (file)
                if ( v.normalize ) {
                        v = v.normalize();
                }
-               re = new RegExp( '^\\s*' + v.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ), 'i' );
+               re = new RegExp( '^\\s*' + v.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ), 'i' ); // eslint-disable-line no-useless-escape
                for ( k in this.values ) {
                        k = +k;
                        if ( !isNaN( k ) && re.test( this.values[ k ] ) ) {
index 1cc168a..08266f0 100644 (file)
                var queryValue = this.query.getValue().trim();
 
                if ( queryValue.match( this.externalLinkUrlProtocolsRegExp ) ) {
-                       queryValue = queryValue.match( /.+\/([^\/]+)/ )[ 1 ];
+                       queryValue = queryValue.match( /.+\/([^/]+)/ )[ 1 ];
                }
                return queryValue;
        };
index c5a2dd4..b91617e 100644 (file)
                                        );
                                        currentMonth.add( 1, 'month' );
                                }
-                               // Shuffle the array to display months in columns rather than rows.
+                               // Shuffle the array to display months in columns rather than rows:
+                               // | Jan | Jul |
+                               // | Feb | Aug |
+                               // | Mar | Sep |
+                               // | Apr | Oct |
+                               // | May | Nov |
+                               // | Jun | Dec |
                                items = [
-                                       items[ 0 ], items[ 6 ],      //  | January  | July      |
-                                       items[ 1 ], items[ 7 ],      //  | February | August    |
-                                       items[ 2 ], items[ 8 ],      //  | March    | September |
-                                       items[ 3 ], items[ 9 ],      //  | April    | October   |
-                                       items[ 4 ], items[ 10 ],     //  | May      | November  |
-                                       items[ 5 ], items[ 11 ]      //  | June     | December  |
+                                       items[ 0 ], items[ 6 ],
+                                       items[ 1 ], items[ 7 ],
+                                       items[ 2 ], items[ 8 ],
+                                       items[ 3 ], items[ 9 ],
+                                       items[ 4 ], items[ 10 ],
+                                       items[ 5 ], items[ 11 ]
                                ];
                                break;
 
index cdc4dbf..322143d 100644 (file)
@@ -19,8 +19,7 @@
                                title: String( page ),
                                user: user,
                                uselang: mw.config.get( 'wgUserLanguage' )
-                       }, params ) )
-                       .then( function ( data ) {
+                       }, params ) ).then( function ( data ) {
                                return data.rollback;
                        } );
                }
index 219dfb8..4a2895d 100644 (file)
 
                        upload = this.uploadWithFormData( file, data );
                        return upload.then(
-                                       null,
-                                       // If the call fails, we may want to try again...
-                                       retries === 0 ? null : retry,
-                                       function ( fraction ) {
-                                               // Since we're only uploading small parts of a file, we
-                                               // need to adjust the reported progress to reflect where
-                                               // we actually are in the combined upload
-                                               return ( start + fraction * ( end - start ) ) / file.size;
-                                       }
-                               ).promise( { abort: upload.abort } );
+                               null,
+                               // If the call fails, we may want to try again...
+                               retries === 0 ? null : retry,
+                               function ( fraction ) {
+                                       // Since we're only uploading small parts of a file, we
+                                       // need to adjust the reported progress to reflect where
+                                       // we actually are in the combined upload
+                                       return ( start + fraction * ( end - start ) ) / file.size;
+                               }
+                       ).promise( { abort: upload.abort } );
                },
 
                /**
index 3bf75ae..6d3c9fd 100644 (file)
@@ -20,7 +20,7 @@
         */
        function hideIfGetField( $el, name ) {
                var $found, $p, $widget,
-                       suffix = name.replace( /^([^\[]+)/, '[$1]' );
+                       suffix = name.replace( /^([^[]+)/, '[$1]' );
 
                function nameFilter() {
                        return this.name === name ||
index 7a1fa7f..ac2bb02 100644 (file)
         */
        mw.ForeignStructuredUpload.BookletLayout.prototype.saveFile = function () {
                var title = mw.Title.newFromText(
-                               this.getFilename(),
-                               mw.config.get( 'wgNamespaceIds' ).file
-                       );
+                       this.getFilename(),
+                       mw.config.get( 'wgNamespaceIds' ).file
+               );
 
                return this.uploadPromise
                        .then( this.validateFilename.bind( this, title ) )
index 1da4ab4..91cdc2d 100644 (file)
@@ -16,7 +16,7 @@
                 * @return {string} Escaped string
                 */
                escape: function ( str ) {
-                       return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+                       return str.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' ); // eslint-disable-line no-useless-escape
                }
        };
 }( mediaWiki ) );
index 253e0ef..398adbb 100644 (file)
                        },
                        // brackets, greater than
                        {
-                               pattern: /[\]\}>]/g,
+                               pattern: /[}\]>]/g,
                                replace: ')',
                                generalRule: true
                        },
                        // brackets, lower than
                        {
-                               pattern: /[\[\{<]/g,
+                               pattern: /[{[<]/g,
                                replace: '(',
                                generalRule: true
                        },
                }
 
                // Any remaining initial :s are illegal.
-               title = title.replace( /^\:+/, '' );
+               title = title.replace( /^:+/, '' );
 
                return Title.newFromText( title, namespace );
        };
                        thumbPhpRegex = /thumb\.php/,
                        regexes = [
                                // Thumbnails
-                               /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)\/[^\s\/]+-[^\s\/]*$/,
+                               /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s/]+)\/[^\s/]+-[^\s/]*$/,
 
                                // Full size images
-                               /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s\/]+)$/,
+                               /\/[a-f0-9]\/[a-f0-9]{2}\/([^\s/]+)$/,
 
                                // Thumbnails in non-hashed upload directories
-                               /\/([^\s\/]+)\/[^\s\/]+-(?:\1|thumbnail)[^\s\/]*$/,
+                               /\/([^\s/]+)\/[^\s/]+-(?:\1|thumbnail)[^\s/]*$/,
 
                                // Full-size images in non-hashed upload directories
-                               /\/([^\s\/]+)$/
+                               /\/([^\s/]+)$/
                        ],
 
                        recount = regexes.length;
index 3c1a668..939b841 100644 (file)
                                return $( '<div>' ).prop( {
                                        id: 'mw-debug-' + id,
                                        className: 'mw-debug-bit'
-                               } )
-                               .appendTo( $bits );
+                               } ).appendTo( $bits );
                        }
 
                        /**
                                        id: 'mw-debug-' + id,
                                        className: 'mw-debug-bit mw-debug-panelink'
                                } )
-                               .append( paneLabel( id, text ) )
-                               .appendTo( $bits );
+                                       .append( paneLabel( id, text ) )
+                                       .appendTo( $bits );
                        }
 
                        paneTriggerBitDiv( 'console', 'Console', this.data.log.length );
                                .append( $( '<th>SQL</th>' ) )
                                .append( $( '<th>Time</th>' ).css( 'width', '8em' ) )
                                .append( $( '<th>Call</th>' ).css( 'width', '18em' ) )
-                       .appendTo( $table );
+                               .appendTo( $table );
 
                        for ( i = 0, length = this.data.queries.length; i < length; i += 1 ) {
                                query = this.data.queries[ i ];
                                        .append( $( '<td>' ).text( query.sql ) )
                                        .append( $( '<td class="stats">' ).text( ( query.time * 1000 ).toFixed( 4 ) + 'ms' ) )
                                        .append( $( '<td>' ).text( query[ 'function' ] ) )
-                               .appendTo( $table );
+                                       .appendTo( $table );
                        }
 
                        return $table;
index f0e13b4..2d55094 100644 (file)
         */
        mw.Feedback.Dialog.prototype.validateFeedbackForm = function () {
                var isValid = (
-                               (
-                                       !this.useragentMandatory ||
-                                       this.useragentCheckbox.isSelected()
-                               ) &&
-                               this.feedbackSubjectInput.getValue()
-                       );
+                       (
+                               !this.useragentMandatory ||
+                               this.useragentCheckbox.isSelected()
+                       ) &&
+                       this.feedbackSubjectInput.getValue()
+               );
 
                this.actions.setAbilities( { submit: isValid } );
        };
index 6d3b4f0..e1681fa 100644 (file)
                        // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
                        // This may be because, to save code, memoization was removed
 
+                       /* eslint-disable no-useless-escape */
                        regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
                        regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
                        regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
                        regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
+                       /* eslint-enable no-useless-escape */
 
                        backslash = makeStringParser( '\\' );
                        doubleQuote = makeStringParser( '"' );
                        templateName = transform(
                                // see $wgLegalTitleChars
                                // not allowing : due to the need to catch "PLURAL:$1"
-                               makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
+                               makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ),
                                function ( result ) { return result.toString(); }
                        );
                        function templateParam() {
                                        $el.attr( {
                                                role: 'button',
                                                tabindex: 0
-                                       } )
-                                       .on( 'click keypress', function ( e ) {
+                                       } ).on( 'click keypress', function ( e ) {
                                                if (
                                                        e.type === 'click' ||
                                                        e.type === 'keypress' && e.which === 13
        // Replace the default message parser with jqueryMsg
        oldParser = mw.Message.prototype.parser;
        mw.Message.prototype.parser = function () {
-               if ( this.format === 'plain' || !/\{\{|[\[<>&]/.test( this.map.get( this.key ) ) ) {
+               if ( this.format === 'plain' || !/\{\{|[<>[&]/.test( this.map.get( this.key ) ) ) {
                        // Fall back to mw.msg's simple parser
                        return oldParser.apply( this );
                }
index 18f7f0a..c5989c0 100644 (file)
        function setGlobalMapValue( map, key, value ) {
                map.values[ key ] = value;
                log.deprecate(
-                               window,
-                               key,
-                               value,
-                               // Deprecation notice for mw.config globals (T58550, T72470)
-                               map === mw.config && 'Use mw.config instead.'
+                       window,
+                       key,
+                       value,
+                       // Deprecation notice for mw.config globals (T58550, T72470)
+                       map === mw.config && 'Use mw.config instead.'
                );
        }
 
index d36c4a0..dfacfc6 100644 (file)
@@ -35,7 +35,7 @@
 
                if ( options.tag ) {
                        // Sanitize options.tag before it is used by any code. (Including Notification class methods)
-                       options.tag = options.tag.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
+                       options.tag = options.tag.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
                        if ( options.tag ) {
                                $notification.addClass( 'mw-notification-tag-' + options.tag );
                        } else {
@@ -45,7 +45,7 @@
 
                if ( options.type ) {
                        // Sanitize options.type
-                       options.type = options.type.replace( /[ _\-]+/g, '-' ).replace( /[^\-a-z0-9]+/ig, '' );
+                       options.type = options.type.replace( /[ _-]+/g, '-' ).replace( /[^-a-z0-9]+/ig, '' );
                        $notification.addClass( 'mw-notification-type-' + options.type );
                }
 
index 6a6aa15..650092b 100644 (file)
@@ -44,8 +44,7 @@
         *  by that time.
         */
        mw.requestIdleCallback = window.requestIdleCallback ?
-               // Bind because it throws TypeError if context is not window
-               window.requestIdleCallback.bind( window ) :
+               window.requestIdleCallback.bind( window ) : // Bind because it throws TypeError if context is not window
                mw.requestIdleCallbackInternal;
        // Note: Polyfill was previously disabled due to
        // https://bugs.chromium.org/p/chromium/issues/detail?id=647870
index bcb6c33..1c1150e 100644 (file)
@@ -87,9 +87,9 @@
                 */
                function getInputLocation( context ) {
                        return context.config.$region
-                                       .closest( 'form' )
-                                       .find( '[data-search-loc]' )
-                                       .data( 'search-loc' ) || 'header';
+                               .closest( 'form' )
+                               .find( '[data-search-loc]' )
+                               .data( 'search-loc' ) || 'header';
                }
 
                /**
                                var $this = $( this );
                                $this
                                        .data( 'suggestions-context' )
-                                       .data.$container
-                                               .css( 'fontSize', $this.css( 'fontSize' ) );
+                                       .data.$container.css( 'fontSize', $this.css( 'fontSize' ) );
                        } );
 
                // Ensure that the thing is actually present!
index 53e8d93..5e10a5b 100644 (file)
@@ -45,8 +45,8 @@
                                        $tocToggleLink
                                                .wrap( '<span class="toctoggle"></span>' )
                                                .parent()
-                                                       .prepend( '&nbsp;[' )
-                                                       .append( ']&nbsp;' )
+                                               .prepend( '&nbsp;[' )
+                                               .append( ']&nbsp;' )
                                );
 
                                if ( hideToc ) {
index 6d6d46d..d8fb249 100644 (file)
@@ -33,8 +33,7 @@
                                formatversion: 2,
                                action: 'patrol',
                                rcid: rcid
-                       } )
-                       .done( function ( data ) {
+                       } ).done( function ( data ) {
                                var title;
                                // Remove all patrollinks from the page (including any spinners inside).
                                $patrolLinks.closest( '.patrollink' ).remove();
@@ -46,8 +45,7 @@
                                        // This should never happen as errors should trigger fail
                                        mw.notify( mw.msg( 'markedaspatrollederrornotify' ), { type: 'error' } );
                                }
-                       } )
-                       .fail( function ( error ) {
+                       } ).fail( function ( error ) {
                                $spinner.remove();
                                // Restore the patrol link. This allows the user to try again
                                // (or open it in a new window, bypassing this ajax module).
index d94b158..6db518d 100644 (file)
@@ -45,8 +45,8 @@
                                        $( e.delegateTarget ).remove();
                                }, function ( errorCode, data ) {
                                        var message = data && data.error && data.error.messageHtml ?
-                                               $.parseHTML( data.error.messageHtml ) :
-                                               mw.msg( 'rollbackfailed' ),
+                                                       $.parseHTML( data.error.messageHtml ) :
+                                                       mw.msg( 'rollbackfailed' ),
                                                type = errorCode === 'alreadyrolled' ? 'warn' : 'error';
 
                                        mw.notify( message, {
index e2209eb..4070bc0 100644 (file)
@@ -37,11 +37,8 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                return $mock;
        }
 
-       /** helper to test SpecialRecentchanges::buildMainQueryConds() */
-       private function assertConditions(
-               $expected,
+       private function buildQuery(
                $requestOptions = null,
-               $message = '',
                $user = null
        ) {
                $context = new RequestContext;
@@ -81,6 +78,18 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                        'ChangesListSpecialPageTest::filterOutRcTimestampCondition'
                );
 
+               return $queryConditions;
+       }
+
+       /** helper to test SpecialRecentchanges::buildQuery() */
+       private function assertConditions(
+               $expected,
+               $requestOptions = null,
+               $message = '',
+               $user = null
+       ) {
+               $queryConditions = $this->buildQuery( $requestOptions, $user );
+
                $this->assertEquals(
                        self::normalizeCondition( $expected ),
                        self::normalizeCondition( $queryConditions ),
@@ -373,6 +382,104 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                );
        }
 
+       public function testFilterUserExpLevelAll() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered;newcomer;learner;experienced',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered;newcomer;learner;experienced"
+               );
+       }
+
+       public function testFilterUserExpLevelRegisteredUnregistered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered"
+               );
+       }
+
+       public function testFilterUserExpLevelRegisteredUnregisteredLearner() {
+               $this->assertConditions(
+                       [
+                               # expected
+                       ],
+                       [
+                               'userExpLevel' => 'registered;unregistered;learner',
+                       ],
+                       "rc conditions: userExpLevel=registered;unregistered;learner"
+               );
+       }
+
+       public function testFilterUserExpLevelAllExperienceLevels() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'newcomer;learner;experienced',
+                       ],
+                       "rc conditions: userExpLevel=newcomer;learner;experienced"
+               );
+       }
+
+       public function testFilterUserExpLevelRegistrered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'registered',
+                       ],
+                       "rc conditions: userExpLevel=registered"
+               );
+       }
+
+       public function testFilterUserExpLevelUnregistrered() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user' => 0,
+                       ],
+                       [
+                               'userExpLevel' => 'unregistered',
+                       ],
+                       "rc conditions: userExpLevel=unregistered"
+               );
+       }
+
+       public function testFilterUserExpLevelRegistreredOrLearner() {
+               $this->assertConditions(
+                       [
+                               # expected
+                               'rc_user != 0',
+                       ],
+                       [
+                               'userExpLevel' => 'registered;learner',
+                       ],
+                       "rc conditions: userExpLevel=registered;learner"
+               );
+       }
+
+       public function testFilterUserExpLevelUnregistreredOrExperienced() {
+               $conds = $this->buildQuery( [ 'userExpLevel' => 'unregistered;experienced' ] );
+
+               $this->assertRegExp(
+                       '/\(rc_user = 0\) OR \(\(user_editcount >= 500\) AND \(user_registration <= \'\d+\'\)\)/',
+                       reset( $conds ),
+                       "rc conditions: userExpLevel=unregistered;experienced"
+               );
+       }
+
        public function testFilterUserExpLevel() {
                $now = time();
                $this->setMwGlobals( [
@@ -438,18 +545,6 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                        $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
                        'Learner and more experienced'
                );
-
-               // newcomers, learner, and more experienced
-               // TOOD: Fix test.  This needs to test that anons are excluded,
-               // and right now the join fails.
-               /* $this->assertArrayEquals( */
-               /*      [ */
-               /*              'Newcomer1', 'Newcomer2', 'Newcomer3', */
-               /*              'Learner1', 'Learner2', 'Learner3', 'Learner4', */
-               /*              'Experienced1', */
-               /*      ], */
-               /*      $this->fetchUsers( [ 'newcomer', 'learner', 'experienced' ], $now ) */
-               /* ); */
        }
 
        private function createUsers( $specs, $now ) {
@@ -798,7 +893,7 @@ class ChangesListSpecialPageTest extends AbstractChangesListSpecialPageTestCase
                                        "hideliu" => true,
                                        "userExpLevel" => "newcomer",
                                ],
-                               "expectedConflicts" => true,
+                               "expectedConflicts" => false,
                        ],
                        [
                                "parameters" => [
index 5c1be67..8555a7e 100644 (file)
        byteLimitTest( {
                description: 'Input filter that increases the length',
                $input: $( '<input>' ).attr( 'type', 'text' )
-               .byteLimit( 10, function ( text ) {
-                       return 'prefix' + text;
-               } ),
+                       .byteLimit( 10, function ( text ) {
+                               return 'prefix' + text;
+                       } ),
                sample: simpleSample,
                // Prefix adds 6 characters, limit is reached after 4
                expected: '1234'
        byteLimitTest( {
                description: 'Input filter of which the base exceeds the limit',
                $input: $( '<input>' ).attr( 'type', 'text' )
-               .byteLimit( 3, function ( text ) {
-                       return 'prefix' + text;
-               } ),
+                       .byteLimit( 3, function ( text ) {
+                               return 'prefix' + text;
+                       } ),
                sample: simpleSample,
                hasLimit: true,
                limit: 6, // 'prefix' length
index 53d29cf..0c91e43 100644 (file)
 
        QUnit.test( 'mw-made-collapsible data added', function ( assert ) {
                var $collapsible = prepareCollapsible(
-                               '<div>' + loremIpsum + '</div>'
-                       );
+                       '<div>' + loremIpsum + '</div>'
+               );
 
                assert.equal( $collapsible.data( 'mw-made-collapsible' ), true, 'mw-made-collapsible data present' );
        } );
 
        QUnit.test( 'mw-collapsible added when missing', function ( assert ) {
                var $collapsible = prepareCollapsible(
-                               '<div>' + loremIpsum + '</div>'
-                       );
+                       '<div>' + loremIpsum + '</div>'
+               );
 
                assert.assertTrue( $collapsible.hasClass( 'mw-collapsible' ), 'mw-collapsible class present' );
        } );
        QUnit.test( 'mw-collapsed added when missing', function ( assert ) {
                var $collapsible = prepareCollapsible(
                        '<div>' + loremIpsum + '</div>',
-                               { collapsed: true }
-                       );
+                       { collapsed: true }
+               );
 
                assert.assertTrue( $collapsible.hasClass( 'mw-collapsed' ), 'mw-collapsed class present' );
        } );
index 854e4b1..4ee8038 100644 (file)
                                'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C'
                        ] ) !== -1 ) {
                                assert.ok( true, 'Repond to ' + request.requestBody );
-                               request.respond( 200, { 'Content-Type': 'application/json' },
-                                               '{ "options": "success" }' );
+                               request.respond(
+                                       200,
+                                       { 'Content-Type': 'application/json' },
+                                       '{ "options": "success" }'
+                               );
                        } else {
                                assert.ok( false, 'Unexpected request: ' + request.requestBody );
                        }
index e10a7fa..2361f70 100644 (file)
                                        'X-Foo': 'Bar'
                                }
                        }
-               )
-               .then( function () {
+               ).then( function () {
                        assert.equal( test.server.requests[ 0 ].requestHeaders[ 'X-Foo' ], 'Bar', 'Header sent' );
 
                        return api.postWithToken( 'csrf',
                                        assert.ok( false, 'This parameter cannot be a callback' );
                                }
                        );
-               } )
-               .then( function ( data ) {
+               } ).then( function ( data ) {
                        assert.equal( data.example, 'quux' );
 
                        assert.equal( test.server.requests.length, 2, 'Request made' );
index 3c77a00..db51fb3 100644 (file)
                );
                assert.equal(
                        formatParse( 'external-link-plural', 2, 'http://example.org' ),
-                       'Foo <a href=\"http://example.org\">two</a> things.',
+                       'Foo <a href="http://example.org">two</a> things.',
                        'Link is expanded inside an explicit plural form and is not escaped html'
                );
                assert.equal(
index 30654fa..985ff92 100644 (file)
                mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] );
 
                return mw.loader.using( 'test.promise' )
-               .done( function () {
-                       assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
-                       delete mw.loader.testCallback;
-               } )
-               .fail( function () {
-                       assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
-               } );
+                       .done( function () {
+                               assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
+                               delete mw.loader.testCallback;
+                       } )
+                       .fail( function () {
+                               assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
+                       } );
        } );
 
        // Covers mw.loader#sortDependencies (with native Set if available)
                                assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
                        }
                )
-               .always( done );
+                       .always( done );
        } );
 
        // @covers mw.loader#sortDependencies (with fallback shim)
                                assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
                        }
                )
-               .always( done );
+                       .always( done );
        } );
 
        QUnit.test( '.load() - Error: Circular dependency', function ( assert ) {
                                }
                        };
                } );
-               return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
-               .then( function ( require ) {
+               return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] ).then( function ( require ) {
                        var module1, module2, module3, module4;
 
                        module1 = require( 'test.require1' );
index c8fb8a7..f3e4877 100644 (file)
@@ -1,7 +1,6 @@
-/* eslint comma-dangle: 0 */
-/* eslint no-undef: "error" */
-/* eslint no-console: 0 */
 /* eslint-env node */
+/* eslint no-undef: "error" */
+/* eslint-disable no-console, comma-dangle */
 'use strict';
 
 const path = require( 'path' );
@@ -114,12 +113,12 @@ exports.config = {
        // with "/", then the base url gets prepended.
        baseUrl: (
                process.env.MW_SERVER === undefined ?
-               'http://127.0.0.1:8080' :
-               process.env.MW_SERVER
+                       'http://127.0.0.1:8080' :
+                       process.env.MW_SERVER
        ) + (
                process.env.MW_SCRIPT_PATH === undefined ?
-               '/w' :
-               process.env.MW_SCRIPT_PATH
+                       '/w' :
+                       process.env.MW_SCRIPT_PATH
        ),
        //
        // Default timeout for all waitFor* commands.