From: jenkins-bot Date: Tue, 17 Oct 2017 19:22:57 +0000 (+0000) Subject: Merge "RCFilters: Move parameter operations to ViewModel" X-Git-Tag: 1.31.0-rc.0~1739 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=cb615a80599a409976518e7564cc6d993b772714;hp=010ea99453f3c5a51983a3b620982a30b2a8ee90 Merge "RCFilters: Move parameter operations to ViewModel" --- diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js index 57d1b4108f..b17355f5ba 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js @@ -916,4 +916,35 @@ mw.rcfilters.dm.FilterGroup.prototype.isExcludedFromSavedQueries = function () { return this.excludedFromSavedQueries; }; + + /** + * Normalize a value given to this group. This is mostly for correcting + * arbitrary values for 'single option' groups, given by the user settings + * or the URL that can go outside the limits that are allowed. + * + * @param {string} value Given value + * @return {string} Corrected value + */ + mw.rcfilters.dm.FilterGroup.prototype.normalizeArbitraryValue = function ( value ) { + if ( + this.getType() === 'single_option' && + this.isAllowArbitrary() + ) { + if ( + this.getMaxValue() !== null && + value > this.getMaxValue() + ) { + // Change the value to the actual max value + return String( this.getMaxValue() ); + } else if ( + this.getMinValue() !== null && + value < this.getMinValue() + ) { + // Change the value to the actual min value + return String( this.getMinValue() ); + } + } + + return value; + }; }( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index 0d65466d2a..b8e112913a 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -17,6 +17,7 @@ this.defaultFiltersEmpty = null; this.highlightEnabled = false; this.parameterMap = {}; + this.emptyParameterState = null; this.views = {}; this.currentView = 'default'; @@ -410,6 +411,200 @@ this.emit( 'initialize' ); }; + /** + * Update filter view model state based on a parameter object + * + * @param {Object} params Parameters object + */ + mw.rcfilters.dm.FiltersViewModel.prototype.updateStateFromParams = function ( params ) { + // For arbitrary numeric single_option values make sure the values + // are normalized to fit within the limits + $.each( this.getFilterGroups(), function ( groupName, groupModel ) { + params[ groupName ] = groupModel.normalizeArbitraryValue( params[ groupName ] ); + } ); + + // Update filter states + this.toggleFiltersSelected( + this.getFiltersFromParameters( + params + ) + ); + + // Update highlight state + this.getItemsSupportingHighlights().forEach( function ( filterItem ) { + var color = params[ filterItem.getName() + '_color' ]; + if ( color ) { + filterItem.setHighlightColor( color ); + } else { + filterItem.clearHighlightColor(); + } + } ); + this.toggleHighlight( !!Number( params.highlight ) ); + + // Check all filter interactions + this.reassessFilterInteractions(); + }; + + /** + * Get a representation of an empty (falsey) parameter state + * + * @return {Object} Empty parameter state + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getEmptyParameterState = function () { + if ( !this.emptyParameterState ) { + this.emptyParameterState = $.extend( + true, + {}, + this.getParametersFromFilters( {} ), + this.getEmptyHighlightParameters(), + { highlight: '0' } + ); + } + return this.emptyParameterState; + }; + + /** + * Get a representation of only the non-falsey parameters + * + * @param {Object} [parameters] A given parameter state to minimize. If not given the current + * state of the system will be used. + * @return {Object} Empty parameter state + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getMinimizedParamRepresentation = function ( parameters ) { + var result = {}; + + parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState(); + + // Params + $.each( this.getEmptyParameterState(), function ( param, value ) { + if ( parameters[ param ] !== undefined && parameters[ param ] !== value ) { + result[ param ] = parameters[ param ]; + } + } ); + + // Highlights + Object.keys( this.getEmptyHighlightParameters() ).forEach( function ( param ) { + if ( param !== 'highlight' && parameters[ param ] ) { + // If a highlight parameter is not undefined and not null + // add it to the result + // Ignore "highlight" parameter because that, we checked already with + // the empty parameter state (and this soon changes to an implicit value) + result[ param ] = parameters[ param ]; + } + } ); + + return result; + }; + + /** + * Get a representation of the full parameter list, including all base values + * + * @param {Object} [parameters] A given parameter state to minimize. If not given the current + * state of the system will be used. + * @param {boolean} [removeExcluded] Remove excluded and sticky parameters + * @return {Object} Full parameter representation + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function ( parameters, removeExcluded ) { + var result = {}; + + parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState(); + + result = $.extend( + true, + {}, + this.getEmptyParameterState(), + parameters + ); + + if ( removeExcluded ) { + result = this.removeExcludedParams( result ); + } + + return result; + }; + + /** + * Get a parameter representation of the current state of the model + * + * @param {boolean} [removeExcludedParams] Remove excluded filters from final result + * @return {Object} Parameter representation of the current state of the model + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeExcludedParams ) { + var excludedParams, + state = this.getMinimizedParamRepresentation( $.extend( + true, + {}, + this.getParametersFromFilters( this.getSelectedState() ), + this.getHighlightParameters(), + { + // HACK: Add highlight. This is only needed while it's + // stored as an outside state + highlight: String( Number( this.isHighlightEnabled() ) ) + } + ) ); + + if ( removeExcludedParams ) { + excludedParams = this.getExcludedParams(); + // Delete all excluded filters + $.each( state, function ( param ) { + if ( excludedParams.indexOf( param ) > -1 ) { + delete state[ param ]; + } + } ); + } + + return state; + }; + + /** + * Delete excluded and sticky filters from given object. If object isn't given, output + * the current filter state without the excluded values + * + * @param {Object} [filterState] Filter state + * @return {Object} Filter state without excluded filters + */ + mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedFilters = function ( filterState ) { + filterState = filterState !== undefined ? + $.extend( true, {}, filterState ) : + this.getFiltersFromParameters(); + + // Remove excluded filters + Object.keys( this.getExcludedFiltersState() ).forEach( function ( filterName ) { + delete filterState[ filterName ]; + } ); + + // Remove sticky filters + Object.keys( this.getStickyFiltersState() ).forEach( function ( filterName ) { + delete filterState[ filterName ]; + } ); + + return filterState; + }; + /** + * Delete excluded and sticky parameters from given object. If object isn't given, output + * the current param state without the excluded values + * + * @param {Object} [paramState] Parameter state + * @return {Object} Parameter state without excluded filters + */ + mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedParams = function ( paramState ) { + paramState = paramState !== undefined ? + $.extend( true, {}, paramState ) : + this.getCurrentParameterState(); + + // Remove excluded filters + this.getExcludedParams().forEach( function ( paramName ) { + delete paramState[ paramName ]; + } ); + + // Remove sticky filters + this.getStickyParams().forEach( function ( paramName ) { + delete paramState[ paramName ]; + } ); + + return paramState; + }; + /** * Get the names of all available filters * @@ -529,9 +724,10 @@ /** * Get an object representing default parameters state * + * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params * @return {Object} Default parameter values */ - mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () { + mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function ( excludeHiddenParams ) { var result = {}; // Get default filter state @@ -539,6 +735,30 @@ $.extend( true, result, model.getDefaultParams() ); } ); + if ( excludeHiddenParams ) { + Object.keys( this.getDefaultHiddenParams() ).forEach( function ( paramName ) { + delete result[ paramName ]; + } ); + } + + return result; + }; + + /** + * Get an object representing defaults for the hidden parameters state + * + * @return {Object} Default values for hidden parameters + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultHiddenParams = function () { + var result = {}; + + // Get default filter state + $.each( this.groups, function ( name, model ) { + if ( model.isHidden() ) { + $.extend( true, result, model.getDefaultParams() ); + } + } ); + return result; }; @@ -548,6 +768,30 @@ * @return {Object} Sticky parameter values */ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () { + var result = []; + + $.each( this.groups, function ( name, model ) { + if ( model.isSticky() ) { + if ( model.isPerGroupRequestParameter() ) { + result.push( name ); + } else { + // Each filter is its own param + result = result.concat( model.getItems().map( function ( filterItem ) { + return filterItem.getParamName(); + } ) ); + } + } + } ); + + return result; + }; + + /** + * Get a parameter representation of all sticky parameters + * + * @return {Object} Sticky parameter values + */ + mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParamsValues = function () { var result = {}; $.each( this.groups, function ( name, model ) { @@ -714,7 +958,9 @@ var result = {}; this.getItems().forEach( function ( filterItem ) { - result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null; + if ( filterItem.isHighlightSupported() ) { + result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor() || null; + } } ); result.highlight = String( Number( this.isHighlightEnabled() ) ); @@ -730,7 +976,9 @@ var result = {}; this.getItems().forEach( function ( filterItem ) { - result[ filterItem.getName() + '_color' ] = null; + if ( filterItem.isHighlightSupported() ) { + result[ filterItem.getName() + '_color' ] = null; + } } ); result.highlight = '0'; diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js index edb9644843..29585e902d 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueriesModel.js @@ -80,8 +80,7 @@ * @fires initialize */ mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) { - var model = this, - excludedParams = this.filtersModel.getExcludedParams(); + var model = this; savedQueries = savedQueries || {}; @@ -122,12 +121,24 @@ if ( normalizedData && normalizedData.params ) { // Backwards-compat fix: Remove excluded parameters from // the given data, if they exist - excludedParams.forEach( function ( name ) { - delete normalizedData.params[ name ]; - } ); + normalizedData.params = model.filtersModel.removeExcludedParams( normalizedData.params ); id = String( id ); - model.addNewQuery( obj.label, normalizedData, isDefault, id ); + + // Skip the addNewQuery method because we don't want to unnecessarily manipulate + // the given saved queries unless we literally intend to (like in backwards compat fixes) + // And the addNewQuery method also uses a minimization routine that checks for the + // validity of items and minimizes the query. This isn't necessary for queries loaded + // from the backend, and has the risk of removing values if they're temporarily + // invalid (example: if we temporarily removed a cssClass from a filter in the backend) + model.addItems( [ + new mw.rcfilters.dm.SavedQueryItemModel( + id, + obj.label, + normalizedData, + { 'default': isDefault } + ) + ] ); if ( isDefault ) { model.default = id; @@ -153,12 +164,16 @@ delete data.highlights.highlight; // Filters - newData.params = this.filtersModel.getParametersFromFilters( fullFilterRepresentation ); + newData.params = this.filtersModel.getMinimizedParamRepresentation( + this.filtersModel.getParametersFromFilters( fullFilterRepresentation ) + ); // Highlights (taking out 'highlight' itself, appending _color to keys) newData.highlights = {}; - Object.keys( data.highlights ).forEach( function ( highlightedFilterName ) { - newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ]; + $.each( data.highlights, function ( highlightedFilterName, value ) { + if ( value ) { + newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ]; + } } ); // Add highlight @@ -167,99 +182,30 @@ return newData; }; - /** - * 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 params, 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 params - * are stored); When we initialize the system, we merge each minimal - * query with the base state (using 'getMinimalParamList') 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 #getMinimalParamList does - * - * @return {Object} Base parameter state - */ - mw.rcfilters.dm.SavedQueriesModel.prototype.getBaseParamState = function () { - var allParams, - highlightedItems = {}; - - if ( !this.baseParamState ) { - allParams = this.filtersModel.getParametersFromFilters( {} ); - - // Prepare highlights - this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { - highlightedItems[ item.getName() + '_color' ] = null; - } ); - - this.baseParamState = { - params: $.extend( true, { highlight: '0' }, allParams ), - highlights: highlightedItems - }; - } - - return this.baseParamState; - }; - - /** - * Get an object that holds only the parameters and highlights that have - * values different than the base 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.dm.SavedQueriesModel.prototype.getMinimalParamList = function ( valuesObject ) { - var result = { params: {}, highlights: {} }, - baseState = this.getBaseParamState(); - - // XOR results - $.each( valuesObject.params, function ( name, value ) { - if ( baseState.params !== undefined && baseState.params[ name ] !== value ) { - result.params[ name ] = value; - } - } ); - - $.each( valuesObject.highlights, function ( name, value ) { - if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) { - result.highlights[ name ] = value; - } - } ); - - return result; - }; - /** * Add a query item * * @param {string} label Label for the new query - * @param {Object} data Data for the new query + * @param {Object} fulldata Full data representation for the new query, combining highlights and filters * @param {boolean} isDefault Item is default * @param {string} [id] Query ID, if exists. If this isn't given, a random * new ID will be created. * @return {string} ID of the newly added query */ - mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) { - var randomID = String( id || ( new Date() ).getTime() ), - normalizedData = this.getMinimalParamList( data ); + mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) { + var normalizedData = { params: {}, highlights: {} }, + highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ), + randomID = String( id || ( new Date() ).getTime() ), + data = this.filtersModel.getMinimizedParamRepresentation( fulldata ); + + // Split highlight/params + $.each( data, function ( param, value ) { + if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) { + normalizedData.highlights[ param ] = value; + } else { + normalizedData.params[ param ] = value; + } + } ); // Add item this.addItems( [ @@ -305,11 +251,11 @@ */ mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) { // Minimize before comparison - fullQueryComparison = this.getMinimalParamList( fullQueryComparison ); + fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison ); return this.getItems().filter( function ( item ) { return OO.compare( - item.getData(), + item.getCombinedData(), fullQueryComparison ); } )[ 0 ]; @@ -329,16 +275,64 @@ }; /** - * Get an item's full data + * Get the full data representation of the default query, if it exists * - * @param {string} queryID Query identifier - * @return {Object} Item's full data + * @param {boolean} [excludeHiddenParams] Exclude hidden parameters in the result + * @return {Object|null} Representation of the default params if exists. + * Null if default doesn't exist or if the user is not logged in. */ - mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) { - var item = this.getItemByID( queryID ); + mw.rcfilters.dm.SavedQueriesModel.prototype.getDefaultParams = function ( excludeHiddenParams ) { + var data = ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {}; + + if ( excludeHiddenParams ) { + Object.keys( this.filtersModel.getDefaultHiddenParams() ).forEach( function ( paramName ) { + delete data[ paramName ]; + } ); + } - // Fill in the base params - return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {}; + return data; + }; + + /** + * Get a full parameter representation of an item data + * + * @param {Object} queryID Query ID + * @return {Object} Parameter representation + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.getItemParams = function ( queryID ) { + var item = this.getItemByID( queryID ), + data = item ? item.getData() : {}; + + return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {}; + }; + + /** + * Build a full parameter representation given item data and model sticky values state + * + * @param {Object} data Item data + * @return {Object} Full param representation + */ + mw.rcfilters.dm.SavedQueriesModel.prototype.buildParamsFromData = function ( data ) { + // Merge saved filter state with sticky filter values + var savedFilters; + + data = data || {}; + + // In order to merge sticky filters with the data, we have to + // transform this to filters first, merge, and then back to + // parameters + savedFilters = $.extend( + true, {}, + this.filtersModel.getFiltersFromParameters( data.params ), + this.filtersModel.getStickyFiltersState() + ); + + // Return parameter representation + return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {}, + this.filtersModel.getParametersFromFilters( savedFilters ), + data.highlights, + { highlight: data.params.highlight } + ) ); }; /** @@ -347,16 +341,11 @@ * @return {Object} Object representing the state of the model and items */ mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () { - var model = this, - obj = { queries: {}, version: '2' }; + var obj = { queries: {}, version: '2' }; // Translate the items to the saved object this.getItems().forEach( function ( item ) { - var itemState = item.getState(); - - itemState.data = model.getMinimalParamList( itemState.data ); - - obj.queries[ item.getID() ] = itemState; + obj.queries[ item.getID() ] = item.getState(); } ); if ( this.getDefault() ) { diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js index 81c8306cdb..a6ff9a1c25 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.SavedQueryItemModel.js @@ -91,6 +91,15 @@ return this.data; }; + /** + * Get the combined data of this item as a flat object of parameters + * + * @return {Object} Combined parameter data + */ + mw.rcfilters.dm.SavedQueryItemModel.prototype.getCombinedData = function () { + return $.extend( true, {}, this.data.params, this.data.highlights ); + }; + /** * Check whether this item is the default * diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 5b12cf7b31..0b2dd8d381 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -369,7 +369,7 @@ * Reset to default filters */ mw.rcfilters.Controller.prototype.resetToDefaults = function () { - this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() ); + this.filtersModel.updateStateFromParams( this._getDefaultParams() ); this.updateChangesList(); }; @@ -380,23 +380,7 @@ * @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.match( /_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 ]; - } ); + return $.isEmptyObject( this._getDefaultParams( true ) ); }; /** @@ -407,10 +391,7 @@ .getHighlightedItems() .map( function ( filterItem ) { return { name: filterItem.getName() }; } ); - this.filtersModel.emptyAllFilters(); - this.filtersModel.clearAllHighlightColors(); - // Check all filter interactions - this.filtersModel.reassessFilterInteractions(); + this.filtersModel.updateStateFromParams( {} ); this.updateChangesList(); @@ -482,7 +463,7 @@ */ mw.rcfilters.Controller.prototype.toggleHighlight = function () { this.filtersModel.toggleHighlight(); - this._updateURL(); + this.uriProcessor.updateURL(); if ( this.filtersModel.isHighlightEnabled() ) { mw.hook( 'RcFilters.highlight.enable' ).fire(); @@ -513,7 +494,7 @@ */ mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) { this.filtersModel.setHighlightColor( filterName, color ); - this._updateURL(); + this.uriProcessor.updateURL(); this._trackHighlight( 'set', { name: filterName, color: color } ); }; @@ -524,7 +505,7 @@ */ mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) { this.filtersModel.clearHighlightColor( filterName ); - this._updateURL(); + this.uriProcessor.updateURL(); this._trackHighlight( 'clear', filterName ); }; @@ -628,32 +609,10 @@ * @param {boolean} [setAsDefault=false] This query should be set as the default */ mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) { - var highlightedItems = {}, - highlightEnabled = this.filtersModel.isHighlightEnabled(), - selectedState = this.filtersModel.getSelectedState(); - - // Prepare highlights - this.filtersModel.getHighlightedItems().forEach( function ( item ) { - highlightedItems[ item.getName() + '_color' ] = highlightEnabled ? - item.getHighlightColor() : null; - } ); - - // Delete all excluded filters - this._deleteExcludedValuesFromFilterState( selectedState ); - // Add item this.savedQueriesModel.addNewQuery( label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ), - { - params: $.extend( - true, - { - highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ) - }, - this.filtersModel.getParametersFromFilters( selectedState ) - ), - highlights: highlightedItems - }, + this.filtersModel.getCurrentParameterState( true ), setAsDefault ); @@ -704,52 +663,27 @@ * @param {string} queryID Query id */ mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) { - var highlights, - queryItem = this.savedQueriesModel.getItemByID( queryID ), - data = this.savedQueriesModel.getItemFullData( queryID ), - currentMatchingQuery = this.findQueryMatchingCurrentState(); + var currentMatchingQuery, + params = this.savedQueriesModel.getItemParams( queryID ); + + currentMatchingQuery = this.findQueryMatchingCurrentState(); 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() - ) + currentMatchingQuery && + currentMatchingQuery.getID() === queryID ) { - highlights = data.highlights; - - // Update model state from filters - this.filtersModel.toggleFiltersSelected( - // Merge filters with excluded values - $.extend( - true, - {}, - this.filtersModel.getFiltersFromParameters( data.params ), - this.filtersModel.getExcludedFiltersState() - ) - ); - - // Update highlight state - this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) ); - this.filtersModel.getItems().forEach( function ( filterItem ) { - var color = highlights[ filterItem.getName() + '_color' ]; - if ( color ) { - filterItem.setHighlightColor( color ); - } else { - filterItem.clearHighlightColor(); - } - } ); + // If the query we want to load is the one that is already + // loaded, don't reload it + return; + } - // Check all filter interactions - this.filtersModel.reassessFilterInteractions(); + // Apply parameters to model + this.filtersModel.updateStateFromParams( params ); - this.updateChangesList(); + this.updateChangesList(); - // Log filter grouping - this.trackFilterGroupings( 'savedfilters' ); - } + // Log filter grouping + this.trackFilterGroupings( 'savedfilters' ); }; /** @@ -759,44 +693,11 @@ * @return {boolean} Query exists */ mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () { - var highlightedItems = {}, - selectedState = this.filtersModel.getSelectedState(); - - // Prepare highlights of the current query - this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) { - highlightedItems[ item.getName() + '_color' ] = item.getHighlightColor(); - } ); - - // 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( - { - params: $.extend( - true, - { - highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ) - }, - this.filtersModel.getParametersFromFilters( selectedState ) - ), - highlights: highlightedItems - } + this.filtersModel.getCurrentParameterState( true ) ); }; - /** - * 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 ]; - } ); - }; - /** * Save the current state of the saved queries model with all * query item representation in the user settings. @@ -924,7 +825,7 @@ * without adding an history entry. */ mw.rcfilters.Controller.prototype.replaceUrl = function () { - mw.rcfilters.UriProcessor.static.replaceState( this._getUpdatedUri() ); + this.uriProcessor.replaceUpdatedUri(); }; /** @@ -960,7 +861,7 @@ updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode; if ( updateMode === this.FILTER_CHANGE ) { - this._updateURL( params ); + this.uriProcessor.updateURL( params ); } if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) { this.changesListModel.invalidate(); @@ -992,81 +893,17 @@ * 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 () { - 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() - ); - - // Return parameter representation - return $.extend( true, {}, - this.filtersModel.getParametersFromFilters( savedFilters ), - data.highlights, - { highlight: data.params.highlight } - ); - } - return this.filtersModel.getDefaultParams(); - }; - - /** - * Update the URL of the page to reflect current filters - * - * This should not be called directly from outside the controller. - * If an action requires changing the URL, it should either use the - * highlighting actions below, or call #updateChangesList which does - * the uri corrections already. - * - * @param {Object} [params] Extra parameters to add to the API call - */ - mw.rcfilters.Controller.prototype._updateURL = function ( params ) { - var currentUri = new mw.Uri(), - updatedUri = this._getUpdatedUri(); - - updatedUri.extend( params || {} ); - - if ( - this.uriProcessor.getVersion( currentUri.query ) !== 2 || - this.uriProcessor.isNewState( currentUri.query, updatedUri.query ) - ) { - mw.rcfilters.UriProcessor.static.replaceState( updatedUri ); + mw.rcfilters.Controller.prototype._getDefaultParams = function ( excludeHiddenParams ) { + if ( this.savedQueriesModel.getDefault() ) { + return this.savedQueriesModel.getDefaultParams( excludeHiddenParams ); + } else { + return this.filtersModel.getDefaultParams( excludeHiddenParams ); } }; - /** - * Get an updated mw.Uri object based on the model state - * - * @return {mw.Uri} Updated Uri - */ - mw.rcfilters.Controller.prototype._getUpdatedUri = function () { - var uri = new mw.Uri(); - - // Minimize url - uri.query = this.uriProcessor.minimizeQuery( - $.extend( - true, - {}, - // We want to retain unrecognized params - // The uri params from model will override - // any recognized value in the current uri - // query, retain unrecognized params, and - // the result will then be minimized - uri.query, - this.uriProcessor.getUriParametersFromModel(), - { urlversion: '2' } - ) - ); - - return uri; - }; - /** * Query the list of changes from the server for the current filters * @@ -1077,8 +914,8 @@ * @return {jQuery.Promise} Promise object resolved with { content, status } */ mw.rcfilters.Controller.prototype._queryChangesList = function ( counterId, params ) { - var uri = this._getUpdatedUri(), - stickyParams = this.filtersModel.getStickyParams(), + var uri = this.uriProcessor.getUpdatedUri(), + stickyParams = this.filtersModel.getStickyParamsValues(), requestId, latestRequest; diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js index 0450639812..044712c6bf 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js @@ -6,11 +6,7 @@ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters view model */ mw.rcfilters.UriProcessor = function MwRcfiltersController( filtersModel ) { - this.emptyParameterState = {}; this.filtersModel = filtersModel; - - // Initialize - this._buildEmptyParameterState(); }; /* Initialization */ @@ -59,104 +55,110 @@ }; /** - * Update the filters model based on the URI query - * This happens on initialization, and from this moment on, - * we consider the system synchronized, and the model serves - * as the source of truth for the URL. - * - * This methods should only be called once on initialiation. - * After initialization, the model updates the URL, not the - * other way around. - * - * @param {Object} [uriQuery] URI query + * Replace the current URI with an updated one from the model state */ - mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) { - var parameters; - - uriQuery = uriQuery || new mw.Uri().query; - - // For arbitrary numeric single_option values, check the uri and see if it's beyond the limit - $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) { - if ( - groupModel.getType() === 'single_option' && - groupModel.isAllowArbitrary() - ) { - if ( - groupModel.getMaxValue() !== null && - uriQuery[ groupName ] > groupModel.getMaxValue() - ) { - // Change the value to the actual max value - uriQuery[ groupName ] = String( groupModel.getMaxValue() ); - } else if ( - groupModel.getMinValue() !== null && - uriQuery[ groupName ] < groupModel.getMinValue() - ) { - // Change the value to the actual min value - uriQuery[ groupName ] = String( groupModel.getMinValue() ); - } - } - } ); - - // Normalize - parameters = this._getNormalizedQueryParams( uriQuery ); + mw.rcfilters.UriProcessor.prototype.replaceUpdatedUri = function () { + this.constructor.static.replaceState( this.getUpdatedUri() ); + }; - // Update filter states - this.filtersModel.toggleFiltersSelected( - this.filtersModel.getFiltersFromParameters( - parameters + /** + * Get an updated mw.Uri object based on the model state + * + * @param {Object} [uriQuery] An external URI query to build the new uri + * with. This is mainly for tests, to be able to supply external parameters + * and make sure they are retained. + * @return {mw.Uri} Updated Uri + */ + mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uriQuery ) { + var uri = new mw.Uri(), + unrecognizedParams = this.getUnrecognizedParams( uriQuery || uri.query ); + + if ( uriQuery ) { + // This is mainly for tests, to be able to give the method + // an initial URI Query and test that it retains parameters + uri.query = uriQuery; + } + + uri.query = this.filtersModel.getMinimizedParamRepresentation( + $.extend( + true, + {}, + uri.query, + // The representation must be expanded so it can + // override the uri query params but we then output + // a minimized version for the entire URI representation + // for the method + this.filtersModel.getExpandedParamRepresentation() ) ); - // Update highlight state - this.filtersModel.getItems().forEach( function ( filterItem ) { - var color = parameters[ filterItem.getName() + '_color' ]; - if ( color ) { - filterItem.setHighlightColor( color ); - } else { - filterItem.clearHighlightColor(); - } - } ); - this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) ); + // Reapply unrecognized params and url version + uri.query = $.extend( true, {}, uri.query, unrecognizedParams, { urlversion: '2' } ); - // Check all filter interactions - this.filtersModel.reassessFilterInteractions(); + return uri; }; /** - * Get parameters representing the current state of the model + * Get an object representing given parameters that are unrecognized by the model * - * @return {Object} Uri query parameters + * @param {Object} params Full params object + * @return {Object} Unrecognized params */ - mw.rcfilters.UriProcessor.prototype.getUriParametersFromModel = function () { - return $.extend( - true, - {}, - this.filtersModel.getParametersFromFilters(), - this.filtersModel.getHighlightParameters(), - { - highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ) + mw.rcfilters.UriProcessor.prototype.getUnrecognizedParams = function ( params ) { + // Start with full representation + var givenParamNames = Object.keys( params ), + unrecognizedParams = $.extend( true, {}, params ); + + // Extract unrecognized parameters + Object.keys( this.filtersModel.getEmptyParameterState() ).forEach( function ( paramName ) { + // Remove recognized params + if ( givenParamNames.indexOf( paramName ) > -1 ) { + delete unrecognizedParams[ paramName ]; } - ); + } ); + + return unrecognizedParams; }; /** - * Build the full parameter representation based on given query parameters + * Update the URL of the page to reflect current filters * - * @private - * @param {Object} uriQuery Given URI query - * @return {Object} Full parameter state representing the URI query + * This should not be called directly from outside the controller. + * If an action requires changing the URL, it should either use the + * highlighting actions below, or call #updateChangesList which does + * the uri corrections already. + * + * @param {Object} [params] Extra parameters to add to the API call */ - mw.rcfilters.UriProcessor.prototype._expandModelParameters = function ( uriQuery ) { - var filterRepresentation = this.filtersModel.getFiltersFromParameters( uriQuery ); + mw.rcfilters.UriProcessor.prototype.updateURL = function ( params ) { + var currentUri = new mw.Uri(), + updatedUri = this.getUpdatedUri(); + + updatedUri.extend( params || {} ); + + if ( + this.getVersion( currentUri.query ) !== 2 || + this.isNewState( currentUri.query, updatedUri.query ) + ) { + this.constructor.static.replaceState( updatedUri ); + } + }; - return $.extend( true, - {}, - uriQuery, - this.filtersModel.getParametersFromFilters( filterRepresentation ), - this.filtersModel.extractHighlightValues( uriQuery ), - { - highlight: String( Number( uriQuery.highlight ) ) - } + /** + * Update the filters model based on the URI query + * This happens on initialization, and from this moment on, + * we consider the system synchronized, and the model serves + * as the source of truth for the URL. + * + * This methods should only be called once on initialiation. + * After initialization, the model updates the URL, not the + * other way around. + * + * @param {Object} [uriQuery] URI query + */ + mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function ( uriQuery ) { + this.filtersModel.updateStateFromParams( + this._getNormalizedQueryParams( uriQuery || new mw.Uri().query ) ); }; @@ -181,8 +183,18 @@ // This will allow us to always have a proper check of whether // the requested new url is one to change or not, regardless of // actual parameter visibility/representation in the URL - currentParamState = this._expandModelParameters( currentUriQuery ); - updatedParamState = this._expandModelParameters( updatedUriQuery ); + currentParamState = $.extend( + true, + {}, + this.filtersModel.getMinimizedParamRepresentation( currentUriQuery ), + this.getUnrecognizedParams( currentUriQuery ) + ); + updatedParamState = $.extend( + true, + {}, + this.filtersModel.getMinimizedParamRepresentation( updatedUriQuery ), + this.getUnrecognizedParams( updatedUriQuery ) + ); return notEquivalent( currentParamState, updatedParamState ); }; @@ -196,7 +208,7 @@ */ mw.rcfilters.UriProcessor.prototype.doesQueryContainRecognizedParams = function ( uriQuery ) { var anyValidInUrl, - validParameterNames = Object.keys( this._getEmptyParameterState() ) + validParameterNames = Object.keys( this.filtersModel.getEmptyParameterState() ) .filter( function ( param ) { // Remove 'highlight' parameter from this check; // if it's the only parameter in the URL we still @@ -214,35 +226,11 @@ return anyValidInUrl || this.getVersion( uriQuery ) === 2; }; - /** - * Remove all parameters that have the same value as the base state - * This method expects uri queries of the urlversion=2 format - * - * @private - * @param {Object} uriQuery Current uri query - * @return {Object} Minimized query - */ - mw.rcfilters.UriProcessor.prototype.minimizeQuery = function ( uriQuery ) { - var baseParams = this._getEmptyParameterState(), - uriResult = $.extend( true, {}, uriQuery ); - - $.each( uriResult, function ( paramName, paramValue ) { - if ( - baseParams[ paramName ] !== undefined && - baseParams[ paramName ] === paramValue - ) { - // Remove parameter from query - delete uriResult[ paramName ]; - } - } ); - - return uriResult; - }; - /** * Get the adjusted URI params based on the url version * If the urlversion is not 2, the parameters are merged with * the model's defaults. + * Always merge in the hidden parameter defaults. * * @private * @param {Object} uriQuery Current URI query @@ -257,53 +245,18 @@ // wiki default. // Any subsequent change of the URL through the RCFilters // system will receive 'urlversion=2' - var hiddenParamDefaults = {}, + var hiddenParamDefaults = this.filtersModel.getDefaultHiddenParams(), base = this.getVersion( uriQuery ) === 2 ? {} : this.filtersModel.getDefaultParams(); - // Go over the model and get all hidden parameters' defaults - // These defaults should be applied regardless of the urlversion - // but be overridden by the URL params if they exist - $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) { - if ( groupModel.isHidden() ) { - $.extend( true, hiddenParamDefaults, groupModel.getDefaultParams() ); - } - } ); - - return this.minimizeQuery( - $.extend( true, {}, hiddenParamDefaults, base, uriQuery, { urlversion: '2' } ) - ); - }; - - /** - * Get the representation of an empty parameter state - * - * @private - * @return {Object} Empty parameter state - */ - mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () { - // Override empty parameter state with the sticky parameter values - return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() ); - }; - - /** - * Build an empty representation of the parameters, where all parameters - * are either set to '0' or '' depending on their type. - * This must run during initialization, before highlights are set. - * - * @private - */ - mw.rcfilters.UriProcessor.prototype._buildEmptyParameterState = function () { - var emptyParams = this.filtersModel.getParametersFromFilters( {} ), - emptyHighlights = this.filtersModel.getEmptyHighlightParameters(); - - this.emptyParameterState = $.extend( + return $.extend( true, {}, - emptyParams, - emptyHighlights, - { highlight: '0' } + this.filtersModel.getMinimizedParamRepresentation( + $.extend( true, {}, hiddenParamDefaults, base, uriQuery ) + ), + { urlversion: '2' } ); }; }( mediaWiki, jQuery ) ); diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js index 38ade4ddcd..291d5c747c 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js @@ -6,24 +6,24 @@ title: 'Group 1', type: 'send_unselected_if_any', filters: [ - { name: 'filter1', default: true }, - { name: 'filter2' } + { name: 'filter1', cssClass: 'filter1class', default: true }, + { name: 'filter2', cssClass: 'filter2class' } ] }, { name: 'group2', title: 'Group 2', type: 'send_unselected_if_any', filters: [ - { name: 'filter3' }, - { name: 'filter4', default: true } + { name: 'filter3', cssClass: 'filter3class' }, + { name: 'filter4', cssClass: 'filter4class', default: true } ] }, { name: 'group3', title: 'Group 3', type: 'string_options', filters: [ - { name: 'filter5' }, - { name: 'filter6' } + { name: 'filter5', cssClass: 'filter5class' }, + { name: 'filter6' } // Not supporting highlights ] } ], minimalDefaultParams = { @@ -49,55 +49,79 @@ ); } ); - QUnit.test( 'updateModelBasedOnQuery & getUriParametersFromModel', function ( assert ) { + QUnit.test( 'getUpdatedUri', function ( assert ) { var uriProcessor, - filtersModel = new mw.rcfilters.dm.FiltersViewModel(), - baseParams = { - filter1: '0', - filter2: '0', - filter3: '0', - filter4: '0', - group3: '', - highlight: '0', - group1__filter1_color: null, - group1__filter2_color: null, - group2__filter3_color: null, - group2__filter4_color: null, - group3__filter5_color: null, - group3__filter6_color: null - }; + filtersModel = new mw.rcfilters.dm.FiltersViewModel(); + + filtersModel.initializeFilters( mockFilterStructure ); + uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( {} ) ).query, + { urlversion: '2' }, + 'Empty model state with empty uri state, assumes the given uri is already normalized, and adds urlversion=2' + ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query, + { urlversion: '2', foo: 'bar' }, + 'Empty model state with unrecognized params retains unrecognized params' + ); + + // Update the model + filtersModel.toggleFiltersSelected( { + group1__filter1: true, // Param: filter2: '1' + group3__filter5: true // Param: group3: 'filter5' + } ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( {} ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5' }, + 'Model state is reflected in the updated URI' + ); + + assert.deepEqual( + ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query, + { urlversion: '2', filter2: '1', group3: 'filter5', foo: 'bar' }, + 'Model state is reflected in the updated URI with existing uri params' + ); + } ); + + QUnit.test( 'updateModelBasedOnQuery', function ( assert ) { + var uriProcessor, + filtersModel = new mw.rcfilters.dm.FiltersViewModel(); filtersModel.initializeFilters( mockFilterStructure ); uriProcessor = new mw.rcfilters.UriProcessor( filtersModel ); uriProcessor.updateModelBasedOnQuery( {} ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, minimalDefaultParams ), + filtersModel.getCurrentParameterState(), + minimalDefaultParams, 'Version 1: Empty url query sets model to defaults' ); uriProcessor.updateModelBasedOnQuery( { urlversion: '2' } ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - baseParams, + filtersModel.getCurrentParameterState(), + {}, 'Version 2: Empty url query sets model to all-false' ); uriProcessor.updateModelBasedOnQuery( { filter1: '1', urlversion: '2' } ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, { filter1: '1' } ), + filtersModel.getCurrentParameterState(), + $.extend( true, {}, { filter1: '1' } ), 'Parameters in Uri query set parameter value in the model' ); uriProcessor.updateModelBasedOnQuery( { highlight: '1', group1__filter1_color: 'c1', urlversion: '2' } ); assert.deepEqual( - uriProcessor.getUriParametersFromModel(), - $.extend( true, {}, baseParams, { + filtersModel.getCurrentParameterState(), + { highlight: '1', group1__filter1_color: 'c1' - } ), + }, 'Highlight parameters in Uri query set highlight state in the model' ); } ); diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index 4eec02a9af..dde49ba00b 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -7,6 +7,7 @@ { name: 'filter1', label: 'group1filter1-label', description: 'group1filter1-desc', default: true, + cssClass: 'filter1class', conflicts: [ { group: 'group2' } ], subset: [ { @@ -22,6 +23,7 @@ { name: 'filter2', label: 'group1filter2-label', description: 'group1filter2-desc', conflicts: [ { group: 'group2', filter: 'filter6' } ], + cssClass: 'filter2class', subset: [ { group: 'group1', @@ -29,18 +31,20 @@ } ] }, + // NOTE: This filter has no highlight! { name: 'filter3', label: 'group1filter3-label', description: 'group1filter3-desc', default: true } ] }, { name: 'group2', type: 'send_unselected_if_any', fullCoverage: true, + excludedFromSavedQueries: true, conflicts: [ { group: 'group1', filter: 'filter1' } ], filters: [ - { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc' }, - { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true }, + { name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc', cssClass: 'filter4class' }, + { name: 'filter5', label: 'group2filter5-label', description: 'group2filter5-desc', default: true, cssClass: 'filter5class' }, { - name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', + name: 'filter6', label: 'group2filter6-label', description: 'group2filter6-desc', cssClass: 'filter6class', conflicts: [ { group: 'group1', filter: 'filter2' } ] } ] @@ -50,15 +54,16 @@ separator: ',', default: 'filter8', filters: [ - { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc' }, - { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc' }, - { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc' } + { name: 'filter7', label: 'group3filter7-label', description: 'group3filter7-desc', cssClass: 'filter7class' }, + { name: 'filter8', label: 'group3filter8-label', description: 'group3filter8-desc', cssClass: 'filter8class' }, + { name: 'filter9', label: 'group3filter9-label', description: 'group3filter9-desc', cssClass: 'filter9class' } ] }, { name: 'group4', type: 'single_option', default: 'option2', filters: [ + // NOTE: The entire group has no highlight supported { name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' }, { name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' }, { name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' } @@ -67,18 +72,18 @@ name: 'group5', type: 'single_option', filters: [ - { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc' }, - { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc' }, - { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc' } + { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc', cssClass: 'group5opt1class' }, + { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc', cssClass: 'group5opt2class' }, + { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc', cssClass: 'group5opt3class' } ] }, { name: 'group6', type: 'boolean', isSticky: true, filters: [ - { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc' }, - { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true }, - { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true } + { name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc', cssClass: 'group6opt1class' }, + { name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true, cssClass: 'group6opt2class' }, + { name: 'group6option3', label: 'group6option3-label', description: 'group6option3-desc', default: true, cssClass: 'group6opt3class' } ] }, { name: 'group7', @@ -86,9 +91,9 @@ isSticky: true, default: 'group7option2', filters: [ - { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc' }, - { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc' }, - { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc' } + { name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc', cssClass: 'group7opt1class' }, + { name: 'group7option2', label: 'group7option2-label', description: 'group7option2-desc', cssClass: 'group7opt2class' }, + { name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc', cssClass: 'group7opt3class' } ] } ], viewsDefinition = { @@ -101,10 +106,10 @@ type: 'string_options', separator: ';', filters: [ - { name: 0, label: 'Main' }, - { name: 1, label: 'Talk' }, - { name: 2, label: 'User' }, - { name: 3, label: 'User talk' } + { name: 0, label: 'Main', cssClass: 'namespace-0' }, + { name: 1, label: 'Talk', cssClass: 'namespace-1' }, + { name: 2, label: 'User', cssClass: 'namespace-2' }, + { name: 3, label: 'User talk', cssClass: 'namespace-3' } ] } ] } @@ -141,6 +146,49 @@ group7: 'group7option2', namespace: '' }, + emptyParamRepresentation = { + filter1: '0', + filter2: '0', + filter3: '0', + filter4: '0', + filter5: '0', + filter6: '0', + group3: '', + group4: '', + group5: '', + group6option1: '0', + group6option2: '0', + group6option3: '0', + group7: '', + namespace: '', + highlight: '0', + // Null highlights + group1__filter1_color: null, + group1__filter2_color: null, + // group1__filter3_color: null, // Highlight isn't supported + group2__filter4_color: null, + group2__filter5_color: null, + group2__filter6_color: null, + group3__filter7_color: null, + group3__filter8_color: null, + group3__filter9_color: null, + // group4__option1_color: null, // Highlight isn't supported + // group4__option2_color: null, // Highlight isn't supported + // group4__option3_color: null, // Highlight isn't supported + group5__option1_color: null, + group5__option2_color: null, + group5__option3_color: null, + group6__group6option1_color: null, + group6__group6option2_color: null, + group6__group6option3_color: null, + group7__group7option1_color: null, + group7__group7option2_color: null, + group7__group7option3_color: null, + namespace__0_color: null, + namespace__1_color: null, + namespace__2_color: null, + namespace__3_color: null + }, baseFilterRepresentation = { group1__filter1: false, group1__filter2: false, @@ -278,6 +326,165 @@ ); } ); + QUnit.test( 'Parameter minimal state', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', + filter2: '0', + filter3: '0', + group3: '', + group4: 'option2' + }, + result: { + filter1: '1', + group4: 'option2' + }, + msg: 'Mixed input results in only non-falsey values as result' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: null + }, + result: {}, + msg: 'An all-falsey input results in an empty result.' + }, + { + input: { + filter1: '0', + filter2: '0', + filter3: '0', + group3: '', + group4: '', + group1__filter1_color: 'c1' + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'An all-falsey input with highlight params result in only the highlight param.' + }, + { + input: { + group1__filter1_color: 'c1', + group1__filter3_color: 'c3' // Not supporting highlights + }, + result: { + group1__filter1_color: 'c1' + }, + msg: 'Unsupported highlights are removed.' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.getMinimizedParamRepresentation( test.input ), + test.result, + test.msg + ); + } ); + } ); + + QUnit.test( 'Parameter states', function ( assert ) { + // Some groups / params have their defaults immediately applied + // to their state. These include single_option which can never + // be empty, etc. These are these states: + var parametersWithoutExcluded, + appliedDefaultParameters = { + group4: 'option2', + group5: 'option1', + // Sticky, their defaults apply immediately + group6option2: '1', + group6option3: '1', + group7: 'group7option2' + }, + model = new mw.rcfilters.dm.FiltersViewModel(); + + model.initializeFilters( filterDefinition, viewsDefinition ); + assert.deepEqual( + model.getEmptyParameterState(), + emptyParamRepresentation, + 'Producing an empty parameter state' + ); + + model.toggleFiltersSelected( { + group1__filter1: true, + group3__filter7: true + } ); + + assert.deepEqual( + model.getCurrentParameterState(), + // appliedDefaultParams applies the default value to parameters + // who must have an initial value to begin with, so we have to + // take it into account in the current state + $.extend( true, {}, appliedDefaultParameters, { + filter2: '1', + filter3: '1', + group3: 'filter7' + } ), + 'Producing a current parameter state' + ); + + // Reset + model = new mw.rcfilters.dm.FiltersViewModel(); + model.initializeFilters( filterDefinition, viewsDefinition ); + + parametersWithoutExcluded = $.extend( true, {}, appliedDefaultParameters ); + delete parametersWithoutExcluded.group7; + delete parametersWithoutExcluded.group6option2; + delete parametersWithoutExcluded.group6option3; + + assert.deepEqual( + model.getCurrentParameterState( true ), + parametersWithoutExcluded, + 'Producing a current clean parameter state without excluded filters' + ); + } ); + + QUnit.test( 'Cleaning up parameter states', function ( assert ) { + var model = new mw.rcfilters.dm.FiltersViewModel(), + cases = [ + { + input: {}, + result: {}, + msg: 'Empty parameter representation produces an empty result' + }, + { + input: { + filter1: '1', // Regular (do not strip) + group6option1: '1', // Sticky + filter4: '1', // Excluded + filter5: '0' // Excluded + }, + result: { filter1: '1' }, + msg: 'Valid input strips all sticky and excluded params regardless of value' + } + ]; + + model.initializeFilters( filterDefinition, viewsDefinition ); + + cases.forEach( function ( test ) { + assert.deepEqual( + model.removeExcludedParams( test.input ), + test.result, + test.msg + ); + } ); + + } ); + QUnit.test( 'Finding matching filters', function ( assert ) { var matches, testCases = [ diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js index 6a0592069f..539bab4f19 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js @@ -6,28 +6,38 @@ filters: [ // Note: The fact filter2 is default means that in the // filter representation, filter1 and filter3 are 'true' - { name: 'filter1' }, - { name: 'filter2', default: true }, - { name: 'filter3' } + { name: 'filter1', cssClass: 'filter1class' }, + { name: 'filter2', cssClass: 'filter2class', default: true }, + { name: 'filter3', cssClass: 'filter3class' } ] }, { name: 'group2', type: 'string_options', separator: ',', filters: [ - { name: 'filter4' }, - { name: 'filter5' }, - { name: 'filter6' } + { name: 'filter4', cssClass: 'filter4class' }, + { name: 'filter5' }, // NOTE: Not supporting highlights! + { name: 'filter6', cssClass: 'filter6class' } ] }, { name: 'group3', type: 'boolean', isSticky: true, filters: [ - { name: 'group3option1' }, - { name: 'group3option2' }, - { name: 'group3option3' } + { name: 'group3option1', cssClass: 'filter1class' }, + { name: 'group3option2', cssClass: 'filter1class' }, + { name: 'group3option3', cssClass: 'filter1class' } ] + }, { + // Copy of the way the controller defines invert + // to check whether the conversion works + name: 'invertGroup', + type: 'boolean', + hidden: true, + filters: [ { + name: 'invert', + default: '0' + } ] } ], queriesFilterRepresentation = { queries: { @@ -54,9 +64,10 @@ }, highlights: { highlight: true, - filter1: 'c5', - group3option1: 'c1' - } + group1__filter1: 'c5', + group3__group3option1: 'c1' + }, + invert: true } } } @@ -78,8 +89,8 @@ highlight: '1' }, highlights: { - filter1_color: 'c5', - group3option1_color: 'c1' + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' } } } @@ -102,7 +113,7 @@ filter2: '1' }, highlights: { - filter5_color: 'c2' + group1__filter3_color: 'c2' } } } @@ -125,6 +136,22 @@ finalState: $.extend( true, {}, queriesParamRepresentation ), msg: 'Conversion from filter representation to parameters retains data.' }, + { + // Converting from old structure + input: $.extend( true, {}, queriesFilterRepresentation, { queries: { 1234: { data: { + filters: { + // Entire group true: normalize params + filter1: true, + filter2: true, + filter3: true + }, + highlights: { + filter3: null // Get rid of empty highlight + } + } } } } ), + finalState: $.extend( true, {}, queriesParamRepresentation ), + msg: 'Conversion from filter representation to parameters normalizes params and highlights.' + }, { // Converting from old structure with default input: $.extend( true, { default: '1234' }, queriesFilterRepresentation ), @@ -136,6 +163,13 @@ input: $.extend( true, {}, queriesParamRepresentation ), finalState: $.extend( true, {}, queriesParamRepresentation ), msg: 'Parameter representation retains its queries structure' + }, + { + // Do not touch invalid color parameters from the initialization routine + // (Normalization, or "fixing" the query should only happen when we add new query or actively convert queries) + input: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + finalState: $.extend( true, { queries: { 1234: { data: { highlights: { group2__filter5_color: 'c2' } } } } }, exampleQueryStructure ), + msg: 'Structure that contains invalid highlights remains the same in initialization' } ]; @@ -151,6 +185,103 @@ } ); } ); + QUnit.test( 'Adding new queries', function ( assert ) { + var filtersModel = new mw.rcfilters.dm.FiltersViewModel(), + queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ), + cases = [ + { + methodParams: [ + 'label1', // Label + { // Data + filter1: '1', + filter2: '2', + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + }, + true, // isDefault + '1234' // ID + ], + result: { + itemState: { + label: 'label1', + data: { + params: { + filter1: '1', + filter2: '2' + }, + highlights: { + group1__filter1_color: 'c2', + group1__filter3_color: 'c5' + } + } + }, + isDefault: true, + id: '1234' + }, + msg: 'Given valid data is preserved.' + }, + { + methodParams: [ + 'label2', + { + filter1: '1', + invert: '1', + filter15: '1', // Invalid filter - removed + filter2: '0', // Falsey value - removed + group1__filter1_color: 'c3', + foobar: 'w00t' // Unrecognized parameter - removed + } + ], + result: { + itemState: { + label: 'label2', + data: { + params: { + filter1: '1', + invert: '1' + }, + highlights: { + group1__filter1_color: 'c3' + } + } + }, + isDefault: false + }, + msg: 'Given data with invalid filters and highlights is normalized' + } + ]; + + filtersModel.initializeFilters( filterDefinition ); + + // Start with an empty saved queries model + queriesModel.initialize( {} ); + + cases.forEach( function ( testCase ) { + var itemID = queriesModel.addNewQuery.apply( queriesModel, testCase.methodParams ), + item = queriesModel.getItemByID( itemID ); + + assert.deepEqual( + item.getState(), + testCase.result.itemState, + testCase.msg + ' (itemState)' + ); + + assert.equal( + item.isDefault(), + testCase.result.isDefault, + testCase.msg + ' (isDefault)' + ); + + if ( testCase.result.id !== undefined ) { + assert.equal( + item.getID(), + testCase.result.id, + testCase.msg + ' (item ID)' + ); + } + } ); + } ); + QUnit.test( 'Manipulating queries', function ( assert ) { var id1, id2, item1, matchingItem, queriesStructure = {}, @@ -166,25 +297,18 @@ id1 = queriesModel.addNewQuery( 'New query 1', { - params: { - group2: 'filter5', - highlight: '1' - }, - highlights: { - filter1_color: 'c5', - group3option1_color: 'c1' - } + group2: 'filter5', + highlight: '1', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' } ); id2 = queriesModel.addNewQuery( 'New query 2', { - params: { - filter1: '1', - filter2: '1', - invert: '1' - }, - highlights: {} + filter1: '1', + filter2: '1', + invert: '1' } ); item1 = queriesModel.getItemByID( id1 ); @@ -207,8 +331,8 @@ highlight: '1' }, highlights: { - filter1_color: 'c5', - group3option1_color: 'c1' + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' } } }; @@ -261,14 +385,10 @@ // Find matching query matchingItem = queriesModel.findMatchingQuery( { - params: { - group2: 'filter5', - highlight: '1' - }, - highlights: { - filter1_color: 'c5', - group3option1_color: 'c1' - } + highlight: '1', + group2: 'filter5', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' } ); assert.deepEqual( @@ -280,16 +400,13 @@ // Find matching query with 0-values (base state) matchingItem = queriesModel.findMatchingQuery( { - params: { - group2: 'filter5', - filter1: '0', - filter2: '0', - highlight: '1' - }, - highlights: { - filter1_color: 'c5', - group3option1_color: 'c1' - } + group2: 'filter5', + filter1: '0', + filter2: '0', + highlight: '1', + invert: '0', + group1__filter1_color: 'c5', + group3__group3option1_color: 'c1' } ); assert.deepEqual(