RCFilters: Set up conditional views for RCLinked
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / mw.rcfilters.Controller.js
index 0b2dd8d..7b5e115 100644 (file)
         * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
         * @param {Object} config Additional configuration
         * @cfg {string} savedQueriesPreferenceName Where to save the saved queries
+        * @cfg {string} daysPreferenceName Preference name for the days filter
+        * @cfg {string} limitPreferenceName Preference name for the limit filter
         */
        mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
                this.filtersModel = filtersModel;
                this.changesListModel = changesListModel;
                this.savedQueriesModel = savedQueriesModel;
                this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
+               this.daysPreferenceName = config.daysPreferenceName;
+               this.limitPreferenceName = config.limitPreferenceName;
 
                this.requestCounter = {};
                this.baseFilterState = {};
         * @param {Array} filterStructure Filter definition and structure for the model
         * @param {Object} [namespaceStructure] Namespace definition
         * @param {Object} [tagList] Tag definition
+        * @param {Object} [conditionalViews] Conditional view definition
         */
-       mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
-               var parsedSavedQueries,
+       mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList, conditionalViews ) {
+               var parsedSavedQueries, pieces,
                        displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
                        defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
                        controller = this,
-                       views = {},
+                       views = $.extend( true, {}, conditionalViews ),
                        items = [],
-                       uri = new mw.Uri(),
-                       $changesList = $( '.mw-changeslist' ).first().contents();
+                       uri = new mw.Uri();
 
                // Prepare views
                if ( namespaceStructure ) {
                                        separator: ';',
                                        fullCoverage: true,
                                        filters: items
-                               },
-                               {
-                                       name: 'invertGroup',
-                                       type: 'boolean',
-                                       hidden: true,
-                                       filters: [ {
-                                               name: 'invert',
-                                               'default': '0'
-                                       } ]
                                } ]
                        };
+                       views.invert = {
+                               groups: [
+                                       {
+                                               name: 'invertGroup',
+                                               type: 'boolean',
+                                               hidden: true,
+                                               filters: [ {
+                                                       name: 'invert',
+                                                       'default': '0'
+                                               } ]
+                                       } ]
+                       };
                }
                if ( tagList ) {
                        views.tags = {
                                                max: 1000
                                        },
                                        sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
-                                       'default': displayConfig.limitDefault,
-                                       // Temporarily making this not sticky until we resolve the problem
-                                       // with the misleading preference. Note that if this is to be permanent
-                                       // we should remove all sticky behavior methods completely
-                                       // See T172156
-                                       // isSticky: true,
-                                       excludedFromSavedQueries: true,
+                                       'default': mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
+                                       sticky: true,
                                        filters: displayConfig.limitArray.map( function ( num ) {
                                                return controller._createFilterDataFromNumber( num, num );
                                        } )
                                                        ( Number( i ) * 24 ).toFixed( 2 ) :
                                                        Number( i );
                                        },
-                                       'default': displayConfig.daysDefault,
-                                       // Temporarily making this not sticky while limit is not sticky, see above
-                                       // isSticky: true,
-                                       excludedFromSavedQueries: true,
+                                       'default': mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
+                                       sticky: true,
                                        filters: [
                                                // Hours (1, 2, 6, 12)
                                                0.04166, 0.0833, 0.25, 0.5
                                        type: 'boolean',
                                        title: '', // Because it's a hidden group, this title actually appears nowhere
                                        hidden: true,
-                                       isSticky: true,
+                                       sticky: true,
                                        filters: [
                                                {
                                                        name: 'enhanced',
                        // again
                        this.updateStateFromUrl( false );
 
+                       pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
+
                        // Update the changes list with the existing data
                        // so it gets processed
                        this.changesListModel.update(
-                               $changesList.length ? $changesList : 'NO_RESULTS',
-                               $( 'fieldset.cloptions' ).first(),
+                               pieces.changes,
+                               pieces.fieldset,
+                               pieces.noResultsDetails,
                                true // We're using existing DOM elements
                        );
                }
                }
        };
 
+       /**
+        * Extracts information from the changes list DOM
+        *
+        * @param {jQuery} $root Root DOM to find children from
+        * @return {Object} Information about changes list
+        * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
+        *   (either normally or as an error)
+        * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
+        *   'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
+        * @return {jQuery} return.fieldset Fieldset
+        */
+       mw.rcfilters.Controller.prototype._extractChangesListInfo = function ( $root ) {
+               var info,
+                       $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
+                       areResults = !!$changesListContents.length;
+
+               info = {
+                       changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
+                       fieldset: $root.find( 'fieldset.cloptions' ).first()
+               };
+
+               if ( !areResults ) {
+                       if ( $root.find( '.mw-changeslist-timeout' ).length ) {
+                               info.noResultsDetails = 'NO_RESULTS_TIMEOUT';
+                       } else if ( $root.find( '.mw-changeslist-notargetpage' ).length ) {
+                               info.noResultsDetails = 'NO_RESULTS_NO_TARGET_PAGE';
+                       } else {
+                               info.noResultsDetails = 'NO_RESULTS_NORMAL';
+                       }
+               }
+
+               return info;
+       };
+
        /**
         * Create filter data from a number, for the filters that are numerical value
         *
         * Reset to default filters
         */
        mw.rcfilters.Controller.prototype.resetToDefaults = function () {
-               this.filtersModel.updateStateFromParams( this._getDefaultParams() );
-
-               this.updateChangesList();
+               var params = this._getDefaultParams();
+               if ( this.applyParamChange( params ) ) {
+                       // Only update the changes list if there was a change to actual filters
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL( params );
+               }
        };
 
        /**
         * @return {boolean} Defaults are all false
         */
        mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
