X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=resources%2Fsrc%2Fmediawiki.rcfilters%2Fmw.rcfilters.Controller.js;h=f0e3bd0d97c6e46de5731ce77670d89051d567f4;hp=256347944e72be564ebdea82efaa9e112af78364;hb=d6551bcbad8e2c0c94af630212433150ac7ff841;hpb=6acb87943214510e3f81d6061dca85ad37efcd29 diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 256347944e..f0e3bd0d97 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -13,12 +13,16 @@ this.filtersModel = filtersModel; this.changesListModel = changesListModel; this.savedQueriesModel = savedQueriesModel; - this.requestCounter = 0; + this.requestCounter = {}; this.baseFilterState = {}; this.uriProcessor = null; this.initializing = false; this.prevLoggedItems = []; + + this.FILTER_CHANGE = 'filterChange'; + this.SHOW_NEW_CHANGES = 'showNewChanges'; + this.LIVE_UPDATE = 'liveUpdate'; }; /* Initialization */ @@ -32,7 +36,8 @@ * @param {Object} [tagList] Tag definition */ mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) { - var parsedSavedQueries, + var parsedSavedQueries, limitDefault, + displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ), controller = this, views = {}, items = [], @@ -88,6 +93,11 @@ }; } + // Convert the default from the old preference + // since the limit preference actually affects more + // than just the RecentChanges page + limitDefault = Number( mw.user.options.get( 'rclimit', '50' ) ); + // Add parameter range operations views.range = { groups: [ @@ -98,9 +108,19 @@ hidden: true, allowArbitrary: true, validate: $.isNumeric, + range: { + min: 0, // The server normalizes negative numbers to 0 results + max: 1000 + }, 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 ) { + 'default': String( 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, + filters: displayConfig.limitArray.map( function ( num ) { return controller._createFilterDataFromNumber( num, num ); } ) }, @@ -111,25 +131,50 @@ hidden: true, allowArbitrary: true, validate: $.isNumeric, + range: { + min: 0, + max: displayConfig.maxDays + }, 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' ), + 'default': mw.user.options.get( 'rcdays', '30' ), + // Temporarily making this not sticky while limit is not sticky, see above + // isSticky: true, + excludedFromSavedQueries: true, 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 - ); - } ) + 0.04166, 0.0833, 0.25, 0.5 + // Days + ].concat( displayConfig.daysArray ) + .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 + ); + } ) + } + ] + }; + + views.display = { + groups: [ + { + name: 'display', + type: 'boolean', + title: '', // Because it's a hidden group, this title actually appears nowhere + hidden: true, + isSticky: true, + filters: [ + { + name: 'enhanced', + 'default': String( mw.user.options.get( 'usenewrc', 0 ) ) + } + ] } ] }; @@ -174,7 +219,9 @@ // can normalize them per each query item this.savedQueriesModel.initialize( parsedSavedQueries, - this._getBaseFilterState() + this._getBaseFilterState(), + // This is for backwards compatibility - delete all excluded filter states + Object.keys( this.filtersModel.getExcludedFiltersState() ) ); // Check whether we need to load defaults. @@ -214,6 +261,8 @@ this.initializing = false; this.switchView( 'default' ); + + this._scheduleLiveUpdate(); }; /** @@ -237,10 +286,30 @@ * @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; + var controller = this, + normalizeWithinRange = function ( range, val ) { + if ( val < range.min ) { + return range.min; // Min + } else if ( val >= range.max ) { + return range.max; // Max + } + return val; + }; arbitraryValues = Array.isArray( arbitraryValues ) ? arbitraryValues : [ arbitraryValues ]; + // Normalize the arbitrary values and the default value for a range + if ( groupData.range ) { + arbitraryValues = arbitraryValues.map( function ( val ) { + return normalizeWithinRange( groupData.range, val ); + } ); + + // Normalize the default, since that's user defined + if ( groupData.default !== undefined ) { + groupData.default = String( normalizeWithinRange( groupData.range, groupData.default ) ); + } + } + // 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 @@ -258,9 +327,9 @@ // but if that value isn't already in the definition groupData.filters .map( function ( filterData ) { - return filterData.name; + return String( filterData.name ); } ) - .indexOf( val ) === -1 + .indexOf( String( val ) ) === -1 ) { // Add the filter information groupData.filters.push( controller._createFilterDataFromNumber( @@ -292,6 +361,7 @@ */ mw.rcfilters.Controller.prototype.resetToDefaults = function () { this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() ); + this.updateChangesList(); }; @@ -382,7 +452,17 @@ */ mw.rcfilters.Controller.prototype.toggleInvertedNamespaces = function () { this.filtersModel.toggleInvertedNamespaces(); - this.updateChangesList(); + + if ( + this.filtersModel.getFiltersByView( 'namespaces' ) + .filter( function ( filterItem ) { + return filterItem.isSelected(); + } ) + .length + ) { + // Only re-fetch results if there are namespace items that are actually selected + this.updateChangesList(); + } }; /** @@ -413,11 +493,9 @@ * @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; + this.changesListModel.toggleLiveUpdate( enable ); + if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) { + this.updateChangesList( null, this.LIVE_UPDATE ); } }; @@ -426,7 +504,7 @@ * @private */ mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () { - this.liveUpdateTimeout = setTimeout( this._doLiveUpdate.bind( this ), 3000 ); + setTimeout( this._doLiveUpdate.bind( this ), 3000 ); }; /** @@ -434,24 +512,82 @@ * @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(); + if ( !this._shouldCheckForNewChanges() ) { + // skip this turn and check back later + this._scheduleLiveUpdate(); + return; + } + + this._checkForNewChanges() + .then( function ( data ) { + if ( !this._shouldCheckForNewChanges() ) { + // by the time the response is received, + // it may not be appropriate anymore + return; } - } ); + + if ( data.changes !== 'NO_RESULTS' ) { + if ( this.changesListModel.getLiveUpdate() ) { + return this.updateChangesList( null, this.LIVE_UPDATE ); + } else { + this.changesListModel.setNewChangesExist( true ); + } + } + }.bind( this ) ) + .always( this._scheduleLiveUpdate.bind( this ) ); + }; + + /** + * @return {boolean} It's appropriate to check for new changes now + * @private + */ + mw.rcfilters.Controller.prototype._shouldCheckForNewChanges = function () { + return !document.hidden && + !this.filtersModel.hasConflict() && + !this.changesListModel.getNewChangesExist() && + !this.updatingChangesList && + mw.rcfilters.featureFlags.liveUpdate; + }; + + /** + * 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 + * + * @private + */ + mw.rcfilters.Controller.prototype._checkForNewChanges = function () { + return this._fetchChangesList( + 'liveUpdate', + { + limit: 1, + from: this.changesListModel.getNextFrom() + } + ); + }; + + /** + * Show the new changes + * + * @return {jQuery.Promise} Promise object that resolves after + * fetching and showing the new changes + */ + mw.rcfilters.Controller.prototype.showNewChanges = function () { + return this.updateChangesList( null, this.SHOW_NEW_CHANGES ); }; /** * Save the current model state as a saved query * * @param {string} [label] Label of the saved query + * @param {boolean} [setAsDefault=false] This query should be set as the default */ - mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label ) { - var highlightedItems = {}, - highlightEnabled = this.filtersModel.isHighlightEnabled(); + mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) { + var queryID, + highlightedItems = {}, + highlightEnabled = this.filtersModel.isHighlightEnabled(), + selectedState = this.filtersModel.getSelectedState(); // Prepare highlights this.filtersModel.getHighlightedItems().forEach( function ( item ) { @@ -461,16 +597,23 @@ // These are filter states; highlight is stored as boolean highlightedItems.highlight = this.filtersModel.isHighlightEnabled(); + // Delete all excluded filters + this._deleteExcludedValuesFromFilterState( selectedState ); + // Add item - this.savedQueriesModel.addNewQuery( + queryID = this.savedQueriesModel.addNewQuery( label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ), { - filters: this.filtersModel.getSelectedState(), + filters: selectedState, highlights: highlightedItems, invert: this.filtersModel.areNamespacesInverted() } ); + if ( setAsDefault ) { + this.savedQueriesModel.setDefault( queryID ); + } + // Save item this._saveSavedQueries(); }; @@ -519,9 +662,18 @@ */ mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) { var data, highlights, - queryItem = this.savedQueriesModel.getItemByID( queryID ); + queryItem = this.savedQueriesModel.getItemByID( queryID ), + currentMatchingQuery = this.findQueryMatchingCurrentState(); - if ( queryItem ) { + if ( + queryItem && + ( + // If there's already a query, don't reload it + // if it's the same as the one that already exists + !currentMatchingQuery || + currentMatchingQuery.getID() !== queryItem.getID() + ) + ) { data = queryItem.getData(); highlights = data.highlights; @@ -529,7 +681,10 @@ highlights.highlight = highlights.highlights || highlights.highlight; // Update model state from filters - this.filtersModel.toggleFiltersSelected( data.filters ); + this.filtersModel.toggleFiltersSelected( + // Merge filters with excluded values + $.extend( true, {}, data.filters, this.filtersModel.getExcludedFiltersState() ) + ); // Update namespace inverted property this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) ); @@ -562,7 +717,8 @@ * @return {boolean} Query exists */ mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () { - var highlightedItems = {}; + var highlightedItems = {}, + selectedState = this.filtersModel.getSelectedState(); // Prepare highlights of the current query this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { @@ -570,15 +726,31 @@ } ); highlightedItems.highlight = this.filtersModel.isHighlightEnabled(); + // Remove anything that should be excluded from the saved query + // this includes sticky filters and filters marked with 'excludedFromSavedQueries' + this._deleteExcludedValuesFromFilterState( selectedState ); + return this.savedQueriesModel.findMatchingQuery( { - filters: this.filtersModel.getSelectedState(), + filters: selectedState, highlights: highlightedItems, invert: this.filtersModel.areNamespacesInverted() } ); }; + /** + * Delete sticky filters from given object + * + * @param {Object} filterState Filter state + */ + mw.rcfilters.Controller.prototype._deleteExcludedValuesFromFilterState = function ( filterState ) { + // Remove excluded filters + $.each( this.filtersModel.getExcludedFiltersState(), function ( filterName ) { + delete filterState[ filterName ]; + } ); + }; + /** * Get an object representing the base state of parameters * and highlights. @@ -701,6 +873,91 @@ mw.user.options.set( 'rcfilters-saved-queries', stringified ); }; + /** + * Update sticky preferences with current model state + */ + mw.rcfilters.Controller.prototype.updateStickyPreferences = function () { + // Update default sticky values with selected, whether they came from + // the initial defaults or from the URL value that is being normalized + this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).getSelectedItems()[ 0 ].getParamName() ); + this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).getSelectedItems()[ 0 ].getParamName() ); + + // TODO: Make these automatic by having the model go over sticky + // items and update their default values automatically + }; + + /** + * Update the limit default 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; + }; + + /** + * Update the days default 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; + }; + + /** + * Update the group by page default value + * + * @param {number} newValue New value + */ + mw.rcfilters.Controller.prototype.updateGroupByPageDefault = function ( newValue ) { + if ( !$.isNumeric( newValue ) ) { + return; + } + + newValue = Number( newValue ); + + if ( mw.user.options.get( 'usenewrc' ) !== newValue ) { + // Save the preference + new mw.Api().saveOption( 'usenewrc', newValue ); + // Update the preference for this session + mw.user.options.set( 'usenewrc', newValue ); + } + }; + /** * Synchronize the URL with the current state of the filters * without adding an history entry. @@ -721,6 +978,10 @@ this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query ); + // Update the sticky preferences, in case we received a value + // from the URL + this.updateStickyPreferences(); + // Only update and fetch new results if it is requested if ( fetchChangesList ) { this.updateChangesList(); @@ -731,24 +992,38 @@ * 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 + * @param {string} [updateMode='filterChange'] One of 'filterChange', 'liveUpdate', 'showNewChanges' * @return {jQuery.Promise} Promise that is resolved when the update is complete */ - mw.rcfilters.Controller.prototype.updateChangesList = function ( params, isLiveUpdate ) { - if ( !isLiveUpdate ) { + mw.rcfilters.Controller.prototype.updateChangesList = function ( params, updateMode ) { + updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode; + + if ( updateMode === this.FILTER_CHANGE ) { this._updateURL( params ); + } + if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) { this.changesListModel.invalidate(); } + this.changesListModel.setNewChangesExist( false ); + this.updatingChangesList = true; return this._fetchChangesList() .then( // Success function ( pieces ) { var $changesListContent = pieces.changes, $fieldset = pieces.fieldset; - this.changesListModel.update( $changesListContent, $fieldset ); + this.changesListModel.update( + $changesListContent, + $fieldset, + false, + updateMode === this.SHOW_NEW_CHANGES + ); }.bind( this ) // Do nothing for failure - ); + ) + .always( function () { + this.updatingChangesList = false; + }.bind( this ) ); }; /** @@ -763,13 +1038,14 @@ savedHighlights = {}, defaultSavedQueryItem = this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() ); - if ( mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ) && - defaultSavedQueryItem ) { - + if ( defaultSavedQueryItem ) { data = defaultSavedQueryItem.getData(); queryHighlights = data.highlights || {}; - savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} ); + savedParams = this.filtersModel.getParametersFromFilters( + // Merge filters with sticky values + $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() ) + ); // Translate highlights to parameters savedHighlights.highlight = String( Number( queryHighlights.highlight ) ); @@ -839,15 +1115,37 @@ /** * Fetch the list of changes from the server for the current filters * + * @param {string} [counterId='updateChangesList'] 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. */ - mw.rcfilters.Controller.prototype._fetchChangesList = function () { + mw.rcfilters.Controller.prototype._fetchChangesList = function ( counterId, params ) { var uri = this._getUpdatedUri(), - requestId = ++this.requestCounter, - latestRequest = function () { - return requestId === this.requestCounter; - }.bind( this ); + stickyParams = this.filtersModel.getStickyParams(), + requestId, + latestRequest; + + counterId = counterId || 'updateChangesList'; + params = params || {}; + + uri.extend( params ); + + this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0; + requestId = ++this.requestCounter[ counterId ]; + latestRequest = function () { + return requestId === this.requestCounter[ counterId ]; + }.bind( this ); + + // Sticky parameters override the URL params + // this is to make sure that whether we represent + // the sticky params in the URL or not (they may + // be normalized out) the sticky parameters are + // always being sent to the server with their + // current/default values + uri.extend( stickyParams ); return $.ajax( uri.toString(), { contentType: 'html' } ) .then(