Merge "Remove perf tracking code that was moved to WikimediaEvents in Ib300af5c"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.Controller.js
index ee74ac5..685adb6 100644 (file)
@@ -21,6 +21,7 @@
                this.baseFilterState = {};
                this.uriProcessor = null;
                this.initializing = false;
+               this.wereSavedQueriesSaved = false;
 
                this.prevLoggedItems = [];
 
                // Initialize the model
                this.filtersModel.initializeFilters( filterStructure, views );
 
-               this._buildBaseFilterState();
-
                this.uriProcessor = new mw.rcfilters.UriProcessor(
                        this.filtersModel
                );
                                parsedSavedQueries = {};
                        }
 
-                       // The queries are saved in a minimized state, so we need
-                       // to send over the base state so the saved queries model
-                       // can normalize them per each query item
-                       this.savedQueriesModel.initialize(
-                               parsedSavedQueries,
-                               this._getBaseFilterState(),
-                               // This is for backwards compatibility - delete all excluded filter states
-                               Object.keys( this.filtersModel.getExcludedFiltersState() )
-                       );
+                       // Initialize saved queries
+                       this.savedQueriesModel.initialize( parsedSavedQueries );
+                       if ( this.savedQueriesModel.isConverted() ) {
+                               // Since we know we converted, we're going to re-save
+                               // the queries so they are now migrated to the new format
+                               this._saveSavedQueries();
+                       }
                }
 
                // Check whether we need to load defaults.
                this.initializing = false;
                this.switchView( 'default' );
 
