RCFilters: Store invert as a standard filter/parameter
authorStephane Bisson <sbisson@wikimedia.org>
Fri, 6 Oct 2017 18:52:36 +0000 (14:52 -0400)
committerStephane Bisson <sbisson@wikimedia.org>
Wed, 11 Oct 2017 00:47:49 +0000 (20:47 -0400)
Change-Id: Ie623dac923cc2feb30f406c48accdadfd8d80753

14 files changed:
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagItemWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.TagItemWidget.js
tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js

index 772ed92..0d65466 100644 (file)
@@ -16,7 +16,6 @@
                this.defaultParams = {};
                this.defaultFiltersEmpty = null;
                this.highlightEnabled = false;
-               this.invertedNamespaces = false;
                this.parameterMap = {};
 
                this.views = {};
         * Highlight feature has been toggled enabled or disabled
         */
 
-       /**
-        * @event invertChange
-        * @param {boolean} isInverted Namespace selected is inverted
-        *
-        * Namespace selection is inverted or straight forward
-        */
-
        /* Methods */
 
        /**
         * Propagate the change to namespace filter items.
         *
         * @param {boolean} enable Inverted property is enabled
-        * @fires invertChange
         */
        mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
-               enable = enable === undefined ? !this.invertedNamespaces : enable;
-
-               if ( this.invertedNamespaces !== enable ) {
-                       this.invertedNamespaces = enable;
-
-                       this.getFiltersByView( 'namespaces' ).forEach( function ( filterItem ) {
-                               filterItem.toggleInverted( this.invertedNamespaces );
-                       }.bind( this ) );
-
-                       this.emit( 'invertChange', this.invertedNamespaces );
-               }
+               this.toggleFilterSelected( this.getInvertModel().getName(), enable );
        };
 
        /**
-        * Check if the namespaces selection is set to be inverted
-        * @return {boolean}
+        * Get the model object that represents the 'invert' filter
+        *
+        * @return {mw.rcfilters.dm.FilterItem}
         */
