jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/361186 )
Change subject: RCFilters: Add range group filters - limit, days and hours
......................................................................
RCFilters: Add range group filters - limit, days and hours
- Add 'hidden' groups that have base defaults but are not
viewed in the filter drop-down.
- Add a UI for days, hours and limit selections, based on their
group models.
- Clean up the fieldset form to remove redundant line breaks
and empty objects.
- Add 'hours' as a subset of days, where the UI can split itself
by picking up values >=1 and <1
- Add the ability to allow 'arbitrary' information from the URL
values, but also make sure there is a validation method
and a possibility to re-sort the values that are added in.
Bug: T162784
Bug: T162786
Change-Id: I8068a7cc411eef40ddb8af4eef1d4f1e5f2a2b82
---
M languages/i18n/en.json
M languages/i18n/qqq.json
M resources/Resources.php
M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
A resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less
M
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
M
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less
A
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less
A
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
A
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
M
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
M tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js
19 files changed, 740 insertions(+), 30 deletions(-)
Approvals:
Catrope: Looks good to me, approved
jenkins-bot: Verified
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 9447de6..fdfe75a 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -1351,6 +1351,12 @@
"recentchanges-submit": "Show",
"rcfilters-activefilters": "Active filters",
"rcfilters-advancedfilters": "Advanced filters",
+ "rcfilters-limit-title": "Changes to show",
+ "rcfilters-limit-shownum": "Show last $1 changes",
+ "rcfilters-days-title": "Recent days",
+ "rcfilters-hours-title": "Recent hours",
+ "rcfilters-days-show-days": "$1 {{PLURAL:$1|day|days}}",
+ "rcfilters-days-show-hours": "$1 {{PLURAL:$1|hour|hours}}",
"rcfilters-quickfilters": "Saved filters",
"rcfilters-quickfilters-placeholder-title": "No links saved yet",
"rcfilters-quickfilters-placeholder-description": "To save your filter
settings and reuse them later, click the bookmark icon in the Active Filter
area, below.",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 7c995f0..a5d6d54 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -1540,6 +1540,12 @@
"recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with
a number for the legend.",
"recentchanges-submit": "Label for submit button in
[[Special:RecentChanges]]\n{{Identical|Show}}",
"rcfilters-activefilters": "Title for the filters selection showing the
active filters.",
+ "rcfilters-limit-title": "Title for the options to change the number of
results shown.",
+ "rcfilters-days-title": "Title for the options to change the number of
days for the results shown.",
+ "rcfilters-hours-title": "Title for the options to change the number of
hours for the results shown.",
+ "rcfilters-limit-shownum": "Title for the button that opens the
operation to control how many results are shown. \n\nParameters: $1 - Number of
results shown",
+ "rcfilters-days-show-days": "Title for the button that opens the
operation to control the day range for the results. \n\nParameters: $1 - Number
of days shown",
+ "rcfilters-days-show-hours": "Title for the button that opens the
operation to control the hour range for the results. \n\nParameters: $1 -
Number of hours shown",
"rcfilters-advancedfilters": "Title for the buttons allowing the user
to switch to the various advanced filters views.",
"rcfilters-quickfilters": "Label for the button that opens the saved
filter settings menu in [[Special:RecentChanges]]",
"rcfilters-quickfilters-placeholder-title": "Title for the text shown
in the quick filters menu on [[Special:RecentChanges]] if the user has not
saved any quick filters.",
diff --git a/resources/Resources.php b/resources/Resources.php
index a8cf91d..967ae6c 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1781,6 +1781,11 @@
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js',
@@ -1806,6 +1811,8 @@
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ViewSwitchWidget.less',
+
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less',
+
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
@@ -1825,6 +1832,12 @@
'messages' => [
'rcfilters-activefilters',
'rcfilters-advancedfilters',
+ 'rcfilters-limit-title',
+ 'rcfilters-limit-shownum',
+ 'rcfilters-days-title',
+ 'rcfilters-hours-title',
+ 'rcfilters-days-show-days',
+ 'rcfilters-days-show-hours',
'rcfilters-quickfilters',
'rcfilters-quickfilters-placeholder-title',
'rcfilters-quickfilters-placeholder-description',
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 2307f30..b061064 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
@@ -13,6 +13,8 @@
* is a part of.
* @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
+ * group from the URL, even if it wasn't initially set up.
* @cfg {string} [separator='|'] Value separator for 'string_options'
groups
* @cfg {boolean} [active] Group is active
* @cfg {boolean} [fullCoverage] This filters in this group
collectively cover all results
@@ -38,9 +40,11 @@
this.view = config.view || 'default';
this.title = config.title || name;
this.hidden = !!config.hidden;
+ this.allowArbitrary = !!config.allowArbitrary;
this.separator = config.separator || '|';
this.labelPrefixKey = config.labelPrefixKey;
+ this.currSelected = null;
this.active = !!config.active;
this.fullCoverage = !!config.fullCoverage;
@@ -75,7 +79,8 @@
* @param {string|Object} [groupDefault] Definition of the group default
*/
mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function (
filterDefinition, groupDefault ) {
- var supersetMap = {},
+ var defaultParam,
+ supersetMap = {},
model = this,
items = [];
@@ -159,11 +164,17 @@
} )
).join( this.getSeparator() );
} else if ( this.getType() === 'single_option' ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0
].getParamName();
+
// For this group, the parameter is the group name,
- // and a single item can be selected, or none at all
- // The item also must be recognized or none is set as
- // default
- model.defaultParams[ this.getName() ] =
this.getItemByParamName( groupDefault ) ? groupDefault : '';
+ // 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 );
}
};
@@ -175,20 +186,24 @@
*/
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function (
item ) {
// Update state
- var active = this.areAnySelected(),
- itemName = item && item.getName();
+ var active = this.areAnySelected();
- if ( item.isSelected() && this.getType() === 'single_option' ) {
- // Change the selection to only be the newly selected
item
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.getName() !== itemName ) {
- filterItem.toggleSelected( false );
- }
- } );
+ if (
+ item.isSelected() &&
+ this.getType() === 'single_option' &&
+ this.currSelected &&
+ this.currSelected !== item
+ ) {
+ this.currSelected.toggleSelected( false );
}
- if ( this.active !== active ) {
+ if (
+ this.active !== active ||
+ this.currSelected !== item
+ ) {
this.active = active;
+ this.currSelected = item;
+
this.emit( 'update' );
}
};
@@ -212,6 +227,15 @@
};
/**
+ * Get group allow arbitrary state
+ *
+ * @return {boolean} Group allows an arbitrary value from the URL
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
+ return this.allowArbitrary;
+ };
+
+ /**
* Get group name
*
* @return {string} Group name
@@ -229,6 +253,15 @@
return this.defaultParams;
};
+ /**
+ * This is for a single_option and string_options group types
+ * it returns the value of the default
+ *
+ * @return {string} Value of the default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function ()
{
+ return this.defaultParams[ this.getName() ];
+ };
/**
* Get the messags defining the 'whats this' popup for this group
*
@@ -499,7 +532,8 @@
* @return {Object} Filter representation
*/
mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation =
function ( paramRepresentation ) {
- var areAnySelected, paramValues,
+ var areAnySelected, paramValues, defaultValue, item,
+ oneWasSelected = false,
model = this,
paramToFilterMap = {},
result = {};
@@ -559,9 +593,10 @@
paramValues.indexOf(
filterItem.getParamName() ) > -1;
} );
} else if ( this.getType() === 'single_option' ) {
- // There is parameter that fits a single filter, or
none at all
+ // There is parameter that fits a single filter and if
not, get the default
this.getItems().forEach( function ( filterItem ) {
result[ filterItem.getName() ] =
filterItem.getParamName() === paramRepresentation;
+ oneWasSelected = oneWasSelected ||
filterItem.getParamName() === paramRepresentation;
} );
}
@@ -569,7 +604,22 @@
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
result[ filterItem.getName() ] = !!result[
filterItem.getName() ];
+ oneWasSelected = oneWasSelected || !!result[
filterItem.getName() ];
} );
+
+ // Make sure that at least one option is selected in
+ // single_option groups, no matter what path was taken
+ // If none was selected by the given definition, then
+ // we need to select the one in the base state -- either
+ // the default given, or the first item
+ if (
+ this.getType() === 'single_option' &&
+ !oneWasSelected
+ ) {
+ defaultValue = this.getDefaultParams();
+ item = this.getItemByParamName( defaultValue[
this.getName() ] );
+ result[ item.getName() ] = true;
+ }
return result;
};
@@ -587,6 +637,17 @@
};
/**
+ * Select an item by its parameter name
+ *
+ * @param {string} paramName Filter parameter name
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function
( paramName ) {
+ this.getItems().forEach( function ( item ) {
+ item.toggleSelected( item.getParamName() === paramName
);
+ } );
+ };
+
+ /**
* Get item by its parameter name
*
* @param {string} paramName Parameter name
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
index 5858566..4d0b803 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
@@ -32,7 +32,13 @@
views = {},
items = [],
uri = new mw.Uri(),
- $changesList = $( '.mw-changeslist'
).first().contents();
+ $changesList = $( '.mw-changeslist'
).first().contents(),
+ createFilterDataFromNumber = function ( num,
convertedNumForLabel ) {
+ return {
+ name: String( num ),
+ label: mw.language.convertNumber(
convertedNumForLabel )
+ };
+ };
// Prepare views
if ( namespaceStructure ) {
@@ -83,6 +89,99 @@
};
}
+ // Add parameter range operations
+ views.range = {
+ groups: [
+ {
+ name: 'limit',
+ type: 'single_option',
+ title: '', // Because it's a hidden
group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ validate: $.isNumeric,
+ sortFunc: function ( a, b ) { return
Number( a.name ) - Number( b.name ); },
+ 'default': '50',
+ filters: [ 50, 100, 250, 500 ].map(
function ( num ) {
+ return
createFilterDataFromNumber( num, num );
+ } )
+ },
+ {
+ name: 'days',
+ type: 'single_option',
+ title: '', // Because it's a hidden
group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ validate: $.isNumeric,
+ sortFunc: function ( a, b ) { return
Number( a.name ) - Number( b.name ); },
+ 'default': '7',
+ filters: [
+ // Hours (1, 2, 6, 12)
+ 0.04166, 0.0833, 0.25, 0.5,
+ // Days
+ 1, 3, 7, 14, 30
+ ].map( function ( num ) {
+ return
createFilterDataFromNumber(
+ num,
+ // Convert fractions of
days to number of hours for the labels
+ num < 1 ? Math.round(
num * 24 ) : num
+ );
+ } )
+ }
+ ]
+ };
+
+ // Before we do anything, we need to see if we require another
item in the
+ // groups that have 'AllowArbitrary'. For the moment, those are
only single_option
+ // groups; if we ever expand it, this might need further
generalization:
+ $.each( views, function ( viewName, viewData ) {
+ viewData.groups.forEach( function ( groupData ) {
+ // This is only true for single_option and
string_options
+ // We assume these are the only groups that
will allow for
+ // arbitrary, since it doesn't make any sense
for the other
+ // groups.
+ var uriValue = uri.query[ groupData.name ];
+
+ if (
+ // If the group allows for arbitrary
data
+ groupData.allowArbitrary &&
+ // and it is single_option (or
string_options, but we
+ // don't have cases of those yet, nor
do we plan to)
+ groupData.type === 'single_option' &&
+ // and if there is a valid value in the
URI already
+ uri.query[ groupData.name ] !==
undefined &&
+ // and, if there is a validate method
and it passes on
+ // the data
+ ( !groupData.validate ||
groupData.validate( uri.query[ groupData.name ] ) ) &&
+ // but if that value isn't already in
the definition
+ groupData.filters
+ .map( function ( filterData ) {
+ return filterData.name;
+ } )
+ .indexOf( uri.query[
groupData.name ] ) === -1
+ ) {
+ // Add the filter information
+ if ( groupData.name === 'days' ) {
+ // Specific fix for hours/days
which go by the same param
+ groupData.filters.push(
createFilterDataFromNumber(
+ uriValue,
+ // In this case we
don't want to round because it can be arbitrary
+ // weird numbers but we
want to round to 2 decimal digits
+ Number( uriValue ) < 1 ?
+ ( Number(
uriValue ) * 24 ).toFixed( 2 ) :
+ Number(
uriValue )
+ ) );
+ } else {
+ groupData.filters.push(
createFilterDataFromNumber( uriValue, uriValue ) );
+ }
+
+ // If there's a sort function set up,
re-sort the values
+ if ( groupData.sortFunc ) {
+ groupData.filters.sort(
groupData.sortFunc );
+ }
+ }
+ } );
+ } );
+
// Initialize the model
this.filtersModel.initializeFilters( filterStructure, views );
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
index b7852d0..b4ea8af 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
@@ -234,12 +234,22 @@
// wiki default.
// Any subsequent change of the URL through the RCFilters
// system will receive 'urlversion=2'
- var base = this.getVersion( uriQuery ) === 2 ?
- {} :
- this.filtersModel.getDefaultParams();
+ var hiddenParamDefaults = {},
+ 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, {}, base, uriQuery, { urlversion: '2' }
)
+ $.extend( true, {}, hiddenParamDefaults, base,
uriQuery, { urlversion: '2' } )
);
};
diff --git
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less
new file mode 100644
index 0000000..4155779
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less
@@ -0,0 +1,5 @@
+.mw-rcfilters-ui-datePopupWidget {
+ &-days {
+ margin-top: 1em;
+ }
+}
diff --git
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
index 5aa866d..df4592c 100644
---
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
+++
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
@@ -10,5 +10,14 @@
&-bottom {
margin-top: 1em;
+
+ .mw-rcfilters-ui-changesLimitButtonWidget,
+ .mw-rcfilters-ui-dateButtonWidget {
+ display: inline-block;
+
+ &:not( :first-child ) {
+ margin-left: 0.5em;
+ }
+ }
}
}
diff --git
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less
index 63ea264..e8f504a 100644
---
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less
+++
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less
@@ -1,4 +1,6 @@
.mw-rcfilters-ui-liveUpdateButtonWidget {
+ margin-left: 1em;
+
&.oo-ui-toggleWidget-on {
position: relative;
overflow: hidden;
diff --git
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less
new file mode 100644
index 0000000..38ad1ee
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less
@@ -0,0 +1,7 @@
+.mw-rcfilters-ui-valuePickerWidget {
+ &-title {
+ display: block;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+ }
+}
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
new file mode 100644
index 0000000..61ee4a5
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js
@@ -0,0 +1,107 @@
+( function ( mw ) {
+ /**
+ * Widget defining the button controlling the popup for the number of
results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for
popups
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget = function
MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitButtonWidget.parent.call( this,
config );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.button = null;
+ this.limitGroupModel = null;
+
+ this.model.connect( this, {
+ initialize: 'onModelInitialize'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitButtonWidget' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ChangesLimitButtonWidget, OO.ui.Widget
);
+
+ /**
+ * Respond to model initialize event
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onModelInitialize =
function () {
+ var changesLimitPopupWidget, selectedItem, currentValue;
+
+ this.limitGroupModel = this.model.getGroup( 'limit' );
+
+ // HACK: We need the model to be ready before we populate the
button
+ // and the widget, because we require the filter items for the
+ // limit and their events. This addition is only done after the
+ // model is initialized.
+ // Note: This will be fixed soon!
+ if ( this.limitGroupModel ) {
+ changesLimitPopupWidget = new
mw.rcfilters.ui.ChangesLimitPopupWidget(
+ this.limitGroupModel
+ );
+
+ selectedItem = this.limitGroupModel.getSelectedItems()[
0 ];
+ currentValue = ( selectedItem &&
selectedItem.getLabel() ) ||
+ mw.language.convertNumber(
this.limitGroupModel.getDefaultParamValue() );
+
+ this.button = new OO.ui.PopupButtonWidget( {
+ indicator: 'down',
+ label: mw.msg( 'rcfilters-limit-shownum',
currentValue ),
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ padded: true,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content:
changesLimitPopupWidget.$element
+ }
+ } );
+
+ // Events
+ this.limitGroupModel.connect( this, { update:
'onLimitGroupModelUpdate' } );
+ changesLimitPopupWidget.connect( this, { limit:
'onPopupLimit' } );
+
+ this.$element.append( this.button.$element );
+ }
+ };
+
+ /**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onPopupLimit =
function ( filterName ) {
+ this.controller.toggleFilterSelect( filterName, true );
+ };
+
+ /**
+ * Respond to limit choose event
+ *
+ * @param {string} filterName Filter name
+ */
+
mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onLimitGroupModelUpdate =
function () {
+ var item = this.limitGroupModel.getSelectedItems()[ 0 ],
+ label = item && item.getLabel();
+
+ // Update the label
+ if ( label ) {
+ this.button.setLabel( mw.msg(
'rcfilters-limit-shownum', label ) );
+ }
+ };
+
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
new file mode 100644
index 0000000..02101ab
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js
@@ -0,0 +1,47 @@
+( function ( mw ) {
+ /**
+ * Widget defining the popup to choose number of results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'limit'
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.ChangesLimitPopupWidget = function
MwRcfiltersUiChangesLimitPopupWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this,
config );
+
+ this.model = model;
+
+ this.valuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ label: mw.msg( 'rcfilters-limit-title' )
+ }
+ );
+
+ // Events
+ this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] }
);
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
+ .append( this.valuePicker.$element );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ChangesLimitPopupWidget, OO.ui.Widget
);
+
+ /* Events */
+
+ /**
+ * @event limit
+ * @param {string} name Item name
+ *
+ * A limit item was chosen
+ */
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
new file mode 100644
index 0000000..1569f38
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js
@@ -0,0 +1,115 @@
+( function ( mw ) {
+ /**
+ * Widget defining the button controlling the popup for the date range
for the results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for
popups
+ */
+ mw.rcfilters.ui.DateButtonWidget = function
MwRcfiltersUiDateButtonWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitButtonWidget.parent.call( this,
config );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.button = null;
+ this.daysGroupModel = null;
+
+ this.model.connect( this, {
+ initialize: 'onModelInitialize'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-dateButtonWidget' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.DateButtonWidget, OO.ui.Widget );
+
+ /**
+ * Respond to model initialize event
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onModelInitialize = function
() {
+ var datePopupWidget;
+
+ this.daysGroupModel = this.model.getGroup( 'days' );
+
+ // HACK: We need the model to be ready before we populate the
button
+ // and the widget, because we require the filter items for the
+ // limit and their events. This addition is only done after the
+ // model is initialized.
+ // Note: This will be fixed soon!
+ if ( this.daysGroupModel ) {
+ datePopupWidget = new mw.rcfilters.ui.DatePopupWidget(
+ this.daysGroupModel
+ );
+
+ this.button = new OO.ui.PopupButtonWidget( {
+ indicator: 'down',
+ icon: 'calendar',
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ padded: true,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content: datePopupWidget.$element
+ }
+ } );
+ this.updateButtonLabel();
+
+ // Events
+ this.daysGroupModel.connect( this, { update:
'onDaysGroupModelUpdate' } );
+ datePopupWidget.connect( this, { days: 'onPopupDays' }
);
+
+ this.$element.append( this.button.$element );
+ }
+ };
+
+ /**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onPopupDays = function (
filterName ) {
+ this.controller.toggleFilterSelect( filterName, true );
+ };
+
+ /**
+ * Respond to limit choose event
+ *
+ * @param {string} filterName Filter name
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onDaysGroupModelUpdate =
function () {
+ this.updateButtonLabel();
+ };
+
+ /**
+ * Update the button label
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.updateButtonLabel = function
() {
+ var item = this.daysGroupModel.getSelectedItems()[ 0 ];
+
+ // Update the label
+ if ( item ) {
+ this.button.setLabel(
+ mw.msg(
+ Number( item.getParamName() ) < 1 ?
+ 'rcfilters-days-show-hours' :
'rcfilters-days-show-days',
+ item.getLabel()
+ )
+ );
+ }
+ };
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
new file mode 100644
index 0000000..6971df5
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js
@@ -0,0 +1,61 @@
+( function ( mw ) {
+ /**
+ * Widget defining the popup to choose date for the results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.DatePopupWidget = function
MwRcfiltersUiDatePopupWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this,
config );
+
+ this.model = model;
+
+ this.hoursValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ classes: [
'mw-rcfilters-ui-datePopupWidget-hours' ],
+ label: mw.msg( 'rcfilters-hours-title' ),
+ itemFilter: function ( itemModel ) { return
Number( itemModel.getParamName() ) < 1; }
+ }
+ );
+ this.daysValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ classes: [
'mw-rcfilters-ui-datePopupWidget-days' ],
+ label: mw.msg( 'rcfilters-days-title' ),
+ itemFilter: function ( itemModel ) { return
Number( itemModel.getParamName() ) >= 1; }
+ }
+ );
+
+ // Events
+ this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days'
] } );
+ this.daysValuePicker.connect( this, { choose: [ 'emit', 'days'
] } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-datePopupWidget' )
+ .append(
+ this.hoursValuePicker.$element,
+ this.daysValuePicker.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.DatePopupWidget, OO.ui.Widget );
+
+ /* Events */
+
+ /**
+ * @event days
+ * @param {string} name Item name
+ *
+ * A days item was chosen
+ */
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
index 4bee31e..0198347 100644
---
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
@@ -385,6 +385,10 @@
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate
= function ( item ) {
+ if ( item.getGroupModel().isHidden() ) {
+ return;
+ }
+
if (
item.isSelected() ||
(
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
index a748063..883527f 100644
---
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
@@ -38,6 +38,22 @@
this.controller
);
+ this.numChangesWidget = new
mw.rcfilters.ui.ChangesLimitButtonWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
+ this.dateWidget = new mw.rcfilters.ui.DateButtonWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' );
@@ -56,7 +72,11 @@
}
$bottom = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom'
);
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom'
)
+ .append(
+ this.numChangesWidget.$element,
+ this.dateWidget.$element
+ );
if ( mw.config.get( 'wgStructuredChangeFiltersEnableLiveUpdate'
) ) {
$bottom.append( this.liveUpdateButton.$element );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
index dbee65c..6004e25 100644
--- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
@@ -138,8 +138,18 @@
this.$element.find( '.mw-tagfilter-label' ).closest(
'tr' ).detach();
}
+ // Hide limit and days
+ this.$element.find( '.rclinks' ).detach();
+
if ( !this.$element.find( '.mw-recentchanges-table tr' ).length
) {
+ this.$element.find( '.mw-recentchanges-table'
).detach();
this.$element.find( 'hr' ).detach();
}
+ if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
+ this.$element.find( '.rcshowhide' ).detach();
+ // If we're hiding rcshowhide, the '<br>'s are around
it,
+ // there's no need for them either.
+ this.$element.find( 'br' ).detach();
+ }
};
}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
new file mode 100644
index 0000000..7045ab6
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js
@@ -0,0 +1,109 @@
+( function ( mw ) {
+ /**
+ * Widget defining the behavior used to choose from a set of values
+ * in a single_value group
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model
+ * @param {Object} [config] Configuration object
+ * @cfg {Function} [itemFilter] A filter function for the items from the
+ * model. If not given, all items will be included. The function must
+ * handle item models and return a boolean whether the item is included
+ * or not. Example: function ( itemModel ) { return
itemModel.isSelected(); }
+ */
+ mw.rcfilters.ui.ValuePickerWidget = function
MwRcfiltersUiValuePickerWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ValuePickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ this.model = model;
+ this.itemFilter = config.itemFilter || function () { return
true; };
+
+ // Build the selection from the item models
+ this.selectWidget = new OO.ui.ButtonSelectWidget();
+ this.initializeSelectWidget();
+
+ // Events
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.selectWidget.connect( this, { choose:
'onSelectWidgetChoose' } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
+ .append(
+ this.$label
+ .addClass(
'mw-rcfilters-ui-valuePickerWidget-title' ),
+ this.selectWidget.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ValuePickerWidget, OO.ui.Widget );
+ OO.mixinClass( mw.rcfilters.ui.ValuePickerWidget,
OO.ui.mixin.LabelElement );
+
+ /* Events */
+
+ /**
+ * @event choose
+ * @param {string} name Item name
+ *
+ * An item has been chosen
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to model update event
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.onModelUpdate = function ()
{
+ this.selectCurrentModelItem();
+ };
+
+ /**
+ * Respond to select widget choose event
+ *
+ * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
+ * @fires choose
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.onSelectWidgetChoose =
function ( chosenItem ) {
+ this.emit( 'choose', chosenItem.getData() );
+ };
+
+ /**
+ * Initialize the select widget
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.initializeSelectWidget =
function () {
+ var items = this.model.getItems()
+ .filter( this.itemFilter )
+ .map( function ( filterItem ) {
+ return new OO.ui.ButtonOptionWidget( {
+ data: filterItem.getName(),
+ label: filterItem.getLabel()
+ } );
+ } );
+
+ this.selectWidget.clearItems();
+ this.selectWidget.addItems( items );
+
+ this.selectCurrentModelItem();
+ };
+
+ /**
+ * Select the current item that corresponds with the model item
+ * that is currently selected
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.selectCurrentModelItem =
function () {
+ var selectedItem = this.model.getSelectedItems()[ 0 ];
+
+ if ( selectedItem ) {
+ this.selectWidget.selectItemByData(
selectedItem.getName() );
+ }
+ };
+}( mediaWiki ) );
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 5212ee9..edaef79 100644
---
a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js
+++
b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js
@@ -57,11 +57,19 @@
}, {
name: 'group4',
type: 'single_option',
- default: 'option1',
+ default: 'option2',
filters: [
{ name: 'option1', label:
'group4option1-label', description: 'group4option1-desc' },
{ name: 'option2', label:
'group4option2-label', description: 'group4option2-desc' },
{ name: 'option3', label:
'group4option3-label', description: 'group4option3-desc' }
+ ]
+ }, {
+ 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' }
]
} ],
viewsDefinition = {
@@ -90,7 +98,8 @@
filter5: '1',
filter6: '0',
group3: 'filter8',
- group4: 'option1',
+ group4: 'option2',
+ group5: 'option1',
namespace: ''
},
baseParamRepresentation = {
@@ -101,7 +110,8 @@
filter5: '0',
filter6: '0',
group3: '',
- group4: '',
+ group4: 'option2',
+ group5: 'option1',
namespace: ''
},
baseFilterRepresentation = {
@@ -114,9 +124,14 @@
group3__filter7: false,
group3__filter8: false,
group3__filter9: false,
+ // The 'single_value' type of group can't have empty
value; it's either
+ // the default given or the first item that will get
the truthy value
group4__option1: false,
- group4__option2: false,
+ group4__option2: true, // Default
group4__option3: false,
+ group5__option1: true, // No default set, first item is
default value
+ group5__option2: false,
+ group5__option3: false,
namespace__0: false,
namespace__1: false,
namespace__2: false,
@@ -133,8 +148,11 @@
group3__filter8: { selected: false, conflicted: false,
included: false },
group3__filter9: { selected: false, conflicted: false,
included: false },
group4__option1: { selected: false, conflicted: false,
included: false },
- group4__option2: { selected: false, conflicted: false,
included: false },
+ group4__option2: { selected: true, conflicted: false,
included: false },
group4__option3: { selected: false, conflicted: false,
included: false },
+ group5__option1: { selected: true, conflicted: false,
included: false },
+ group5__option2: { selected: false, conflicted: false,
included: false },
+ group5__option3: { 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 },
@@ -557,7 +575,7 @@
assert.deepEqual(
model.getFiltersFromParameters( {} ),
baseFilterRepresentation,
- 'Empty parameter query results in an object
representing all filters set to false'
+ 'Empty parameter query results in an object
representing all filters set to their base state'
);
assert.deepEqual(
@@ -705,7 +723,8 @@
assert.deepEqual(
model.getSelectedState(),
$.extend( {}, baseFilterRepresentation, {
- group4__option1: true
+ group4__option1: true,
+ group4__option2: false
} ),
'A \'single_option\' parameter reflects a single
selected value.'
);
--
To view, visit https://gerrit.wikimedia.org/r/361186
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8068a7cc411eef40ddb8af4eef1d4f1e5f2a2b82
Gerrit-PatchSet: 23
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Jack Phoenix <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits