Merge "RCFilters UI: Add a 'what's this?' link to filter groups"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 17 Mar 2017 20:05:21 +0000 (20:05 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 17 Mar 2017 20:05:22 +0000 (20:05 +0000)
1  2 
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js

diff --combined resources/Resources.php
@@@ -1092,12 -1092,6 +1092,12 @@@ return 
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
 +      'mediawiki.htmlform.checker' => [
 +              'scripts' => [
 +                      'resources/src/mediawiki/htmlform/htmlform.Checker.js',
 +              ],
 +              'targets' => [ 'desktop', 'mobile' ],
 +      ],
        'mediawiki.htmlform.ooui' => [
                'scripts' => [
                        'resources/src/mediawiki/htmlform/htmlform.Element.js',
                        'rcfilters-filterlist-title',
                        'rcfilters-filterlist-feedbacklink',
                        'rcfilters-filterlist-noresults',
+                       'rcfilters-filterlist-whatsthis',
                        'rcfilters-highlightbutton-title',
                        'rcfilters-highlightmenu-title',
                        'rcfilters-highlightmenu-help',
                        'mediawiki.htmlform',
                ],
        ],
 +      'mediawiki.special.changecredentials.js' => [
 +              'scripts' => 'resources/src/mediawiki.special/mediawiki.special.changecredentials.js',
 +              'dependencies' => [
 +                      'mediawiki.api',
 +                      'mediawiki.htmlform.ooui'
 +              ],
 +              'targets' => [ 'desktop', 'mobile' ],
 +      ],
        'mediawiki.special.changeslist' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.changeslist.css',
                'targets' => [ 'desktop', 'mobile' ],
        'mediawiki.special.userlogin.signup.js' => [
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.userlogin.signup.js',
                'messages' => [
 -                      'createacct-error',
                        'createacct-emailrequired',
                        'noname',
                        'userexists',
                        'mediawiki.api',
                        'mediawiki.jqueryMsg',
                        'jquery.throttle-debounce',
 +                      'mediawiki.htmlform.checker',
                ],
        ],
        'mediawiki.special.unwatchedPages' => [
         * @cfg {boolean} [active] Group is active
         * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
         * @cfg {Object} [conflicts] Defines the conflicts for this filter group
+        * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
+        * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
+        * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
+        * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
+        * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
         */
        mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
                config = config || {};
@@@ -30,6 -35,8 +35,8 @@@
                this.active = !!config.active;
                this.fullCoverage = !!config.fullCoverage;
  
+               this.whatsThis = config.whatsThis || {};
                this.conflicts = config.conflicts || {};
  
                this.aggregate( { update: 'filterItemUpdate' } );
                return this.name;
        };
  
+       /**
+        * Get the messags defining the 'whats this' popup for this group
+        *
+        * @return {Object} What's this messages
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
+               return this.whatsThis;
+       };
+       /**
+        * Check whether this group has a 'what's this' message
+        *
+        * @return {boolean} This group has a what's this message
+        */
+       mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
+               return !!this.whatsThis.body;
+       };
        /**
         * Get the conflicts associated with the entire group.
         * Conflict object is set up by filter name keys and conflict
                );
        };
  
 +      /**
 +       * Get the parameter representation from this group
 +       *
 +       * @return {Object} Parameter representation
 +       */
 +      mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function () {
 +              var i, values,
 +                      result = {},
 +                      filterItems = this.getItems();
 +
 +              if ( this.getType() === 'send_unselected_if_any' ) {
 +                      // First, check if any of the items are selected at all.
 +                      // If none is selected, we're treating it as if they are
 +                      // all false
 +
 +                      // Go over the items and define the correct values
 +                      for ( i = 0; i < filterItems.length; i++ ) {
 +                              result[ filterItems[ i ].getParamName() ] = this.areAnySelected() ?
 +                                      Number( !filterItems[ i ].isSelected() ) : 0;
 +                      }
 +
 +              } else if ( this.getType() === 'string_options' ) {
 +                      values = [];
 +                      for ( i = 0; i < filterItems.length; i++ ) {
 +                              if ( filterItems[ i ].isSelected() ) {
 +                                      values.push( filterItems[ i ].getParamName() );
 +                              }
 +                      }
 +
 +                      result[ this.getName() ] = ( values.length === filterItems.length ) ?
 +                              'all' : values.join( this.getSeparator() );
 +              }
 +
 +              return result;
 +      };
 +
        /**
         * Get group type
         *
                return this.type;
        };
  
 +      /**
 +       * Get the prefix used for the filter names inside this group
 +       *
 +       * @return {string} Group prefix
 +       */
 +      mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
 +              return this.getName() + '__';
 +      };
 +
        /**
         * Get group's title
         *
@@@ -16,7 -16,6 +16,7 @@@
                this.defaultParams = {};
                this.defaultFiltersEmpty = null;
                this.highlightEnabled = false;
 +              this.parameterMap = {};
  
                // Events
                this.aggregate( { update: 'filterItemUpdate' } );
         * @param {Array} filters Filter group definition
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
 -              var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult,
 +              var i, filterItem, selectedFilterNames, filterConflictResult, groupConflictResult, subsetNames,
                        model = this,
                        items = [],
                        supersetMap = {},
                        expandConflictDefinitions = function ( obj ) {
                                var result = {};
  
 -                              $.each( obj, function ( group, conflicts ) {
 -                                      var adjustedConflicts = {};
 +                              $.each( obj, function ( key, conflicts ) {
 +                                      var filterName,
 +                                              adjustedConflicts = {};
 +
                                        conflicts.forEach( function ( conflict ) {
                                                if ( conflict.filter ) {
 -                                                      adjustedConflicts[ conflict.filter ] = conflict;
 +                                                      filterName = model.groups[ conflict.group ].getNamePrefix() + conflict.filter;
 +
 +                                                      // Rename
 +                                                      adjustedConflicts[ filterName ] = $.extend(
 +                                                              {},
 +                                                              conflict,
 +                                                              { filter: filterName }
 +                                                      );
                                                } else {
                                                        // This conflict is for an entire group. Split it up to
                                                        // represent each filter
                                                        // Get the relevant group items
                                                        model.groups[ conflict.group ].getItems().forEach( function ( groupItem ) {
                                                                // Rebuild the conflict
 -                                                              adjustedConflicts[ groupItem.getName() ] = $.extend( {}, conflict, { filter: groupItem.getName() } );
 +                                                              adjustedConflicts[ groupItem.getName() ] = $.extend(
 +                                                                      {},
 +                                                                      conflict,
 +                                                                      { filter: groupItem.getName() }
 +                                                              );
                                                        } );
                                                }
                                        } );
  
 -                                      result[ group ] = adjustedConflicts;
 +                                      result[ key ] = adjustedConflicts;
                                } );
  
                                return result;
                                        type: data.type,
                                        title: mw.msg( data.title ),
                                        separator: data.separator,
-                                       fullCoverage: !!data.fullCoverage
+                                       fullCoverage: !!data.fullCoverage,
+                                       whatsThis: {
+                                               body: data.whatsThisBody,
+                                               header: data.whatsThisHeader,
+                                               linkText: data.whatsThisLinkText,
+                                               url: data.whatsThisUrl
+                                       }
                                } );
                        }
  
                                        group: group,
                                        label: mw.msg( data.filters[ i ].label ),
                                        description: mw.msg( data.filters[ i ].description ),
 -                                      subset: data.filters[ i ].subset,
                                        cssClass: data.filters[ i ].cssClass
                                } );
  
 -                              // For convenience, we should store each filter's "supersets" -- these are
 -                              // the filters that have that item in their subset list. This will just
 -                              // make it easier to go through whether the item has any other items
 -                              // that affect it (and are selected) at any given time
                                if ( data.filters[ i ].subset ) {
 +                                      subsetNames = [];
                                        data.filters[ i ].subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
 -                                              supersetMap[ subsetFilterName ] = supersetMap[ subsetFilterName ] || [];
 +                                              var subsetName = model.groups[ group ].getNamePrefix() + subsetFilterName;
 +                                              // For convenience, we should store each filter's "supersets" -- these are
 +                                              // the filters that have that item in their subset list. This will just
 +                                              // make it easier to go through whether the item has any other items
 +                                              // that affect it (and are selected) at any given time
 +                                              supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
                                                addArrayElementsUnique(
 -                                                      supersetMap[ subsetFilterName ],
 +                                                      supersetMap[ subsetName ],
                                                        filterItem.getName()
                                                );
 +
 +                                              // Translate subset param name to add the group name, so we
 +                                              // get consistent naming. We know that subsets are only within
 +                                              // the same group
 +                                              subsetNames.push( subsetName );
                                        } );
 +
 +                                      // Set translated subset
 +                                      filterItem.setSubset( subsetNames );
                                }
  
                                // Store conflicts
                                if ( data.filters[ i ].conflicts ) {
 -                                      filterConflictMap[ data.filters[ i ].name ] = data.filters[ i ].conflicts;
 +                                      filterConflictMap[ filterItem.getName() ] = data.filters[ i ].conflicts;
                                }
  
                                if ( data.type === 'send_unselected_if_any' ) {
                        }
                } );
  
 +              // Create a map between known parameters and their models
 +              $.each( this.groups, function ( group, groupModel ) {
 +                      if ( groupModel.getType() === 'send_unselected_if_any' ) {
 +                              // Individual filters
 +                              groupModel.getItems().forEach( function ( filterItem ) {
 +                                      model.parameterMap[ filterItem.getParamName() ] = filterItem;
 +                              } );
 +                      } else if ( groupModel.getType() === 'string_options' ) {
 +                              // Group
 +                              model.parameterMap[ groupModel.getName() ] = groupModel;
 +                      }
 +              } );
 +
                // Add items to the model
                this.addItems( items );
  
         * @return {Object} Parameter state object
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getParametersFromFilters = function ( filterGroups ) {
 -              var i, filterItems, anySelected, values,
 -                      result = {},
 +              var result = {},
                        groupItems = filterGroups || this.getFilterGroups();
  
                $.each( groupItems, function ( group, model ) {
 -                      filterItems = model.getItems();
 -
 -                      if ( model.getType() === 'send_unselected_if_any' ) {
 -                              // First, check if any of the items are selected at all.
 -                              // If none is selected, we're treating it as if they are
 -                              // all false
 -                              anySelected = filterItems.some( function ( filterItem ) {
 -                                      return filterItem.isSelected();
 -                              } );
 -
 -                              // Go over the items and define the correct values
 -                              for ( i = 0; i < filterItems.length; i++ ) {
 -                                      result[ filterItems[ i ].getName() ] = anySelected ?
 -                                              Number( !filterItems[ i ].isSelected() ) : 0;
 -                              }
 -                      } else if ( model.getType() === 'string_options' ) {
 -                              values = [];
 -                              for ( i = 0; i < filterItems.length; i++ ) {
 -                                      if ( filterItems[ i ].isSelected() ) {
 -                                              values.push( filterItems[ i ].getName() );
 -                                      }
 -                              }
 -
 -                              if ( values.length === filterItems.length ) {
 -                                      result[ group ] = 'all';
 -                              } else {
 -                                      result[ group ] = values.join( model.getSeparator() );
 -                              }
 -                      }
 +                      $.extend( result, model.getParamRepresentation() );
                } );
  
                return result;
        mw.rcfilters.dm.FiltersViewModel.prototype.sanitizeStringOptionGroup = function( groupName, valueArray ) {
                var result = [],
                        validNames = this.getGroupFilters( groupName ).map( function ( filterItem ) {
 -                              return filterItem.getName();
 +                              return filterItem.getParamName();
                        } );
  
                if ( valueArray.indexOf( 'all' ) > -1 ) {
         * @return {Object} Filter state object
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersFromParameters = function ( params ) {
 -              var i, filterItem,
 +              var i,
                        groupMap = {},
                        model = this,
                        base = this.getDefaultParams(),
  
                params = $.extend( {}, base, params );
  
 +              // Go over the given parameters
                $.each( params, function ( paramName, paramValue ) {
 -                      // Find the filter item
 -                      filterItem = model.getItemByName( paramName );
 -                      // Ignore if no filter item exists
 -                      if ( filterItem ) {
 -                              groupMap[ filterItem.getGroupName() ] = groupMap[ filterItem.getGroupName() ] || {};
 +                      var itemOrGroup = model.parameterMap[ paramName ];
  
 +                      if ( itemOrGroup instanceof mw.rcfilters.dm.FilterItem ) {
                                // Mark the group if it has any items that are selected
 -                              groupMap[ filterItem.getGroupName() ].hasSelected = (
 -                                      groupMap[ filterItem.getGroupName() ].hasSelected ||
 +                              groupMap[ itemOrGroup.getGroupName() ] = groupMap[ itemOrGroup.getGroupName() ] || {};
 +                              groupMap[ itemOrGroup.getGroupName() ].hasSelected = (
 +                                      groupMap[ itemOrGroup.getGroupName() ].hasSelected ||
                                        !!Number( paramValue )
                                );
  
 -                              // Add the relevant filter into the group map
 -                              groupMap[ filterItem.getGroupName() ].filters = groupMap[ filterItem.getGroupName() ].filters || [];
 -                              groupMap[ filterItem.getGroupName() ].filters.push( filterItem );
 -                      } else if ( model.groups.hasOwnProperty( paramName ) ) {
 +                              // Add filters
 +                              groupMap[ itemOrGroup.getGroupName() ].filters = groupMap[ itemOrGroup.getGroupName() ].filters || [];
 +                              groupMap[ itemOrGroup.getGroupName() ].filters.push( itemOrGroup );
 +                      } else if ( itemOrGroup instanceof mw.rcfilters.dm.FilterGroup ) {
 +                              groupMap[ itemOrGroup.getName() ] = groupMap[ itemOrGroup.getName() ] || {};
                                // This parameter represents a group (values are the filters)
                                // this is equivalent to checking if the group is 'string_options'
 -                              groupMap[ paramName ] = { filters: model.groups[ paramName ].getItems() };
 +                              groupMap[ itemOrGroup.getName() ].filters = itemOrGroup.getItems();
                        }
                } );
  
                                for ( i = 0; i < allItemsInGroup.length; i++ ) {
                                        filterItem = allItemsInGroup[ i ];
  
 -                                      result[ filterItem.getName() ] = data.hasSelected ?
 +                                      result[ filterItem.getName() ] = groupMap[ filterItem.getGroupName() ].hasSelected ?
                                                // Flip the definition between the parameter
                                                // state and the filter state
                                                // This is what the 'toggleSelected' value of the filter is
 -                                              !Number( params[ filterItem.getName() ] ) :
 +                                              !Number( params[ filterItem.getParamName() ] ) :
                                                // Otherwise, there are no selected items in the
                                                // group, which means the state is false
                                                false;
                                }
                        } else if ( model.groups[ group ].getType() === 'string_options' ) {
 -                              paramValues = model.sanitizeStringOptionGroup( group, params[ group ].split( model.groups[ group ].getSeparator() ) );
 +                              paramValues = model.sanitizeStringOptionGroup(
 +                                      group,
 +                                      params[ group ].split(
 +                                              model.groups[ group ].getSeparator()
 +                                      )
 +                              );
  
                                for ( i = 0; i < allItemsInGroup.length; i++ ) {
                                        filterItem = allItemsInGroup[ i ];
                                                // is the same as all filters set to false
                                                false :
                                                // Otherwise, the filter is selected only if it appears in the parameter values
 -                                              paramValues.indexOf( filterItem.getName() ) > -1;
 +                                              paramValues.indexOf( filterItem.getParamName() ) > -1;
                                }
                        }
                } );
 +
                return result;
        };
  
         * @param {boolean} [isSelected] Filter selected state
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) {
 -              this.getItemByName( name ).toggleSelected( isSelected );
 +              var item = this.getItemByName( name );
 +
 +              if ( item ) {
 +                      item.toggleSelected( isSelected );
 +              }
        };
  
        /**