Follow-up 94b6ba5453: cast default value to string
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.Controller.js
index 7de6669..f1468b8 100644 (file)
@@ -15,6 +15,8 @@
                this.baseFilterState = {};
                this.uriProcessor = null;
                this.initializing = false;
+
+               this.prevLoggedItems = [];
        };
 
        /* Initialization */
         */
        mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
                var parsedSavedQueries,
+                       controller = this,
                        views = {},
                        items = [],
                        uri = new mw.Uri(),
-                       $changesList = $( '.mw-changeslist' ).first().contents();
+                       $changesList = $( '.mw-changeslist' ).first().contents(),
+                       experimentalViews = mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' );
 
                // Prepare views
-               if ( namespaceStructure ) {
+               if ( namespaceStructure && experimentalViews ) {
                        items = [];
                        $.each( namespaceStructure, function ( namespaceID, label ) {
                                // Build and clean up the individual namespace items definition
@@ -56,7 +60,7 @@
                                trigger: ':',
                                groups: [ {
                                        // Group definition (single group)
-                                       name: 'namespaces',
+                                       name: 'namespace', // parameter name is singular
                                        type: 'string_options',
                                        title: mw.msg( 'namespaces' ),
                                        labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
@@ -66,7 +70,7 @@
                                } ]
                        };
                }
-               if ( tagList ) {
+               if ( tagList && experimentalViews ) {
                        views.tags = {
                                title: mw.msg( 'rcfilters-view-tags' ),
                                trigger: '#',
                        };
                }
 
+               // Add parameter range operations
+               views.range = {
+                       groups: [
+                               {
+                                       name: 'limit',
+                                       type: 'single_option',
+                                       title: '', // Because it's a hidden group, this title actually appears nowhere
+                                       hidden: true,
+                                       allowArbitrary: true,
+                                       validate: $.isNumeric,
+                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+                                       'default': mw.user.options.get( 'rclimit' ),
+                                       filters: [ 50, 100, 250, 500 ].map( function ( num ) {
+                                               return controller._createFilterDataFromNumber( num, num );
+                                       } )
+                               },
+                               {
+                                       name: 'days',
+                                       type: 'single_option',
+                                       title: '', // Because it's a hidden group, this title actually appears nowhere
+                                       hidden: true,
+                                       allowArbitrary: true,
+                                       validate: $.isNumeric,
+                                       sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+                                       numToLabelFunc: function ( i ) {
+                                               return Number( i ) < 1 ?
+                                                       ( Number( i ) * 24 ).toFixed( 2 ) :
+                                                       Number( i );
+                                       },
+                                       'default': mw.user.options.get( 'rcdays' ),
+                                       filters: [
+                                               // Hours (1, 2, 6, 12)
+                                               0.04166, 0.0833, 0.25, 0.5,
+                                               // Days
+                                               1, 3, 7, 14, 30
+                                       ].map( function ( num ) {
+                                               return controller._createFilterDataFromNumber(
+                                                       num,
+                                                       // Convert fractions of days to number of hours for the labels
+                                                       num < 1 ? Math.round( num * 24 ) : num
+                                               );
+                                       } )
+                               }
+                       ]
+               };
+
+               // Before we do anything, we need to see if we require additional items in the
+               // groups that have 'AllowArbitrary'. For the moment, those are only single_option
+               // groups; if we ever expand it, this might need further generalization:
+               $.each( views, function ( viewName, viewData ) {
+                       viewData.groups.forEach( function ( groupData ) {
+                               var extraValues = [];
+                               if ( groupData.allowArbitrary ) {
+                                       // If the value in the URI isn't in the group, add it
+                                       if ( uri.query[ groupData.name ] !== undefined ) {
+                                               extraValues.push( uri.query[ groupData.name ] );
+                                       }
+                                       // If the default value isn't in the group, add it
+                                       if ( groupData.default !== undefined ) {
+                                               extraValues.push( String( groupData.default ) );
+                                       }
+                                       controller.addNumberValuesToGroup( groupData, extraValues );
+                               }
+                       } );
+               } );
+
                // Initialize the model
                this.filtersModel.initializeFilters( filterStructure, views );
 
                        // so it gets processed
                        this.changesListModel.update(
                                $changesList.length ? $changesList : 'NO_RESULTS',
-                               $( 'fieldset.rcoptions' ).first()
+                               $( 'fieldset.rcoptions' ).first(),
+                               true // We're using existing DOM elements
                        );
                }
 
                this.switchView( 'default' );
        };
 
+       /**
+        * Create filter data from a number, for the filters that are numerical value
+        *
+        * @param {Number} num Number
+        * @param {Number} numForDisplay Number for the label
+        * @return {Object} Filter data
+        */
+       mw.rcfilters.Controller.prototype._createFilterDataFromNumber = function ( num, numForDisplay ) {
+               return {
+                       name: String( num ),
+                       label: mw.language.convertNumber( numForDisplay )
+               };
+       };
+
+       /**
+        * Add an arbitrary values to groups that allow arbitrary values
+        *
+        * @param {Object} groupData Group data
+        * @param {string|string[]} arbitraryValues An array of arbitrary values to add to the group
+        */
+       mw.rcfilters.Controller.prototype.addNumberValuesToGroup = function ( groupData, arbitraryValues ) {
+               var controller = this;
+
+               arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ];
+
+               // This is only true for single_option group
+               // We assume these are the only groups that will allow for
+               // arbitrary, since it doesn't make any sense for the other
+               // groups.
+               arbitraryValues.forEach( function ( val ) {
+                       if (
+                               // If the group allows for arbitrary data
+                               groupData.allowArbitrary &&
+                               // and it is single_option (or string_options, but we
+                               // don't have cases of those yet, nor do we plan to)
+                               groupData.type === 'single_option' &&
+                               // and, if there is a validate method and it passes on
+                               // the data
+                               ( !groupData.validate || groupData.validate( val ) ) &&
+                               // but if that value isn't already in the definition
+                               groupData.filters
+                                       .map( function ( filterData ) {
+                                               return filterData.name;
+                                       } )
+                                       .indexOf( val ) === -1
+                       ) {
+                               // Add the filter information
+                               groupData.filters.push( controller._createFilterDataFromNumber(
+                                       val,
+                                       groupData.numToLabelFunc ?
+                                               groupData.numToLabelFunc( val ) :
+                                               val
+                               ) );
+
+                               // If there's a sort function set up, re-sort the values
+                               if ( groupData.sortFunc ) {
+                                       groupData.filters.sort( groupData.sortFunc );
+                               }
+                       }
+               } );
+       };
+
        /**
         * Switch the view of the filters model
         *
                        this.filtersModel.toggleFilterSelected( filterName, false );
                        this.updateChangesList();
                        this.filtersModel.reassessFilterInteractions( filterItem );
+
+                       // Log filter grouping
+                       this.trackFilterGroupings( 'removefilter' );
                }
 
                if ( isHighlighted ) {
                this._trackHighlight( 'clear', filterName );
        };
 
+       /**
+        * Enable or disable live updates.
+        * @param {boolean} enable True to enable, false to disable
+        */
+       mw.rcfilters.Controller.prototype.toggleLiveUpdate = function ( enable ) {
+               if ( enable && !this.liveUpdateTimeout ) {
+                       this._scheduleLiveUpdate();
+               } else if ( !enable && this.liveUpdateTimeout ) {
+                       clearTimeout( this.liveUpdateTimeout );
+                       this.liveUpdateTimeout = null;
+               }
+       };
+
+       /**
+        * Set a timeout for the next live update.
+        * @private
+        */
+       mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () {
+               this.liveUpdateTimeout = setTimeout( this._doLiveUpdate.bind( this ), 3000 );
+       };
+
+       /**
+        * Perform a live update.
+        * @private
+        */
+       mw.rcfilters.Controller.prototype._doLiveUpdate = function () {
+               var controller = this;
+               this.updateChangesList( {}, true )
+                       .always( function () {
+                               if ( controller.liveUpdateTimeout ) {
+                                       // Live update was not disabled in the meantime
+                                       controller._scheduleLiveUpdate();
+                               }
+                       } );
+       };
+
        /**
         * Save the current model state as a saved query
         *
                        this.filtersModel.reassessFilterInteractions();
 
                        this.updateChangesList();
+
+                       // Log filter grouping
+                       this.trackFilterGroupings( 'savedfilters' );
                }
        };
 
         * Update the list of changes and notify the model
         *
         * @param {Object} [params] Extra parameters to add to the API call
+        * @param {boolean} [isLiveUpdate] Don't update the URL or invalidate the changes list
+        * @return {jQuery.Promise} Promise that is resolved when the update is complete
         */
-       mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) {
-               this._updateURL( params );
-               this.changesListModel.invalidate();
-               this._fetchChangesList()
+       mw.rcfilters.Controller.prototype.updateChangesList = function ( params, isLiveUpdate ) {
+               if ( !isLiveUpdate ) {
+                       this._updateURL( params );
+                       this.changesListModel.invalidate();
+               }
+               return this._fetchChangesList()
                        .then(
                                // Success
                                function ( pieces ) {
                );
        };
 
+       /**
+        * 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 ) );