-               this._scheduleLiveUpdate();
+               this.pollingRate = mw.config.get( 'StructuredChangeFiltersLiveUpdatePollingRate' );
+               if ( this.pollingRate ) {
+                       this._scheduleLiveUpdate();
+               }
        };
 
        /**
                this.updateChangesList();
        };
 
+       /**
+        * Check whether the default values of the filters are all false.
+        *
+        * @return {boolean} Defaults are all false
+        */
+       mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
+               var defaultParams = this._getDefaultParams(),
+                       defaultFilters = this.filtersModel.getFiltersFromParameters( defaultParams );
+
+               this._deleteExcludedValuesFromFilterState( defaultFilters );
+
+               if ( Object.keys( defaultParams ).some( function ( paramName ) {
+                       return paramName.endsWith( '_color' ) && defaultParams[ paramName ] !== null;
+               } ) ) {
+                       // There are highlights in the defaults, they're definitely
+                       // not empty
+                       return false;
+               }
+
+               // Defaults can change in a session, so we need to do this every time
+               return Object.keys( defaultFilters ).every( function ( filterName ) {
+                       return !defaultFilters[ filterName ];
+               } );
+       };
+
        /**
         * Empty all selected filters
         */
         * @private
         */
        mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () {
-               setTimeout( this._doLiveUpdate.bind( this ), 3000 );
+               setTimeout( this._doLiveUpdate.bind( this ), this.pollingRate * 1000 );
        };
 
        /**
                }
 
                this._checkForNewChanges()
-                       .then( function ( data ) {
+                       .then( function ( newChanges ) {
                                if ( !this._shouldCheckForNewChanges() ) {
                                        // by the time the response is received,
                                        // it may not be appropriate anymore
                                        return;
                                }
 
-                               if ( data.changes !== 'NO_RESULTS' ) {
+                               if ( newChanges ) {
                                        if ( this.changesListModel.getLiveUpdate() ) {
                                                return this.updateChangesList( null, this.LIVE_UPDATE );
                                        } else {
        /**
         * Check if new changes, newer than those currently shown, are available
         *
-        * @return {jQuery.Promise} Promise object that resolves after trying
-        * to fetch 1 change newer than the last known 'from' parameter value
+        * @return {jQuery.Promise} Promise object that resolves with a bool
+        *      specifying if there are new changes or not
         *
         * @private
         */
        mw.rcfilters.Controller.prototype._checkForNewChanges = function () {
-               return this._fetchChangesList(
-                       'liveUpdate',
-                       {
-                               limit: 1,
-                               // temporarily disabled ( T173613#3591657 )
-                               // peek: 1, // bypasses all UI
-                               from: this.changesListModel.getNextFrom()
+               var params = {
+                       limit: 1,
+                       peek: 1, // bypasses ChangesList specific UI
+                       from: this.changesListModel.getNextFrom()
+               };
+               return this._queryChangesList( 'liveUpdate', params ).then(
+                       function ( data ) {
+                               // no result is 204 with the 'peek' param
+                               return data.status === 200;
                        }
                );
        };
         * @param {boolean} [setAsDefault=false] This query should be set as the default
         */
        mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
-               var queryID,
-                       highlightedItems = {},
+               var highlightedItems = {},
                        highlightEnabled = this.filtersModel.isHighlightEnabled(),
                        selectedState = this.filtersModel.getSelectedState();
 
                // Prepare highlights
                this.filtersModel.getHighlightedItems().forEach( function ( item ) {
-                       highlightedItems[ item.getName() ] = highlightEnabled ?
+                       highlightedItems[ item.getName() + '_color' ] = highlightEnabled ?
                                item.getHighlightColor() : null;
                } );
-               // These are filter states; highlight is stored as boolean
-               highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
 
                // Delete all excluded filters
                this._deleteExcludedValuesFromFilterState( selectedState );
 
                // Add item
-               queryID = this.savedQueriesModel.addNewQuery(
+               this.savedQueriesModel.addNewQuery(
                        label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
                        {
-                               filters: selectedState,
-                               highlights: highlightedItems,
-                               invert: this.filtersModel.areNamespacesInverted()
-                       }
+                               params: $.extend(
+                                       true,
+                                       {
+                                               invert: String( Number( this.filtersModel.areNamespacesInverted() ) ),
+                                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
+                                       },
+                                       this.filtersModel.getParametersFromFilters( selectedState )
+                               ),
+                               highlights: highlightedItems
+                       },
+                       setAsDefault
                );
 
-               if ( setAsDefault ) {
-                       this.savedQueriesModel.setDefault( queryID );
-               }
-
                // Save item
                this._saveSavedQueries();
        };
         * @param {string} queryID Query id
         */
        mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
-               var data, highlights,
+               var highlights,
                        queryItem = this.savedQueriesModel.getItemByID( queryID ),
+                       data = this.savedQueriesModel.getItemFullData( queryID ),
                        currentMatchingQuery = this.findQueryMatchingCurrentState();
 
                if (
                                currentMatchingQuery.getID() !== queryItem.getID()
                        )
                ) {
-                       data = queryItem.getData();
                        highlights = data.highlights;
 
-                       // Backwards compatibility; initial version mispelled 'highlight' with 'highlights'
-                       highlights.highlight = highlights.highlights || highlights.highlight;
-
                        // Update model state from filters
                        this.filtersModel.toggleFiltersSelected(
                                // Merge filters with excluded values
-                               $.extend( true, {}, data.filters, this.filtersModel.getExcludedFiltersState() )
+                               $.extend(
+                                       true,
+                                       {},
+                                       this.filtersModel.getFiltersFromParameters( data.params ),
+                                       this.filtersModel.getExcludedFiltersState()
+                               )
                        );
 
                        // Update namespace inverted property
-                       this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
+                       this.filtersModel.toggleInvertedNamespaces( !!Number( data.params.invert ) );
 
                        // Update highlight state
-                       this.filtersModel.toggleHighlight( !!Number( highlights.highlight ) );
+                       this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) );
                        this.filtersModel.getItems().forEach( function ( filterItem ) {
-                               var color = highlights[ filterItem.getName() ];
+                               var color = highlights[ filterItem.getName() + '_color' ];
                                if ( color ) {
                                        filterItem.setHighlightColor( color );
                                } else {
 
                // Prepare highlights of the current query
                this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
-                       highlightedItems[ item.getName() ] = item.getHighlightColor();
+                       highlightedItems[ item.getName() + '_color' ] = item.getHighlightColor();
                } );
-               highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
 
                // Remove anything that should be excluded from the saved query
                // this includes sticky filters and filters marked with 'excludedFromSavedQueries'
 
                return this.savedQueriesModel.findMatchingQuery(
                        {
-                               filters: selectedState,
-                               highlights: highlightedItems,
-                               invert: this.filtersModel.areNamespacesInverted()
+                               params: $.extend(
+                                       true,
+                                       {
+                                               highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ),
+                                               invert: String( Number( this.filtersModel.areNamespacesInverted() ) )
+                                       },
+                                       this.filtersModel.getParametersFromFilters( selectedState )
+                               ),
+                               highlights: highlightedItems
                        }
                );
        };
                } );
        };
 