-               return $.isEmptyObject( this._getDefaultParams( true ) );
+               return $.isEmptyObject( this._getDefaultParams() );
        };
 
        /**
         * Empty all selected filters
         */
        mw.rcfilters.Controller.prototype.emptyFilters = function () {
-               var highlightedFilterNames = this.filtersModel
-                       .getHighlightedItems()
+               var highlightedFilterNames = this.filtersModel.getHighlightedItems()
                        .map( function ( filterItem ) { return { name: filterItem.getName() }; } );
 
-               this.filtersModel.updateStateFromParams( {} );
-
-               this.updateChangesList();
+               if ( this.applyParamChange( {} ) ) {
+                       // Only update the changes list if there was a change to actual filters
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL();
+               }
 
                if ( highlightedFilterNames ) {
                        this._trackHighlight( 'clearAll', highlightedFilterNames );
         */
        mw.rcfilters.Controller.prototype.toggleInvertedNamespaces = function () {
                this.filtersModel.toggleInvertedNamespaces();
-
                if (
                        this.filtersModel.getFiltersByView( 'namespaces' ).filter(
                                function ( filterItem ) { return filterItem.isSelected(); }
                ) {
                        // Only re-fetch results if there are namespace items that are actually selected
                        this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL();
                }
        };
 
+       /**
+        * Set the value of the 'showlinkedto' parameter
+        * @param {boolean} value
+        */
+       mw.rcfilters.Controller.prototype.setShowLinkedTo = function ( value ) {
+               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' ),
+                       showLinkedToItem = this.filtersModel.getGroup( 'toOrFrom' ).getItemByParamName( 'showlinkedto' );
+
+               this.filtersModel.toggleFilterSelected( showLinkedToItem.getName(), value );
+               this.uriProcessor.updateURL();
+               // reload the results only when target is set
+               if ( targetItem.getValue() ) {
+                       this.updateChangesList();
+               }
+       };
+
+       /**
+        * Set the target page
+        * @param {string} page
+        */
+       mw.rcfilters.Controller.prototype.setTargetPage = function ( page ) {
+               var targetItem = this.filtersModel.getGroup( 'page' ).getItemByParamName( 'target' );
+               targetItem.setValue( page );
+               this.uriProcessor.updateURL();
+               this.updateChangesList();
+       };
+
        /**
         * Set the highlight color for a filter item
         *
                        return;
                }
 
-               // Apply parameters to model
-               this.filtersModel.updateStateFromParams( params );
-
-               this.updateChangesList();
+               if ( this.applyParamChange( params ) ) {
+                       // Update changes list only if there was a difference in filter selection
+                       this.updateChangesList();
+               } else {
+                       this.uriProcessor.updateURL( params );
+               }
 
                // Log filter grouping
                this.trackFilterGroupings( 'savedfilters' );
         * Check whether the current filter and highlight state exists
         * in the saved queries model.
         *
-        * @return {boolean} Query exists
+        * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
         */
        mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
                return this.savedQueriesModel.findMatchingQuery(
        /**
         * Update the limit default value
         *
-        * param {number} newValue New value
+        * @param {number} newValue New value
         */
-       mw.rcfilters.Controller.prototype.updateLimitDefault = function ( /* newValue */ ) {
-               // HACK: Temporarily remove this from being sticky
-               // See T172156
-
-               /*
-               if ( !$.isNumeric( newValue ) ) {
-                       return;
-               }
-
-               newValue = Number( newValue );
-
-               if ( mw.user.options.get( 'rcfilters-rclimit' ) !== newValue ) {
-                       // Save the preference
-                       new mw.Api().saveOption( 'rcfilters-rclimit', newValue );
-                       // Update the preference for this session
-                       mw.user.options.set( 'rcfilters-rclimit', newValue );
-               }
-               */
-               return;
+       mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
+               this.updateNumericPreference( this.limitPreferenceName, newValue );
        };
 
        /**
         * Update the days default value
         *
-        * param {number} newValue New value
+        * @param {number} newValue New value
         */
-       mw.rcfilters.Controller.prototype.updateDaysDefault = function ( /* newValue */ ) {
-               // HACK: Temporarily remove this from being sticky
-               // See T172156
-
-               /*
-               if ( !$.isNumeric( newValue ) ) {
-                       return;
-               }
-
-               newValue = Number( newValue );
-
-               if ( mw.user.options.get( 'rcdays' ) !== newValue ) {
-                       // Save the preference
-                       new mw.Api().saveOption( 'rcdays', newValue );
-                       // Update the preference for this session
-                       mw.user.options.set( 'rcdays', newValue );
-               }
-               */
-               return;
+       mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
+               this.updateNumericPreference( this.daysPreferenceName, newValue );
        };
 
        /**
         * Update the group by page default value
         *
-        * @param {number} newValue New value
+        * @param {boolean} newValue New value
         */
        mw.rcfilters.Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
+               this.updateNumericPreference( 'usenewrc', Number( newValue ) );
+       };
+
+       /**
+        * Update a numeric preference with a new value
+        *
+        * @param {string} prefName Preference name
+        * @param {number|string} newValue New value
+        */
+       mw.rcfilters.Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
                if ( !$.isNumeric( newValue ) ) {
                        return;
                }
 
                newValue = Number( newValue );
 
-               if ( mw.user.options.get( 'usenewrc' ) !== newValue ) {
+               if ( mw.user.options.get( prefName ) !== newValue ) {
                        // Save the preference
-                       new mw.Api().saveOption( 'usenewrc', newValue );
+                       new mw.Api().saveOption( prefName, newValue );
                        // Update the preference for this session
-                       mw.user.options.set( 'usenewrc', newValue );
+                       mw.user.options.set( prefName, newValue );
                }
        };
 
         * without adding an history entry.
         */
        mw.rcfilters.Controller.prototype.replaceUrl = function () {
-               this.uriProcessor.replaceUpdatedUri();
+               this.uriProcessor.updateURL();
        };
 
        /**
        mw.rcfilters.Controller.prototype.updateStateFromUrl = function ( fetchChangesList ) {
                fetchChangesList = fetchChangesList === undefined ? true : !!fetchChangesList;
 
-               this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
+               this.uriProcessor.updateModelBasedOnQuery();
 
                // Update the sticky preferences, in case we received a value
                // from the URL
                                        this.changesListModel.update(
                                                $changesListContent,
                                                $fieldset,
+                                               pieces.noResultsDetails,
                                                false,
                                                // separator between old and new changes
                                                updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
         * Get an object representing the default parameter state, whether
         * it is from the model defaults or from the saved queries.
         *
-        * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params
         * @return {Object} Default parameters
         */
-       mw.rcfilters.Controller.prototype._getDefaultParams = function ( excludeHiddenParams ) {
+       mw.rcfilters.Controller.prototype._getDefaultParams = function () {
                if ( this.savedQueriesModel.getDefault() ) {
-                       return this.savedQueriesModel.getDefaultParams( excludeHiddenParams );
+                       return this.savedQueriesModel.getDefaultParams();
                } else {
-                       return this.filtersModel.getDefaultParams( excludeHiddenParams );
+                       return this.filtersModel.getDefaultParams();
                }
        };
 
                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()
+                                       var $parsed;
+
+                                       // Status code 0 is not HTTP status code,
+                                       // but is valid value of XMLHttpRequest status.
+                                       // It is used for variety of network errors, for example
+                                       // when an AJAX call was cancelled before getting the response
+                                       if ( data && data.status === 0 ) {
+                                               return {
+                                                       changes: 'NO_RESULTS',
+                                                       // We need empty result set, to avoid exceptions because of undefined value
+                                                       fieldset: $( [] ),
+                                                       noResultsDetails: 'NO_RESULTS_NETWORK_ERROR'
                                                };
-
-                                       if ( pieces.changes.length === 0 ) {
-                                               pieces.changes = 'NO_RESULTS';
                                        }
 
-                                       return pieces;
-                               }
+                                       $parsed = $( '<div>' ).append( $( $.parseHTML(
+                                               data ? data.content : ''
+                                       ) ) );
+
+                                       return this._extractChangesListInfo( $parsed );
+                               }.bind( this )
                        );
        };
 
                }
        };
 
+       /**
+        * Apply a change of parameters to the model state, and check whether
+        * the new state is different than the old state.
+        *
+        * @param  {Object} newParamState New parameter state to apply
+        * @return {boolean} New applied model state is different than the previous state
+        */
+       mw.rcfilters.Controller.prototype.applyParamChange = function ( newParamState ) {
+               var after,
+                       before = this.filtersModel.getSelectedState();
+
+               this.filtersModel.updateStateFromParams( newParamState );
+
+               after = this.filtersModel.getSelectedState();
+
+               return !OO.compare( before, after );
+       };
+
        /**
         * Mark all changes as seen on Watchlist
         */