$defaultPreferences['rcfilters-saved-queries'] = [
'type' => 'api',
];
+ $defaultPreferences['rcfilters-rclimit'] = [
+ 'type' => 'api',
+ ];
if ( $config->get( 'RCWatchCategoryMembership' ) ) {
$defaultPreferences['hidecategorization'] = [
* @cfg {string} [type='send_unselected_if_any'] Group type
* @cfg {string} [view='default'] Name of the display group this group
* is a part of.
+ * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
+ * that every time a value is changed, it becomes the new default
* @cfg {string} [title] Group title
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
* @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
this.name = name;
this.type = config.type || 'send_unselected_if_any';
this.view = config.view || 'default';
+ this.sticky = !!config.isSticky;
this.title = config.title || name;
this.hidden = !!config.hidden;
this.allowArbitrary = !!config.allowArbitrary;
var subsetNames = [],
filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
group: model.getName(),
- useDefaultAsBaseValue: !!filter.useDefaultAsBaseValue,
label: filter.label || filter.name,
description: filter.description || '',
labelPrefixKey: model.labelPrefixKey,
// For this group, the parameter is the group name,
// and a single item can be selected: default or first item
this.defaultParams[ this.getName() ] = defaultParam;
-
- // Single option means there must be a single option
- // selected, so we have to either select the default
- // or select the first option
- this.selectItemByParamName( defaultParam );
}
// Store default filter state based on default params
this.defaultFilters = this.getFilterRepresentation( this.getDefaultParams() );
// Check for filters that should be initially selected by their default value
- this.getItems().forEach( function ( item ) {
- if (
- item.isUsingDefaultAsBaseValue() &&
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- )
- ) {
- // Apply selection
- item.toggleSelected( !!model.defaultFilters[ item.getName() ] );
- }
- } );
+ if ( this.isSticky() ) {
+ $.each( this.defaultFilters, function ( filterName, filterValue ) {
+ model.getItemByName( filterName ).toggleSelected( filterValue );
+ } );
+ }
+
+ // Verify that single_option group has at least one item selected
+ if (
+ this.getType() === 'single_option' &&
+ this.getSelectedItems().length === 0
+ ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.selectItemByParamName( defaultParam );
+ }
};
/**
// Single option means there must be a single option
// selected, so we have to either select the default
// or select the first option
- this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] );
+ this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] ) ||
+ this.getItems()[ 0 ];
this.currSelected.toggleSelected( true );
changed = true;
}
this.active !== active ||
this.currSelected !== item
) {
+ if ( this.isSticky() ) {
+ // If this group is sticky, then change the default according to the
+ // current selection.
+ this.defaultParams = this.getParamRepresentation( this.getSelectedState() );
+ }
+
this.active = active;
this.currSelected = item;
// This means we have not been given a filter representation
// so we are building one based on current state
filterRepresentation[ item.getName() ] = item.isSelected();
- } else if ( !filterRepresentation[ item.getName() ] ) {
+ } else if ( filterRepresentation[ item.getName() ] === undefined ) {
// We are given a filter representation, but we have to make
// sure that we fill in the missing filters if there are any
- // we will assume they are all falsey, unless they have
- // isUsingDefaultAsBaseValue, in which case they get their
- // default state
- if (
- item.isUsingDefaultAsBaseValue() &&
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- )
- ) {
+ // we will assume they are all falsey
+ if ( model.isSticky() ) {
filterRepresentation[ item.getName() ] = !!defaultFilters[ item.getName() ];
} else {
filterRepresentation[ item.getName() ] = false;
* @return {Object} Filter representation
*/
mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
- var areAnySelected, paramValues, defaultValue, item, currentValue,
+ var areAnySelected, paramValues, item, currentValue,
oneWasSelected = false,
defaultParams = this.getDefaultParams(),
- defaultFilters = this.getDefaultFilters(),
expandedParams = $.extend( true, {}, paramRepresentation ),
model = this,
paramToFilterMap = {},
result = {};
+ if ( this.isSticky() ) {
+ // If the group is sticky, check if all parameters are represented
+ // and for those that aren't represented, add them with their default
+ // values
+ paramRepresentation = $.extend( true, {}, this.getDefaultParams(), paramRepresentation );
+ }
+
paramRepresentation = paramRepresentation || {};
if (
this.getType() === 'send_unselected_if_any' ||
} );
$.each( expandedParams, function ( paramName, paramValue ) {
- var value = paramValue,
- filterItem = paramToFilterMap[ paramName ];
+ var filterItem = paramToFilterMap[ paramName ];
if ( model.getType() === 'send_unselected_if_any' ) {
// Flip the definition between the parameter
false;
} else if ( model.getType() === 'boolean' ) {
// Straight-forward definition of state
- if (
- filterItem.isUsingDefaultAsBaseValue() &&
- paramRepresentation[ filterItem.getParamName() ] === undefined
- ) {
- value = defaultParams[ filterItem.getParamName() ];
- }
- result[ filterItem.getName() ] = !!Number( value );
+ result[ filterItem.getName() ] = !!Number( paramRepresentation[ filterItem.getParamName() ] );
}
} );
} else if ( this.getType() === 'string_options' ) {
} else if ( this.getType() === 'single_option' ) {
// There is parameter that fits a single filter and if not, get the default
this.getItems().forEach( function ( filterItem ) {
- var selected = false;
+ var selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
- if (
- filterItem.isUsingDefaultAsBaseValue() &&
- paramRepresentation[ model.getName() ] === undefined
- ) {
- selected = !!Number( paramRepresentation[ model.getName() ] );
- } else {
- selected = filterItem.getParamName() === paramRepresentation[ model.getName() ];
- }
result[ filterItem.getName() ] = selected;
oneWasSelected = oneWasSelected || selected;
} );
// Go over result and make sure all filters are represented.
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
- if (
- (
- // This setting can only be applied to these groups
- // the other groups are way too complex for that
- model.getType() === 'single_option' ||
- model.getType() === 'boolean'
- ) &&
- result[ filterItem.getName() ] === undefined &&
- filterItem.isUsingDefaultAsBaseValue()
- ) {
- result[ filterItem.getName() ] = !!defaultFilters[ filterItem.getName() ];
+ if ( result[ filterItem.getName() ] === undefined ) {
+ result[ filterItem.getName() ] = false;
}
- oneWasSelected = oneWasSelected || !!result[ filterItem.getName() ];
} );
// Make sure that at least one option is selected in
this.getType() === 'single_option' &&
!oneWasSelected
) {
- defaultValue = this.getDefaultParams();
- item = this.getItemByParamName( defaultValue[ this.getName() ] );
+ if ( defaultParams[ this.getName() ] ) {
+ item = this.getItemByParamName( defaultParams[ this.getName() ] );
+ } else {
+ item = this.getItems()[ 0 ];
+ }
result[ item.getName() ] = true;
}
return result;
};
+ /**
+ * Get current selected state of all filter items in this group
+ *
+ * @return {Object} Selected state
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getSelectedState = function () {
+ var state = {};
+
+ this.getItems().forEach( function ( filterItem ) {
+ state[ filterItem.getName() ] = filterItem.isSelected();
+ } );
+
+ return state;
+ };
+
/**
* Get item by its filter name
*
mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
return this.fullCoverage;
};
+
+ /**
+ * Check whether the group is defined as sticky default
+ *
+ * @return {boolean} Group is sticky default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isSticky = function () {
+ return this.sticky;
+ };
}( mediaWiki ) );
return result;
};
+ /**
+ * Get a parameter representation of all sticky parameters
+ *
+ * @return {Object} Sticky parameter values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyParams = function () {
+ var result = {};
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getDefaultParams() );
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get a filter representation of all sticky parameters
+ *
+ * @return {Object} Sticky filters values
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getStickyFiltersState = function () {
+ var result = {};
+
+ $.each( this.groups, function ( name, model ) {
+ if ( model.isSticky() ) {
+ $.extend( true, result, model.getSelectedState() );
+ }
+ } );
+
+ return result;
+ };
+
/**
* Analyze the groups and their filters and output an object representing
* the state of the parameters they represent.
this.namePrefix = config.namePrefix || 'item_';
this.name = this.namePrefix + param;
- this.useDefaultAsBaseValue = !!config.useDefaultAsBaseValue;
this.label = config.label || this.name;
this.labelPrefixKey = config.labelPrefixKey;
this.description = config.description || '';
return this.identifiers;
};
- /**
- * Check whether the item uses its default state as a base value
- *
- * @return {boolean} Use default as base value
- */
- mw.rcfilters.dm.ItemModel.prototype.isUsingDefaultAsBaseValue = function () {
- return this.useDefaultAsBaseValue;
- };
-
/**
* Toggle the highlight feature on and off for this filter.
* It only works if highlight is supported for this filter.
* 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 ) {
+ mw.rcfilters.dm.SavedQueriesModel.prototype.initialize = function ( savedQueries, baseState, ignoreFilters ) {
var items = [],
defaultItem = null;
savedQueries = savedQueries || {};
+ ignoreFilters = ignoreFilters || {};
this.baseState = baseState;
// 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 ];
+ } );
+
item = new mw.rcfilters.dm.SavedQueryItemModel(
id,
obj.label,
* @param {Object} [tagList] Tag definition
*/
mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
- var parsedSavedQueries,
+ var parsedSavedQueries, limitDefault,
controller = this,
views = {},
items = [],
};
}
+ // Convert the default from the old preference
+ // since the limit preference actually affects more
+ // than just the RecentChanges page
+ limitDefault = Number( mw.user.options.get( 'rcfilters-rclimit', mw.user.options.get( 'rclimit', '50' ) ) );
+
// Add parameter range operations
views.range = {
groups: [
allowArbitrary: true,
validate: $.isNumeric,
sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
- 'default': mw.user.options.get( 'rclimit' ),
+ 'default': String( limitDefault ),
+ isSticky: true,
filters: [ 50, 100, 250, 500 ].map( function ( num ) {
return controller._createFilterDataFromNumber( num, num );
} )
( Number( i ) * 24 ).toFixed( 2 ) :
Number( i );
},
- 'default': mw.user.options.get( 'rcdays' ),
+ 'default': mw.user.options.get( 'rcdays', '30' ),
+ isSticky: true,
filters: [
// Hours (1, 2, 6, 12)
0.04166, 0.0833, 0.25, 0.5,
// can normalize them per each query item
this.savedQueriesModel.initialize(
parsedSavedQueries,
- this._getBaseFilterState()
+ this._getBaseFilterState(),
+ // This is for backwards compatibility - delete all sticky filter states
+ Object.keys( this.filtersModel.getStickyFiltersState() )
);
// Check whether we need to load defaults.
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
+
this.updateChangesList();
};
mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
var queryID,
highlightedItems = {},
- highlightEnabled = this.filtersModel.isHighlightEnabled();
+ highlightEnabled = this.filtersModel.isHighlightEnabled(),
+ selectedState = this.filtersModel.getSelectedState();
// Prepare highlights
this.filtersModel.getHighlightedItems().forEach( function ( item ) {
// These are filter states; highlight is stored as boolean
highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
+ // Delete all sticky filters
+ this._deleteStickyValuesFromFilterState( selectedState );
+
// Add item
queryID = this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
{
- filters: this.filtersModel.getSelectedState(),
+ filters: selectedState,
highlights: highlightedItems,
invert: this.filtersModel.areNamespacesInverted()
}
highlights.highlight = highlights.highlights || highlights.highlight;
// Update model state from filters
- this.filtersModel.toggleFiltersSelected( data.filters );
+ this.filtersModel.toggleFiltersSelected(
+ // Merge filters with sticky values
+ $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+ );
// Update namespace inverted property
this.filtersModel.toggleInvertedNamespaces( !!Number( data.invert ) );
* @return {boolean} Query exists
*/
mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
- var highlightedItems = {};
+ var highlightedItems = {},
+ selectedState = this.filtersModel.getSelectedState();
// Prepare highlights of the current query
this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
} );
highlightedItems.highlight = this.filtersModel.isHighlightEnabled();
+ // Remove sticky filters
+ this._deleteStickyValuesFromFilterState( selectedState );
+
return this.savedQueriesModel.findMatchingQuery(
{
- filters: this.filtersModel.getSelectedState(),
+ filters: selectedState,
highlights: highlightedItems,
invert: this.filtersModel.areNamespacesInverted()
}
);
};
+ /**
+ * Delete sticky filters from given object
+ *
+ * @param {Object} filterState Filter state
+ */
+ mw.rcfilters.Controller.prototype._deleteStickyValuesFromFilterState = function ( filterState ) {
+ // Remove sticky filters
+ $.each( this.filtersModel.getStickyFiltersState(), function ( filterName ) {
+ delete filterState[ filterName ];
+ } );
+ };
+
/**
* Get an object representing the base state of parameters
* and highlights.
mw.user.options.set( 'rcfilters-saved-queries', stringified );
};
+ /**
+ * Update sticky preferences with current model state
+ */
+ mw.rcfilters.Controller.prototype.updateStickyPreferences = function () {
+ // Update default sticky values with selected, whether they came from
+ // the initial defaults or from the URL value that is being normalized
+ this.updateDaysDefault( this.filtersModel.getGroup( 'days' ).getSelectedItems()[ 0 ].getParamName() );
+ this.updateLimitDefault( this.filtersModel.getGroup( 'limit' ).getSelectedItems()[ 0 ].getParamName() );
+ };
+
+ /**
+ * Update the limit default value
+ *
+ * @param {number} newValue New value
+ */
+ mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
+ if ( !$.isNumeric( newValue ) ) {
+ return;
+ }
+
+ newValue = Number( newValue );
+
+ if ( mw.user.options.get( 'rcfilters-rclimit' ) !== newValue ) {
+ // Save the preference
+ new mw.Api().saveOption( 'rcfilters-rclimit', newValue );
+ // Update the preference for this session
+ mw.user.options.set( 'rcfilters-rclimit', newValue );
+ }
+ };
+
+ /**
+ * Update the days default value
+ *
+ * @param {number} newValue New value
+ */
+ mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
+ if ( !$.isNumeric( newValue ) ) {
+ return;
+ }
+
+ newValue = Number( newValue );
+
+ if ( mw.user.options.get( 'rcdays' ) !== newValue ) {
+ // Save the preference
+ new mw.Api().saveOption( 'rcdays', newValue );
+ // Update the preference for this session
+ mw.user.options.set( 'rcdays', newValue );
+ }
+ };
+
/**
* Synchronize the URL with the current state of the filters
* without adding an history entry.
this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
+ // Update the sticky preferences, in case we received a value
+ // from the URL
+ this.updateStickyPreferences();
+
// Only update and fetch new results if it is requested
if ( fetchChangesList ) {
this.updateChangesList();
data = defaultSavedQueryItem.getData();
queryHighlights = data.highlights || {};
- savedParams = this.filtersModel.getParametersFromFilters( data.filters || {} );
+ savedParams = this.filtersModel.getParametersFromFilters(
+ // Merge filters with sticky values
+ $.extend( true, {}, data.filters, this.filtersModel.getStickyFiltersState() )
+ );
// Translate highlights to parameters
savedHighlights.highlight = String( Number( queryHighlights.highlight ) );
*/
mw.rcfilters.Controller.prototype._fetchChangesList = function () {
var uri = this._getUpdatedUri(),
+ stickyParams = this.filtersModel.getStickyParams(),
requestId = ++this.requestCounter,
latestRequest = function () {
return requestId === this.requestCounter;
}.bind( this );
+ // Sticky parameters override the URL params
+ // this is to make sure that whether we represent
+ // the sticky params in the URL or not (they may
+ // be normalized out) the sticky parameters are
+ // always being sent to the server with their
+ // current/default values
+ uri.extend( stickyParams );
+
return $.ajax( uri.toString(), { contentType: 'html' } )
.then(
// Success
* @return {Object} Empty parameter state
*/
mw.rcfilters.UriProcessor.prototype._getEmptyParameterState = function () {
- return this.emptyParameterState;
+ // Override empty parameter state with the sticky parameter values
+ return $.extend( true, {}, this.emptyParameterState, this.filtersModel.getStickyParams() );
};
/**
* @param {string} filterName Chosen filter name
*/
mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+ var item = this.limitGroupModel.getItemByName( filterName );
+
this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateLimitDefault( item.getParamName() );
this.button.popup.toggle( false );
};
* @param {string} filterName Chosen filter name
*/
mw.rcfilters.ui.DateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+ var item = this.daysGroupModel.getItemByName( filterName );
+
this.controller.toggleFilterSelect( filterName, true );
+ this.controller.updateDaysDefault( item.getParamName() );
this.button.popup.toggle( false );
};
}, {
name: 'group6',
type: 'boolean',
+ isSticky: true,
filters: [
- { name: 'group6option1', label: 'group6option1-label', description: 'group5option1-desc' },
- { name: 'group6option2', label: 'group6option2-label', description: 'group5option2-desc', default: true, useDefaultAsBaseValue: true },
- { name: 'group6option3', label: 'group6option3-label', description: 'group5option3-desc', default: true }
+ { 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: 'group7',
+ type: 'single_option',
+ 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' }
]
} ],
viewsDefinition = {
group6option1: '0',
group6option2: '1',
group6option3: '1',
+ group7: 'group7option2',
namespace: ''
},
baseParamRepresentation = {
group5: 'option1',
group6option1: '0',
group6option2: '1',
- group6option3: '0',
+ group6option3: '1',
+ group7: 'group7option2',
namespace: ''
},
baseFilterRepresentation = {
group5__option3: false,
group6__group6option1: false,
group6__group6option2: true,
- group6__group6option3: false,
+ group6__group6option3: true,
+ group7__group7option1: false,
+ group7__group7option2: true,
+ group7__group7option3: false,
namespace__0: false,
namespace__1: false,
namespace__2: false,
group5__option3: { selected: false, conflicted: false, included: false },
group6__group6option1: { selected: false, conflicted: false, included: false },
group6__group6option2: { selected: true, conflicted: false, included: false },
- group6__group6option3: { selected: false, conflicted: false, included: false },
+ group6__group6option3: { selected: true, conflicted: false, included: false },
+ group7__group7option1: { selected: false, conflicted: false, included: false },
+ group7__group7option2: { selected: true, conflicted: false, included: false },
+ group7__group7option3: { selected: false, conflicted: false, included: false },
namespace__0: { selected: false, conflicted: false, included: false },
namespace__1: { selected: false, conflicted: false, included: false },
namespace__2: { selected: false, conflicted: false, included: false },
defaultParameters,
'Default parameters are stored properly per filter and group'
);
+
+ // Change sticky filter
+ model.toggleFiltersSelected( {
+ group7__group7option1: true
+ } );
+
+ // Make sure defaults have changed
+ assert.deepEqual(
+ model.getDefaultParams(),
+ $.extend( true, {}, defaultParameters, {
+ group7: 'group7option1'
+ } ),
+ 'Default parameters are stored properly per filter and group'
+ );
} );
QUnit.test( 'Finding matching filters', function ( assert ) {