-       /**
-        * Get an object representing the base state of parameters
-        * and highlights.
-        *
-        * This is meant to make sure that the saved queries that are
-        * in memory are always the same structure as what we would get
-        * by calling the current model's "getSelectedState" and by checking
-        * highlight items.
-        *
-        * In cases where a user saved a query when the system had a certain
-        * set of filters, and then a filter was added to the system, we want
-        * to make sure that the stored queries can still be comparable to
-        * the current state, which means that we need the base state for
-        * two operations:
-        *
-        * - Saved queries are stored in "minimal" view (only changed filters
-        *   are stored); When we initialize the system, we merge each minimal
-        *   query with the base state (using 'getNormalizedFilters') so all
-        *   saved queries have the exact same structure as what we would get
-        *   by checking the getSelectedState of the filter.
-        * - When we save the queries, we minimize the object to only represent
-        *   whatever has actually changed, rather than store the entire
-        *   object. To check what actually is different so we can store it,
-        *   we need to obtain a base state to compare against, this is
-        *   what #_getMinimalFilterList does
-        */
-       mw.rcfilters.Controller.prototype._buildBaseFilterState = function () {
-               var defaultParams = this.filtersModel.getDefaultParams(),
-                       highlightedItems = {};
-
-               // Prepare highlights
-               this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
-                       highlightedItems[ item.getName() ] = null;
-               } );
-               highlightedItems.highlight = false;
-
-               this.baseFilterState = {
-                       filters: this.filtersModel.getFiltersFromParameters( defaultParams ),
-                       highlights: highlightedItems,
-                       invert: false
-               };
-       };
-
-       /**
-        * Get an object representing the base filter state of both
-        * filters and highlights. The structure is similar to what we use
-        * to store each query in the saved queries object:
-        * {
-        *    filters: {
-        *        filterName: (bool)
-        *    },
-        *    highlights: {
-        *        filterName: (string|null)
-        *    }
-        * }
-        *
-        * @return {Object} Object representing the base state of
-        *  parameters and highlights
-        */
-       mw.rcfilters.Controller.prototype._getBaseFilterState = function () {
-               return this.baseFilterState;
-       };
-
-       /**
-        * Get an object that holds only the parameters and highlights that have
-        * values different than the base default value.
-        *
-        * This is the reverse of the normalization we do initially on loading and
-        * initializing the saved queries model.
-        *
-        * @param {Object} valuesObject Object representing the state of both
-        *  filters and highlights in its normalized version, to be minimized.
-        * @return {Object} Minimal filters and highlights list
-        */
-       mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) {
-               var result = { filters: {}, highlights: {}, invert: valuesObject.invert },
-                       baseState = this._getBaseFilterState();
-
-               // XOR results
-               $.each( valuesObject.filters, function ( name, value ) {
-                       if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) {
-                               result.filters[ name ] = value;
-                       }
-               } );
-
-               $.each( valuesObject.highlights, function ( name, value ) {
-                       if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
-                               result.highlights[ name ] = value;
-                       }
-               } );
-
-               return result;
-       };
-
        /**
         * Save the current state of the saved queries model with all
         * query item representation in the user settings.
         */
        mw.rcfilters.Controller.prototype._saveSavedQueries = function () {
-               var stringified,
-                       state = this.savedQueriesModel.getState(),
-                       controller = this;
-
-               // Minimize before save
-               $.each( state.queries, function ( queryID, info ) {
-                       state.queries[ queryID ].data = controller._getMinimalFilterList( info.data );
-               } );
+               var stringified, oldPrefValue,
+                       backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
+                       state = this.savedQueriesModel.getState();
 
                // Stringify state
                stringified = JSON.stringify( state );
                        return;
                }
 
+               if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
+                       // The queries were converted from the previous version
+                       // Keep the old string in the [prefname]-versionbackup
+                       oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+
+                       // Save the old preference in the backup preference
+                       new mw.Api().saveOption( backupPrefName, oldPrefValue );
+                       // Update the preference for this session
+                       mw.user.options.set( backupPrefName, oldPrefValue );
+               }
+
                // Save the preference
                new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
                // Update the preference for this session
                mw.user.options.set( this.savedQueriesPreferenceName, stringified );