-       mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesInverted = function () {
-               return !!this.invertedNamespaces;
+       mw.rcfilters.dm.FiltersViewModel.prototype.getInvertModel = function () {
+               return this.getGroup( 'invertGroup' ).getItemByParamName( 'invert' );
        };
 
        /**
index 9c56f09..4a8869a 100644 (file)
@@ -14,8 +14,6 @@
         *  with 'default' and 'inverted' as keys.
         * @cfg {boolean} [active=true] The filter is active and affecting the result
         * @cfg {boolean} [selected] The item is selected
-        * @cfg {boolean} [inverted] The item is inverted, meaning the search is excluding
-        *  this parameter.
         * @cfg {string} [namePrefix='item_'] A prefix to add to the param name to act as a unique
         *  identifier
         * @cfg {string} [cssClass] The class identifying the results that match this filter
@@ -38,7 +36,6 @@
                this.description = config.description || '';
                this.selected = !!config.selected;
 
-               this.inverted = !!config.inverted;
                this.identifiers = config.identifiers || [];
 
                // Highlight
@@ -69,8 +66,7 @@
         */
        mw.rcfilters.dm.ItemModel.prototype.getState = function () {
                return {
-                       selected: this.isSelected(),
-                       inverted: this.isInverted()
+                       selected: this.isSelected()
                };
        };
 
        /**
         * Get a prefixed label
         *
+        * @param {boolean} inverted This item should be considered inverted
         * @return {string} Prefixed label
         */
-       mw.rcfilters.dm.ItemModel.prototype.getPrefixedLabel = function () {
+       mw.rcfilters.dm.ItemModel.prototype.getPrefixedLabel = function ( inverted ) {
                if ( this.labelPrefixKey ) {
                        if ( typeof this.labelPrefixKey === 'string' ) {
                                return mw.message( this.labelPrefixKey, this.getLabel() ).parse();
@@ -97,7 +94,7 @@
                                        this.labelPrefixKey[
                                                // Only use inverted-prefix if the item is selected
                                                // Highlight-only an inverted item makes no sense
-                                               this.isInverted() && this.isSelected() ?
+                                               inverted && this.isSelected() ?
                                                        'inverted' : 'default'
                                        ],
                                        this.getLabel()
                }
        };
 
-       /**
-        * Get the inverted state of this item
-        *
-        * @return {boolean} Item is inverted
-        */
-       mw.rcfilters.dm.ItemModel.prototype.isInverted = function () {
-               return this.inverted;
-       };
-
-       /**
-        * Toggle the inverted state of the item
-        *
-        * @param {boolean} [isInverted] Item is inverted
-        * @fires update
-        */
-       mw.rcfilters.dm.ItemModel.prototype.toggleInverted = function ( isInverted ) {
-               isInverted = isInverted === undefined ? !this.inverted : isInverted;
-
-               if ( this.inverted !== isInverted ) {
-                       this.inverted = isInverted;
-                       this.emit( 'update' );
-               }
-       };
-
        /**
         * Set the highlight color
         *
index 2b17897..edb9644 100644 (file)
                        newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
                } );
 
-               // Add highlight and invert toggles to params
+               // Add highlight
                newData.params.highlight = String( Number( highlightEnabled || 0 ) );
-               newData.params.invert = String( Number( data.invert || 0 ) );
 
                return newData;
        };
                        } );
 
                        this.baseParamState = {
-                               params: $.extend( true, { invert: '0', highlight: '0' }, allParams ),
+                               params: $.extend( true, { highlight: '0' }, allParams ),
                                highlights: highlightedItems
                        };
                }
index 3e71729..6da8119 100644 (file)
                                        separator: ';',
                                        fullCoverage: true,
                                        filters: items
+                               },
+                               {
+                                       name: 'invertGroup',
+                                       type: 'boolean',
+                                       hidden: true,
+                                       filters: [ {
+                                               name: 'invert',
+                                               'default': '0'
+                                       } ]
                                } ]
                        };
                }
                                params: $.extend(
                                        true,
                                        {
-                                               invert: String( Number( this.filtersModel.areNamespacesInverted() ) ),
                                                highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
                                        },
                                        this.filtersModel.getParametersFromFilters( selectedState )
                                )
                        );
 
-                       // Update namespace inverted property
-                       this.filtersModel.toggleInvertedNamespaces( !!Number( data.params.invert ) );
-
                        // Update highlight state
                        this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) );
                        this.filtersModel.getItems().forEach( function ( filterItem ) {
                                params: $.extend(
                                        true,
                                        {
-                                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ),
-                                               invert: String( Number( this.filtersModel.areNamespacesInverted() ) )
+                                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
                                        },
                                        this.filtersModel.getParametersFromFilters( selectedState )
                                ),
                        return $.extend( true, {},
                                this.filtersModel.getParametersFromFilters( savedFilters ),
                                data.highlights,
-                               { highlight: data.params.highlight, invert: data.params.invert }
+                               { highlight: data.params.highlight }
                        );
                }
                return this.filtersModel.getDefaultParams();
index c9436f4..0450639 100644 (file)
                        )
                );
 
-               this.filtersModel.toggleInvertedNamespaces( !!Number( parameters.invert ) );
-
                // Update highlight state
                this.filtersModel.getItems().forEach( function ( filterItem ) {
                        var color = parameters[ filterItem.getName() + '_color' ];
                        this.filtersModel.getParametersFromFilters(),
                        this.filtersModel.getHighlightParameters(),
                        {
-                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ),
-                               invert: String( Number( this.filtersModel.areNamespacesInverted() ) )
+                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
                        }
                );
        };
                        this.filtersModel.getParametersFromFilters( filterRepresentation ),
                        this.filtersModel.extractHighlightValues( uriQuery ),
                        {
-                               highlight: String( Number( uriQuery.highlight ) ),
-                               invert: String( Number( uriQuery.invert ) )
+                               highlight: String( Number( uriQuery.highlight ) )
                        }
                );
        };
                        {},
                        emptyParams,
                        emptyHighlights,
-                       { highlight: '0', invert: '0' }
+                       { highlight: '0' }
                );
        };
 }( mediaWiki, jQuery ) );
index 1a0c5ff..a000696 100644 (file)
@@ -59,7 +59,6 @@
                        classes: [ 'mw-rcfilters-ui-filterMenuHeaderWidget-invertNamespacesButton' ]
                } );
                this.invertNamespacesButton.toggle( this.model.getCurrentView() === 'namespaces' );
-               this.updateInvertButton( this.model.areNamespacesInverted() );
 
                // Events
                this.backButton.connect( this, { click: 'onBackButtonClick' } );
@@ -69,8 +68,8 @@
                        .connect( this, { click: 'onInvertNamespacesButtonClick' } );
                this.model.connect( this, {
                        highlightChange: 'onModelHighlightChange',
-                       invertChange: 'onModelInvertChange',
-                       update: 'onModelUpdate'
+                       update: 'onModelUpdate',
+                       initialize: 'onModelInitialize'
                } );
 
                // Initialize
 
        /* Methods */
 
+       /**
+        * Respond to model initialization event
+        *
+        * Note: need to wait for initialization before getting the invertModel
+        * and registering its update event. Creating all the models before the UI
+        * would help with that.
+        */
+       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelInitialize = function () {
+               this.invertModel = this.model.getInvertModel();
+               this.updateInvertButton();
+               this.invertModel.connect( this, { update: 'updateInvertButton' } );
+       };
+
        /**
         * Respond to model update event
         */
                this.highlightButton.setActive( highlightEnabled );
        };
 
-       /**
-        * Respond to model invert change event
-        *
-        * @param {boolean} isInverted Namespaces selection is inverted
-        */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.onModelInvertChange = function ( isInverted ) {
-               this.updateInvertButton( isInverted );
-       };
-
        /**
         * Update the state of the invert button
-        *
-        * @param {boolean} isInverted Namespaces selection is inverted
         */
-       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.updateInvertButton = function ( isInverted ) {
-               this.invertNamespacesButton.setActive( isInverted );
+       mw.rcfilters.ui.FilterMenuHeaderWidget.prototype.updateInvertButton = function () {
+               this.invertNamespacesButton.setActive( this.invertModel.isSelected() );
                this.invertNamespacesButton.setLabel(
-                       isInverted ?
+                       this.invertModel.isSelected() ?
                                mw.msg( 'rcfilters-exclude-button-on' ) :
                                mw.msg( 'rcfilters-exclude-button-off' )
                );
index 5198c69..1292901 100644 (file)
@@ -6,17 +6,19 @@
         *
         * @constructor
         * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
         * @param {mw.rcfilters.dm.FilterItem} model Filter item model
         * @param {Object} config Configuration object
         */
-       mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, model, config ) {
+       mw.rcfilters.ui.FilterMenuOptionWidget = function MwRcfiltersUiFilterMenuOptionWidget( controller, invertModel, model, config ) {
                config = config || {};
 
                this.controller = controller;
+               this.invertModel = invertModel;
                this.model = model;
 
                // Parent
-               mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, controller, model, config );
+               mw.rcfilters.ui.FilterMenuOptionWidget.parent.call( this, controller, this.invertModel, model, config );
 
                // Event
                this.model.getGroupModel().connect( this, { update: 'onGroupModelUpdate' } );
@@ -58,7 +60,7 @@
        mw.rcfilters.ui.FilterMenuOptionWidget.prototype.setCurrentMuteState = function () {
                if (
                        this.model.getGroupModel().getView() === 'namespaces' &&
-                       this.model.isInverted()
+                       this.invertModel.isSelected()
                ) {
                        // This is an inverted behavior than the other rules, specifically
                        // for inverted namespaces
index 8a36eb4..43a301f 100644 (file)
@@ -7,13 +7,14 @@
         *
         * @constructor
         * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
         * @param {mw.rcfilters.dm.FilterItem} model Item model
         * @param {Object} config Configuration object
         */
-       mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget( controller, model, config ) {
+       mw.rcfilters.ui.FilterTagItemWidget = function MwRcfiltersUiFilterTagItemWidget( controller, invertModel, model, config ) {
                config = config || {};
 
-               mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, controller, model, config );
+               mw.rcfilters.ui.FilterTagItemWidget.parent.call( this, controller, invertModel, model, config );
 
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterTagItemWidget' );
index 757a000..ef95f2f 100644 (file)
         * @param {mw.rcfilters.dm.FilterItem} item Filter item model
         */
        mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
-               if ( item.getGroupModel().isHidden() ) {
-                       return;
-               }
-
-               if (
-                       item.isSelected() ||
-                       (
-                               this.model.isHighlightEnabled() &&
-                               item.isHighlightSupported() &&
-                               item.getHighlightColor()
-                       )
-               ) {
-                       this.addTag( item.getName(), item.getLabel() );
-               } else {
-                       this.removeTagByData( item.getName() );
+               if ( !item.getGroupModel().isHidden() ) {
+                       if (
+                               item.isSelected() ||
+                               (
+                                       this.model.isHighlightEnabled() &&
+                                       item.isHighlightSupported() &&
+                                       item.getHighlightColor()
+                               )
+                       ) {
+                               this.addTag( item.getName(), item.getLabel() );
+                       } else {
+                               this.removeTagByData( item.getName() );
+                       }
                }
 
                this.setSavedQueryVisibility();
                if ( filterItem ) {
                        return new mw.rcfilters.ui.FilterTagItemWidget(
                                this.controller,
+                               this.model.getInvertModel(),
                                filterItem,
                                {
                                        $overlay: this.$overlay
index f2e9b1d..36bc6cb 100644 (file)
@@ -6,10 +6,11 @@
         *
         * @constructor
         * @param {mw.rcfilters.Controller} controller RCFilters controller
+        * @param {mw.rcfilters.dm.ItemModel} invertModel
         * @param {mw.rcfilters.dm.ItemModel} model Item model
         * @param {Object} config Configuration object
         */
-       mw.rcfilters.ui.ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget( controller, model, config ) {
+       mw.rcfilters.ui.ItemMenuOptionWidget = function MwRcfiltersUiItemMenuOptionWidget( controller, invertModel, model, config ) {
                var layout,
                        classes = [],
                        $label = $( '<div>' )
@@ -18,6 +19,7 @@
                config = config || {};
 
                this.controller = controller;
+               this.invertModel = invertModel;
                this.model = model;
 
                // Parent
@@ -59,7 +61,7 @@
                this.excludeLabel = new OO.ui.LabelWidget( {
                        label: mw.msg( 'rcfilters-filter-excluded' )
                } );
-               this.excludeLabel.toggle( this.model.isSelected() && this.model.isInverted() );
+               this.excludeLabel.toggle( this.model.isSelected() && this.invertModel.isSelected() );
 
                layout = new OO.ui.FieldLayout( this.checkboxWidget, {
                        label: $label,
@@ -67,6 +69,7 @@
                } );
 
                // Events
+               this.invertModel.connect( this, { update: 'onModelUpdate' } );
                this.model.connect( this, { update: 'onModelUpdate' } );
                // HACK: Prevent defaults on 'click' for the label so it
                // doesn't steal the focus away from the input. This means
                this.checkboxWidget.setSelected( this.model.isSelected() );
 
                this.highlightButton.toggle( this.model.isHighlightEnabled() );
-               this.excludeLabel.toggle( this.model.isSelected() && this.model.isInverted() );
+               this.excludeLabel.toggle( this.model.isSelected() && this.invertModel.isSelected() );
        };
 
        /**
index 2aabe68..63a563c 100644 (file)
                                        currentItems.push(
                                                new mw.rcfilters.ui.FilterMenuOptionWidget(
                                                        widget.controller,
+                                                       widget.model.getInvertModel(),
                                                        filterItem,
                                                        {
                                                                $overlay: widget.$overlay
index 81889b2..cc314ac 100644 (file)
@@ -8,21 +8,22 @@
         *
         * @constructor
         * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.FilterItem} invertModel
         * @param {mw.rcfilters.dm.FilterItem} model Item model
         * @param {Object} config Configuration object
         * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
         */
-       mw.rcfilters.ui.TagItemWidget = function MwRcfiltersUiTagItemWidget( controller, model, config ) {
+       mw.rcfilters.ui.TagItemWidget = function MwRcfiltersUiTagItemWidget( controller, invertModel, model, config ) {
                // Configuration initialization
                config = config || {};
 
                this.controller = controller;
+               this.invertModel = invertModel;
                this.model = model;
                this.selected = false;
 
                mw.rcfilters.ui.TagItemWidget.parent.call( this, $.extend( {
-                       data: this.model.getName(),
-                       label: $( '<div>' ).html( this.model.getPrefixedLabel() ).contents()
+                       data: this.model.getName()
                }, config ) );
 
                this.$overlay = config.$overlay || this.$element;
@@ -52,7 +53,8 @@
                this.closeButton.setTitle( mw.msg( 'rcfilters-tag-remove', this.model.getLabel() ) );
 
                // Events
-               this.model.connect( this, { update: 'onModelUpdate' } );
+               this.invertModel.connect( this, { update: 'updateUiBasedOnState' } );
+               this.model.connect( this, { update: 'updateUiBasedOnState' } );
 
                // Initialization
                this.$overlay.append( this.popup.$element );
@@ -63,8 +65,7 @@
                        .on( 'mouseenter', this.onMouseEnter.bind( this ) )
                        .on( 'mouseleave', this.onMouseLeave.bind( this ) );
 
-               this.setCurrentMuteState();
-               this.setHighlightColor();
+               this.updateUiBasedOnState();
        };
 
        /* Initialization */
        /**
         * Respond to model update event
         */
-       mw.rcfilters.ui.TagItemWidget.prototype.onModelUpdate = function () {
+       mw.rcfilters.ui.TagItemWidget.prototype.updateUiBasedOnState = function () {
                this.setCurrentMuteState();
 
                // Update label if needed
-               this.setLabel( $( '<div>' ).html( this.model.getPrefixedLabel() ).contents() );
+               this.setLabel( $( '<div>' ).html( this.model.getPrefixedLabel( this.invertModel.isSelected() ) ).contents() );
 
                this.setHighlightColor();
        };
index edaaa39..38ade4d 100644 (file)
@@ -59,7 +59,6 @@
                                filter4: '0',
                                group3: '',
                                highlight: '0',
-                               invert: '0',
                                group1__filter1_color: null,
                                group1__filter2_color: null,
                                group2__filter3_color: null,
                        } ),
                        'Highlight parameters in Uri query set highlight state in the model'
                );
-
-               uriProcessor.updateModelBasedOnQuery( { invert: '1', urlversion: '2' } );
-               assert.deepEqual(
-                       uriProcessor.getUriParametersFromModel(),
-                       $.extend( true, {}, baseParams, {
-                               invert: '1'
-                       } ),
-                       'Invert parameter in Uri query set invert state in the model'
-               );
        } );
 
        QUnit.test( 'isNewState', function ( assert ) {
index 324a652..6a05920 100644 (file)
@@ -56,8 +56,7 @@
                                                        highlight: true,
                                                        filter1: 'c5',
                                                        group3option1: 'c1'
-                                               },
-                                               invert: true
+                                               }
                                        }
                                }
                        }
@@ -75,8 +74,7 @@
                                                        // Group type string_options
                                                        group2: 'filter4',
                                                        // Note - Group3 is sticky, so it won't show in output
-                                                       // Invert/highlight toggles
-                                                       invert: '1',
+                                                       // highlight toggle
                                                        highlight: '1'
                                                },
                                                highlights: {
                                        group2: 'filter5',
                                        filter1: '0',
                                        filter2: '0',
-                                       highlight: '1',
-                                       invert: '0'
+                                       highlight: '1'
                                },
                                highlights: {
                                        filter1_color: 'c5',