$defaultPreferences['rcfilters-wl-saved-queries'] = [
'type' => 'api',
];
+ $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
+ 'type' => 'api',
+ ];
+ $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
+ 'type' => 'api',
+ ];
$defaultPreferences['rcfilters-rclimit'] = [
'type' => 'api',
];
return this.type;
};
+ /**
+ * Check whether this group is represented by a single parameter
+ * or whether each item is its own parameter
+ *
+ * @return {boolean} This group is a single parameter
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isPerGroupRequestParameter = function () {
+ return (
+ this.getType() === 'string_options' ||
+ this.getType() === 'single_option'
+ );
+ };
+
/**
* Get display group
*
return result;
};
+ /**
+ * Get the parameter names that represent filters that are excluded
+ * from saved queries.
+ *
+ * @return {string[]} Parameter names
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getExcludedParams = function () {
+ var result = [];
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isExcludedFromSavedQueries() ) {
+ 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;
+ };
+
/**
* Analyze the groups and their filters and output an object representing
* the state of the parameters they represent.
* @mixins OO.EmitterList
*
* @constructor
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
* @param {Object} [config] Configuration options
* @cfg {string} [default] Default query ID
*/
- mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( config ) {
+ mw.rcfilters.dm.SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
config = config || {};
// Mixin constructor
OO.EmitterList.call( this );
this.default = config.default;
- this.baseState = {};
+ this.filtersModel = filtersModel;
+ this.converted = false;
// Events
this.aggregate( { update: 'itemUpdate' } );
* Initialize the saved queries model by reading it from the user's settings.
* The structure of the saved queries is:
* {
+ * version: (string) Version number; if version 2, the query represents
+ * parameters. Otherwise, the older version represented filters
+ * and needs to be readjusted,
* default: (string) Query ID
* queries:{
* query_id_1: {
*
* @param {Object} [savedQueries] An object with the saved queries with
* the above structure.
- * @param {Object} [baseState] An object representing the base state
- * so we can normalize the data
- * @param {string[]} [ignoreFilters] Filters to ignore and remove from
- * the data
* @fires initialize
*/
- mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState, ignoreFilters ) {
- var items = [],
- defaultItem = null;
+ mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
+ var model = this,
+ excludedParams = this.filtersModel.getExcludedParams();
savedQueries = savedQueries || {};
- ignoreFilters = ignoreFilters || {};
-
- this.baseState = baseState;
this.clearItems();
+ this.default = null;
+ this.converted = false;
+
+ if ( savedQueries.version !== '2' ) {
+ // Old version dealt with filter names. We need to migrate to the new structure
+ // The new structure:
+ // {
+ // version: (string) '2',
+ // default: (string) Query ID,
+ // queries: {
+ // query_id: {
+ // label: (string) Name of the query
+ // data: {
+ // params: (object) Representing all the parameter states
+ // highlights: (object) Representing all the filter highlight states
+ // }
+ // }
+ // }
+ $.each( savedQueries.queries || {}, function ( id, obj ) {
+ if ( obj.data && obj.data.filters ) {
+ obj.data = model.convertToParameters( obj.data );
+ }
+ } );
+
+ this.converted = true;
+ savedQueries.version = '2';
+ }
+
+ // Initialize the query items
$.each( savedQueries.queries || {}, function ( id, obj ) {
- var item,
- normalizedData = $.extend( true, {}, baseState, obj.data ),
+ var normalizedData = obj.data,
isDefault = String( savedQueries.default ) === String( id );
- // Backwards-compat fix: We stored the 'highlight' state with
- // "1" and "0" instead of true/false; for already-stored states,
- // we need to fix that.
- // NOTE: Since this feature is only available in beta, we should
- // not need this line when we release this to the general wikis.
- // This method will automatically fix all saved queries anyways
- // for existing users, who are only betalabs users at the moment.
- normalizedData.highlights.highlight = !!Number( normalizedData.highlights.highlight );
-
- // Backwards-compat fix: Remove sticky parameters from the 'ignoreFilters' list
- ignoreFilters.forEach( function ( name ) {
- delete normalizedData.filters[ name ];
- } );
+ 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 ];
+ } );
- item = new mw.rcfilters.dm.SavedQueryItemModel(
- id,
- obj.label,
- normalizedData,
- { 'default': isDefault }
- );
+ id = String( id );
+ model.addNewQuery( obj.label, normalizedData, isDefault, id );
- if ( isDefault ) {
- defaultItem = item;
+ if ( isDefault ) {
+ model.default = id;
+ }
}
+ } );
- items.push( item );
+ this.emit( 'initialize' );
+ };
+
+ /**
+ * Convert from representation of filters to representation of parameters
+ *
+ * @param {Object} data Query data
+ * @return {Object} New converted query data
+ */
+ mw.rcfilters.dm.SavedQueriesModel.prototype.convertToParameters = function ( data ) {
+ var newData = {},
+ defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
+ fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
+ highlightEnabled = data.highlights.highlight;
+
+ delete data.highlights.highlight;
+
+ // Filters
+ newData.params = 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 ];
} );
- if ( defaultItem ) {
- this.default = defaultItem.getID();
+ // Add highlight and invert toggles to params
+ newData.params.highlight = String( Number( highlightEnabled || 0 ) );
+ newData.params.invert = String( Number( data.invert || 0 ) );
+
+ 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, { invert: '0', highlight: '0' }, allParams ),
+ highlights: highlightedItems
+ };
}
- this.addItems( items );
+ return this.baseParamState;
+ };
- this.emit( 'initialize' );
+ /**
+ * 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;
};
/**
*
* @param {string} label Label for the new query
* @param {Object} data Data for the new query
+ * @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 ) {
- var randomID = ( new Date() ).getTime(),
- normalizedData = $.extend( true, {}, this.baseState, data );
+ mw.rcfilters.dm.SavedQueriesModel.prototype.addNewQuery = function ( label, data, isDefault, id ) {
+ var randomID = String( id || ( new Date() ).getTime() ),
+ normalizedData = this.getMinimalParamList( data );
// Add item
this.addItems( [
new mw.rcfilters.dm.SavedQueryItemModel(
randomID,
label,
- normalizedData
+ normalizedData,
+ { 'default': isDefault }
)
] );
+ if ( isDefault ) {
+ this.setDefault( randomID );
+ }
+
return randomID;
};
* @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
- var model = this;
-
- fullQueryComparison = this.getDifferenceFromBase( fullQueryComparison );
+ // Minimize before comparison
+ fullQueryComparison = this.getMinimalParamList( fullQueryComparison );
return this.getItems().filter( function ( item ) {
- var comparedData = model.getDifferenceFromBase( item.getData() );
return OO.compare(
- comparedData,
+ item.getData(),
fullQueryComparison
);
} )[ 0 ];
};
- /**
- * Get a minimal representation of the state for comparison
- *
- * @param {Object} state Given state
- * @return {Object} Minimal state
- */
- mw.rcfilters.dm.SavedQueriesModel.prototype.getDifferenceFromBase = function ( state ) {
- var result = { filters: {}, highlights: {}, invert: state.invert },
- baseState = this.baseState;
-
- // XOR results
- $.each( state.filters, function ( name, value ) {
- if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) {
- result.filters[ name ] = value;
- }
- } );
-
- $.each( state.highlights, function ( name, value ) {
- if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value && name !== 'highlight' ) {
- result.highlights[ name ] = value;
- }
- } );
-
- return result;
- };
/**
* Get query by its identifier
*
} )[ 0 ];
};
+ /**
+ * Get an item's full data
+ *
+ * @param {string} queryID Query identifier
+ * @return {Object} Item's full data
+ */
+ mw.rcfilters.dm.SavedQueriesModel.prototype.getItemFullData = function ( queryID ) {
+ var item = this.getItemByID( queryID );
+
+ // Fill in the base params
+ return item ? $.extend( true, {}, this.getBaseParamState(), item.getData() ) : {};
+ };
+
/**
* Get the object representing the state of the entire model and items
*
* @return {Object} Object representing the state of the model and items
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.getState = function () {
- var obj = { queries: {} };
+ var model = this,
+ 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;
} );
mw.rcfilters.dm.SavedQueriesModel.prototype.getDefault = function () {
return this.default;
};
+
+ /**
+ * Check if the saved queries were converted
+ *
+ * @return {boolean} Saved queries were converted from the previous
+ * version to the new version
+ */
+ mw.rcfilters.dm.SavedQueriesModel.prototype.isConverted = function () {
+ return this.converted;
+ };
}( mediaWiki, jQuery ) );
this.baseFilterState = {};
this.uriProcessor = null;
this.initializing = false;
+ this.wereSavedQueriesSaved = false;
this.prevLoggedItems = [];
// Initialize the model
this.filtersModel.initializeFilters( filterStructure, views );
- this._buildBaseFilterState();
-
this.uriProcessor = new mw.rcfilters.UriProcessor(
this.filtersModel
);
parsedSavedQueries = {};
}
- // The queries are saved in a minimized state, so we need
- // to send over the base state so the saved queries model
- // can normalize them per each query item
- this.savedQueriesModel.initialize(
- parsedSavedQueries,
- this._getBaseFilterState(),
- // This is for backwards compatibility - delete all excluded filter states
- Object.keys( this.filtersModel.getExcludedFiltersState() )
- );
+ // Initialize saved queries
+ this.savedQueriesModel.initialize( parsedSavedQueries );
+ if ( this.savedQueriesModel.isConverted() ) {
+ // Since we know we converted, we're going to re-save
+ // the queries so they are now migrated to the new format
+ this._saveSavedQueries();
+ }
}
// Check whether we need to load defaults.
* @return {boolean} Defaults are all false
*/
mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
- var defaultFilters = this.filtersModel.getFiltersFromParameters( this._getDefaultParams() );
+ var defaultParams = this._getDefaultParams(),
+ defaultFilters = this.filtersModel.getFiltersFromParameters( defaultParams );
this._deleteExcludedValuesFromFilterState( defaultFilters );
+ if ( Object.keys( defaultParams ).some( function ( paramName ) {
+ return paramName.endsWith( '_color' ) && defaultParams[ paramName ] !== null;
+ } ) ) {
+ // There are highlights in the defaults, they're definitely
+ // not empty
+ return false;
+ }
+
// Defaults can change in a session, so we need to do this every time
return Object.keys( defaultFilters ).every( function ( filterName ) {
return !defaultFilters[ filterName ];
* @param {boolean} [setAsDefault=false] This query should be set as the default
*/
mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
- var queryID,
- highlightedItems = {},
+ var highlightedItems = {},
highlightEnabled = this.filtersModel.isHighlightEnabled(),
selectedState = this.filtersModel.getSelectedState();
// Prepare highlights
this.filtersModel.getHighlightedItems().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = highlightEnabled ?
+ highlightedItems[ item.getName() + '_color' ] = highlightEnabled ?
item.getHighlightColor() : null;
} );
- // These are filter states; highlight is stored as boolean
- highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
// Delete all excluded filters
this._deleteExcludedValuesFromFilterState( selectedState );
// Add item
- queryID = this.savedQueriesModel.addNewQuery(
+ this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
{
- filters: selectedState,
- highlights: highlightedItems,
- invert: this.filtersModel.areNamespacesInverted()
- }
+ params: $.extend(
+ true,
+ {
+ invert: String( Number( this.filtersModel.areNamespacesInverted() ) ),
+ highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
+ },
+ this.filtersModel.getParametersFromFilters( selectedState )
+ ),
+ highlights: highlightedItems
+ },
+ setAsDefault
);
- if ( setAsDefault ) {
- this.savedQueriesModel.setDefault( queryID );
- }
-
// Save item
this._saveSavedQueries();
};
* @param {string} queryID Query id
*/
mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
- var data, highlights,
+ var highlights,
queryItem = this.savedQueriesModel.getItemByID( queryID ),
+ data = this.savedQueriesModel.getItemFullData( queryID ),
currentMatchingQuery = this.findQueryMatchingCurrentState();
if (
currentMatchingQuery.getID() !== queryItem.getID()
)
) {
- data = queryItem.getData();
highlights = data.highlights;
- // Backwards compatibility; initial version mispelled 'highlight' with 'highlights'
- highlights.highlight = highlights.highlights || highlights.highlight;
-
// Update model state from filters
this.filtersModel.toggleFiltersSelected(
// Merge filters with excluded values
- $.extend( true, {}, data.filters, this.filtersModel.getExcludedFiltersState() )
+ $.extend(
+ true,
+ {},
+ this.filtersModel.getFiltersFromParameters( data.params ),
+ this.filtersModel.getExcludedFiltersState()
+ )
);
// Update namespace inverted property
- this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
+ this.filtersModel.toggleInvertedNamespaces( !!Number( data.params.invert ) );
// Update highlight state
- this.filtersModel.toggleHighlight( !!Number( highlights.highlight ) );
+ this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) );
this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = highlights[ filterItem.getName() ];
+ var color = highlights[ filterItem.getName() + '_color' ];
if ( color ) {
filterItem.setHighlightColor( color );
} else {
// Prepare highlights of the current query
this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = item.getHighlightColor();
+ highlightedItems[ item.getName() + '_color' ] = item.getHighlightColor();
} );
- highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
// Remove anything that should be excluded from the saved query
// this includes sticky filters and filters marked with 'excludedFromSavedQueries'
return this.savedQueriesModel.findMatchingQuery(
{
- filters: selectedState,
- highlights: highlightedItems,
- invert: this.filtersModel.areNamespacesInverted()
+ params: $.extend(
+ true,
+ {
+ highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ),
+ invert: String( Number( this.filtersModel.areNamespacesInverted() ) )
+ },
+ this.filtersModel.getParametersFromFilters( selectedState )
+ ),
+ highlights: highlightedItems
}
);
};
} );
};
- /**
- * Get an object representing the base state of parameters
- * and highlights.
- *
- * This is meant to make sure that the saved queries that are
- * in memory are always the same structure as what we would get
- * by calling the current model's "getSelectedState" and by checking
- * highlight items.
- *
- * In cases where a user saved a query when the system had a certain
- * set of filters, and then a filter was added to the system, we want
- * to make sure that the stored queries can still be comparable to
- * the current state, which means that we need the base state for
- * two operations:
- *
- * - Saved queries are stored in "minimal" view (only changed filters
- * are stored); When we initialize the system, we merge each minimal
- * query with the base state (using 'getNormalizedFilters') so all
- * saved queries have the exact same structure as what we would get
- * by checking the getSelectedState of the filter.
- * - When we save the queries, we minimize the object to only represent
- * whatever has actually changed, rather than store the entire
- * object. To check what actually is different so we can store it,
- * we need to obtain a base state to compare against, this is
- * what #_getMinimalFilterList does
- */
- mw.rcfilters.Controller.prototype._buildBaseFilterState = function () {
- var defaultParams = this.filtersModel.getDefaultParams(),
- highlightedItems = {};
-
- // Prepare highlights
- this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() ] = null;
- } );
- highlightedItems.highlight = false;
-
- this.baseFilterState = {
- filters: this.filtersModel.getFiltersFromParameters( defaultParams ),
- highlights: highlightedItems,
- invert: false
- };
- };
-
- /**
- * Get an object representing the base filter state of both
- * filters and highlights. The structure is similar to what we use
- * to store each query in the saved queries object:
- * {
- * filters: {
- * filterName: (bool)
- * },
- * highlights: {
- * filterName: (string|null)
- * }
- * }
- *
- * @return {Object} Object representing the base state of
- * parameters and highlights
- */
- mw.rcfilters.Controller.prototype._getBaseFilterState = function () {
- return this.baseFilterState;
- };
-
- /**
- * Get an object that holds only the parameters and highlights that have
- * values different than the base default value.
- *
- * This is the reverse of the normalization we do initially on loading and
- * initializing the saved queries model.
- *
- * @param {Object} valuesObject Object representing the state of both
- * filters and highlights in its normalized version, to be minimized.
- * @return {Object} Minimal filters and highlights list
- */
- mw.rcfilters.Controller.prototype._getMinimalFilterList = function ( valuesObject ) {
- var result = { filters: {}, highlights: {}, invert: valuesObject.invert },
- baseState = this._getBaseFilterState();
-
- // XOR results
- $.each( valuesObject.filters, function ( name, value ) {
- if ( baseState.filters !== undefined && baseState.filters[ name ] !== value ) {
- result.filters[ name ] = value;
- }
- } );
-
- $.each( valuesObject.highlights, function ( name, value ) {
- if ( baseState.highlights !== undefined && baseState.highlights[ name ] !== value ) {
- result.highlights[ name ] = value;
- }
- } );
-
- return result;
- };
-
/**
* Save the current state of the saved queries model with all
* query item representation in the user settings.
*/
mw.rcfilters.Controller.prototype._saveSavedQueries = function () {
- var stringified,
- state = this.savedQueriesModel.getState(),
- controller = this;
-
- // Minimize before save
- $.each( state.queries, function ( queryID, info ) {
- state.queries[ queryID ].data = controller._getMinimalFilterList( info.data );
- } );
+ var stringified, oldPrefValue,
+ backupPrefName = this.savedQueriesPreferenceName + '-versionbackup',
+ state = this.savedQueriesModel.getState();
// Stringify state
stringified = JSON.stringify( state );
return;
}
+ if ( !this.wereSavedQueriesSaved && this.savedQueriesModel.isConverted() ) {
+ // The queries were converted from the previous version
+ // Keep the old string in the [prefname]-versionbackup
+ oldPrefValue = mw.user.options.get( this.savedQueriesPreferenceName );
+
+ // Save the old preference in the backup preference
+ new mw.Api().saveOption( backupPrefName, oldPrefValue );
+ // Update the preference for this session
+ mw.user.options.set( backupPrefName, oldPrefValue );
+ }
+
// Save the preference
new mw.Api().saveOption( this.savedQueriesPreferenceName, stringified );
// Update the preference for this session
mw.user.options.set( this.savedQueriesPreferenceName, stringified );
+
+ // Tag as already saved so we don't do this again
+ this.wereSavedQueriesSaved = true;
};
/**
* @return {Object} Default parameters
*/
mw.rcfilters.Controller.prototype._getDefaultParams = function () {
- var data, queryHighlights,
- savedParams = {},
- savedHighlights = {},
- defaultSavedQueryItem = !mw.user.isAnon() && this.savedQueriesModel.getItemByID( this.savedQueriesModel.getDefault() );
-
- if ( defaultSavedQueryItem ) {
- data = defaultSavedQueryItem.getData();
-
- queryHighlights = data.highlights || {};
- savedParams = this.filtersModel.getParametersFromFilters(
- $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+ var savedFilters,
+ data = ( !mw.user.isAnon() && this.savedQueriesModel.getItemFullData( this.savedQueriesModel.getDefault() ) ) || {};
+
+ if ( !$.isEmptyObject( data ) ) {
+ // Merge saved filter state with sticky filter values
+ savedFilters = $.extend(
+ true, {},
+ this.filtersModel.getFiltersFromParameters( data.params ),
+ this.filtersModel.getStickyFiltersState()
);
- // Translate highlights to parameters
- savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
- $.each( queryHighlights, function ( filterName, color ) {
- if ( filterName !== 'highlights' ) {
- savedHighlights[ filterName + '_color' ] = color;
- }
- } );
-
- return $.extend( true, {}, savedParams, savedHighlights, { invert: String( Number( data.invert || 0 ) ) } );
+ // Return parameter representation
+ return $.extend( true, {},
+ this.filtersModel.getParametersFromFilters( savedFilters ),
+ data.highlights,
+ { highlight: data.params.highlight, invert: data.params.invert }
+ );
}
-
return this.filtersModel.getDefaultParams();
};
savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
changesListModel = new mw.rcfilters.dm.ChangesListViewModel(),
- savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel(),
+ savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
controller = new mw.rcfilters.Controller(
filtersModel, changesListModel, savedQueriesModel,
{
'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js',
'tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js',
+ 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueryItemModel.test.js',
+ 'tests/qunit/suites/resources/mediawiki.rcfilters/dm.SavedQueriesModel.test.js',
'tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
--- /dev/null
+/* eslint-disable camelcase */
+( function ( mw ) {
+ var filterDefinition = [ {
+ name: 'group1',
+ type: 'send_unselected_if_any',
+ 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: 'group2',
+ type: 'string_options',
+ separator: ',',
+ filters: [
+ { name: 'filter4' },
+ { name: 'filter5' },
+ { name: 'filter6' }
+ ]
+ }, {
+ name: 'group3',
+ type: 'boolean',
+ isSticky: true,
+ filters: [
+ { name: 'group3option1' },
+ { name: 'group3option2' },
+ { name: 'group3option3' }
+ ]
+ } ],
+ queriesFilterRepresentation = {
+ queries: {
+ 1234: {
+ label: 'Item converted',
+ data: {
+ filters: {
+ // - This value is true, but the original filter-representation
+ // of the saved queries ran against defaults. Since filter1 was
+ // set as default in the definition, the value would actually
+ // not appear in the representation itself.
+ // It is considered 'true', though, and should appear in the
+ // converted result in its parameter representation.
+ // >> group1__filter1: true,
+ // - The reverse is true for filter3. Filter3 is set as default
+ // but we don't want it in this representation of the saved query.
+ // Since the filter representation ran against default values,
+ // it will appear as 'false' value in this representation explicitly
+ // and the resulting parameter representation should have that
+ // as the result as well
+ group1__filter3: false,
+ group2__filter4: true,
+ group3__group3option1: true
+ },
+ highlights: {
+ highlight: true,
+ filter1: 'c5',
+ group3option1: 'c1'
+ },
+ invert: true
+ }
+ }
+ }
+ },
+ queriesParamRepresentation = {
+ version: '2',
+ queries: {
+ 1234: {
+ label: 'Item converted',
+ data: {
+ params: {
+ // filter1 is 'true' so filter2 and filter3 are both '1'
+ // in param representation
+ filter2: '1', filter3: '1',
+ // Group type string_options
+ group2: 'filter4',
+ // Note - Group3 is sticky, so it won't show in output
+ // Invert/highlight toggles
+ invert: '1',
+ highlight: '1'
+ },
+ highlights: {
+ filter1_color: 'c5',
+ group3option1_color: 'c1'
+ }
+ }
+ }
+ }
+ };
+
+ QUnit.module( 'mediawiki.rcfilters - SavedQueriesModel' );
+
+ QUnit.test( 'Initializing queries', function ( assert ) {
+ var filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
+ exampleQueryStructure = {
+ version: '2',
+ default: '1234',
+ queries: {
+ 1234: {
+ label: 'Query 1234',
+ data: {
+ params: {
+ filter2: '1'
+ },
+ highlights: {
+ filter5_color: 'c2'
+ }
+ }
+ }
+ }
+ },
+ cases = [
+ {
+ input: {},
+ finalState: { version: '2', queries: {} },
+ msg: 'Empty initial query structure results in base saved queries structure.'
+ },
+ {
+ input: $.extend( true, {}, exampleQueryStructure ),
+ finalState: $.extend( true, {}, exampleQueryStructure ),
+ msg: 'Initialization of given query structure does not corrupt the structure.'
+ },
+ {
+ // Converting from old structure
+ input: $.extend( true, {}, queriesFilterRepresentation ),
+ finalState: $.extend( true, {}, queriesParamRepresentation ),
+ msg: 'Conversion from filter representation to parameters retains data.'
+ },
+ {
+ // Converting from old structure with default
+ input: $.extend( true, { default: '1234' }, queriesFilterRepresentation ),
+ finalState: $.extend( true, { default: '1234' }, queriesParamRepresentation ),
+ msg: 'Conversion from filter representation to parameters, with default set up, retains data.'
+ },
+ {
+ // New structure
+ input: $.extend( true, {}, queriesParamRepresentation ),
+ finalState: $.extend( true, {}, queriesParamRepresentation ),
+ msg: 'Parameter representation retains its queries structure'
+ }
+ ];
+
+ filtersModel.initializeFilters( filterDefinition );
+
+ cases.forEach( function ( testCase ) {
+ queriesModel.initialize( testCase.input );
+ assert.deepEqual(
+ queriesModel.getState(),
+ testCase.finalState,
+ testCase.msg
+ );
+ } );
+ } );
+
+ QUnit.test( 'Manipulating queries', function ( assert ) {
+ var id1, id2, item1, matchingItem,
+ queriesStructure = {},
+ filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ queriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel );
+
+ filtersModel.initializeFilters( filterDefinition );
+
+ // Start with an empty saved queries model
+ queriesModel.initialize( {} );
+
+ // Add items
+ id1 = queriesModel.addNewQuery(
+ 'New query 1',
+ {
+ params: {
+ group2: 'filter5',
+ highlight: '1'
+ },
+ highlights: {
+ filter1_color: 'c5',
+ group3option1_color: 'c1'
+ }
+ }
+ );
+ id2 = queriesModel.addNewQuery(
+ 'New query 2',
+ {
+ params: {
+ filter1: '1',
+ filter2: '1',
+ invert: '1'
+ },
+ highlights: {}
+ }
+ );
+ item1 = queriesModel.getItemByID( id1 );
+
+ assert.equal(
+ item1.getID(),
+ id1,
+ 'Item created and its data retained successfully'
+ );
+
+ // NOTE: All other methods that the item itself returns are
+ // tested in the dm.SavedQueryItemModel.test.js file
+
+ // Build the query structure we expect per item
+ queriesStructure[ id1 ] = {
+ label: 'New query 1',
+ data: {
+ params: {
+ group2: 'filter5',
+ highlight: '1'
+ },
+ highlights: {
+ filter1_color: 'c5',
+ group3option1_color: 'c1'
+ }
+ }
+ };
+ queriesStructure[ id2 ] = {
+ label: 'New query 2',
+ data: {
+ params: {
+ filter1: '1',
+ filter2: '1',
+ invert: '1'
+ },
+ highlights: {}
+ }
+ };
+
+ assert.deepEqual(
+ queriesModel.getState(),
+ {
+ version: '2',
+ queries: queriesStructure
+ },
+ 'Full query represents current state of items'
+ );
+
+ // Add default
+ queriesModel.setDefault( id2 );
+
+ assert.deepEqual(
+ queriesModel.getState(),
+ {
+ version: '2',
+ default: id2,
+ queries: queriesStructure
+ },
+ 'Setting default is reflected in queries state'
+ );
+
+ // Remove default
+ queriesModel.setDefault( null );
+
+ assert.deepEqual(
+ queriesModel.getState(),
+ {
+ version: '2',
+ queries: queriesStructure
+ },
+ 'Removing default is reflected in queries state'
+ );
+
+ // Find matching query
+ matchingItem = queriesModel.findMatchingQuery(
+ {
+ params: {
+ group2: 'filter5',
+ highlight: '1'
+ },
+ highlights: {
+ filter1_color: 'c5',
+ group3option1_color: 'c1'
+ }
+ }
+ );
+ assert.deepEqual(
+ matchingItem.getID(),
+ id1,
+ 'Finding matching item by identical state'
+ );
+
+ // Find matching query with 0-values (base state)
+ matchingItem = queriesModel.findMatchingQuery(
+ {
+ params: {
+ group2: 'filter5',
+ filter1: '0',
+ filter2: '0',
+ highlight: '1',
+ invert: '0'
+ },
+ highlights: {
+ filter1_color: 'c5',
+ group3option1_color: 'c1'
+ }
+ }
+ );
+ assert.deepEqual(
+ matchingItem.getID(),
+ id1,
+ 'Finding matching item by "dirty" state with 0-base values'
+ );
+ } );
+}( mediaWiki ) );
--- /dev/null
+/* eslint-disable camelcase */
+( function ( mw ) {
+ var itemData = {
+ params: {
+ param1: '1',
+ param2: 'foo|bar',
+ highlight: '1',
+ invert: '0'
+ },
+ highlights: {
+ param1_color: 'c1',
+ param2_color: 'c2'
+ }
+ };
+
+ QUnit.module( 'mediawiki.rcfilters - SavedQueryItemModel' );
+
+ QUnit.test( 'Initializing and getters', function ( assert ) {
+ var model;
+
+ model = new mw.rcfilters.dm.SavedQueryItemModel(
+ 'randomID',
+ 'Some label',
+ $.extend( true, {}, itemData )
+ );
+
+ assert.equal(
+ model.getID(),
+ 'randomID',
+ 'Item ID is retained'
+ );
+
+ assert.equal(
+ model.getLabel(),
+ 'Some label',
+ 'Item label is retained'
+ );
+
+ assert.deepEqual(
+ model.getData(),
+ itemData,
+ 'Item data is retained'
+ );
+
+ assert.ok(
+ !model.isDefault(),
+ 'Item default state is retained.'
+ );
+ } );
+
+ QUnit.test( 'Default', function ( assert ) {
+ var model;
+
+ model = new mw.rcfilters.dm.SavedQueryItemModel(
+ 'randomID',
+ 'Some label',
+ $.extend( true, {}, itemData )
+ );
+
+ assert.ok(
+ !model.isDefault(),
+ 'Default state represented when item initialized with default:false.'
+ );
+
+ model.toggleDefault( true );
+ assert.ok(
+ model.isDefault(),
+ 'Default state toggles to true successfully'
+ );
+
+ model.toggleDefault( false );
+ assert.ok(
+ !model.isDefault(),
+ 'Default state toggles to false successfully'
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.SavedQueryItemModel(
+ 'randomID',
+ 'Some label',
+ $.extend( true, {}, itemData ),
+ { default: true }
+ );
+
+ assert.ok(
+ model.isDefault(),
+ 'Default state represented when item initialized with default:true.'
+ );
+ } );
+}( mediaWiki ) );