+
+               // Tag as already saved so we don't do this again
+               this.wereSavedQueriesSaved = true;
        };
 
        /**
         * @return {Object} Default parameters
         */
        mw.rcfilters.Controller.prototype._getDefaultParams = function () {
-               var data, queryHighlights,
-                       savedParams = {},
-                       savedHighlights = {},
-                       defaultSavedQueryItem = !mw.user.isAnon() && this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
-
-               if ( defaultSavedQueryItem ) {
-                       data = defaultSavedQueryItem.getData();
-
-                       queryHighlights = data.highlights || {};
-                       savedParams = this.filtersModel.getParametersFromFilters(
-                               // Merge filters with sticky values
-                               $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+               var savedFilters,
+                       data = ( !mw.user.isAnon() && this.savedQueriesModel.getItemFullData( this.savedQueriesModel.getDefault() ) ) || {};
+
+               if ( !$.isEmptyObject( data ) ) {
+                       // Merge saved filter state with sticky filter values
+                       savedFilters = $.extend(
+                               true, {},
+                               this.filtersModel.getFiltersFromParameters( data.params ),
+                               this.filtersModel.getStickyFiltersState()
                        );
 
-                       // Translate highlights to parameters
-                       savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
-                       $.each( queryHighlights, function ( filterName, color ) {
-                               if ( filterName !== 'highlights' ) {
-                                       savedHighlights[ filterName + '_color' ] = color;
-                               }
-                       } );
-
-                       return $.extend( true, {}, savedParams, savedHighlights, { invert: String( Number( data.invert || 0 ) ) } );
+                       // Return parameter representation
+                       return $.extend( true, {},
+                               this.filtersModel.getParametersFromFilters( savedFilters ),
+                               data.highlights,
+                               { highlight: data.params.highlight, invert: data.params.invert }
+                       );
                }
-
                return this.filtersModel.getDefaultParams();
        };
 
        };
 
        /**
-        * Fetch the list of changes from the server for the current filters
+        * Query the list of changes from the server for the current filters
         *
-        * @param {string} [counterId='updateChangesList'] Id for this request. To allow concurrent requests
+        * @param {string} counterId Id for this request. To allow concurrent requests
         *  not to invalidate each other.
         * @param {Object} [params={}] Parameters to add to the query
         *
-        * @return {jQuery.Promise} Promise object that will resolve with the changes list
-        *  or with a string denoting no results.
+        * @return {jQuery.Promise} Promise object resolved with { content, status }
         */
-       mw.rcfilters.Controller.prototype._fetchChangesList = function ( counterId, params ) {
+       mw.rcfilters.Controller.prototype._queryChangesList = function ( counterId, params ) {
                var uri = this._getUpdatedUri(),
                        stickyParams = this.filtersModel.getStickyParams(),
                        requestId,
                        latestRequest;
 
-               counterId = counterId || 'updateChangesList';
                params = params || {};
                params.action = 'render'; // bypasses MW chrome
 
 
                return $.ajax( uri.toString(), { contentType: 'html' } )
                        .then(
-                               function ( html, reason ) {
-                                       var $parsed,
-                                               pieces;
-
+                               function ( content, message, jqXHR ) {
                                        if ( !latestRequest() ) {
                                                return $.Deferred().reject();
                                        }
-
-                                       if ( params.peek && reason === 'notmodified' ) {
-                                               return {
-                                                       changes: 'NO_RESULTS'
-                                               };
+                                       return {
+                                               content: content,
+                                               status: jqXHR.status
+                                       };
+                               },
+                               // RC returns 404 when there is no results
+                               function ( jqXHR ) {
+                                       if ( latestRequest() ) {
+                                               return $.Deferred().resolve(
+                                                       {
+                                                               content: jqXHR.responseText,
+                                                               status: jqXHR.status
+                                                       }
+                                               ).promise();
                                        }
+                               }
+                       );
+       };
 
-                                       // Because of action=render, the response is a list of nodes.
-                                       // It has to be put under a root node so it can be queried.
-                                       $parsed = $( '<div>' ).append( $( $.parseHTML( html ) ) );
-
-                                       pieces = {
-                                               // Changes list
-                                               changes: $parsed.find( '.mw-changeslist' ).first().contents(),
-                                               // Fieldset
-                                               fieldset: $parsed.find( 'fieldset.cloptions' ).first()
-                                       };
+       /**
+        * Fetch the list of changes from the server for the current filters
+        *
+        * @return {jQuery.Promise} Promise object that will resolve with the changes list
+        *  and the fieldset.
+        */
+       mw.rcfilters.Controller.prototype._fetchChangesList = function () {
+               return this._queryChangesList( 'updateChangesList' )
+                       .then(
+                               function ( data ) {
+                                       var $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) ),
+                                               pieces = {
+                                                       // Changes list
+                                                       changes: $parsed.find( '.mw-changeslist' ).first().contents(),
+                                                       // Fieldset
+                                                       fieldset: $parsed.find( 'fieldset.cloptions' ).first()
+                                               };
 
-                                       // Watchlist returns 200 when there is no results
                                        if ( pieces.changes.length === 0 ) {
                                                pieces.changes = 'NO_RESULTS';
                                        }
 
                                        return pieces;
-                               },
-                               // RC returns 404 when there is no results
-                               function ( responseObj ) {
-                                       var $parsed;
-
-                                       if ( !latestRequest() ) {
-                                               return $.Deferred().reject();
-                                       }
-
-                                       $parsed = $( $.parseHTML( responseObj.responseText ) );
-
-                                       // Force a resolve state to this promise
-                                       return $.Deferred().resolve( {
-                                               changes: 'NO_RESULTS',
-                                               fieldset: $parsed.find( 'fieldset.cloptions' ).first()
-                                       } ).promise();
                                }
                        );
        };