jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/378103 )
Change subject: RCLFilters: convert related changes tool to new UX
......................................................................
RCLFilters: convert related changes tool to new UX
Bug: T172161
Change-Id: I96af7ba583d03e6ff9833ac3b5f4b80cfd0ee626
---
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/dm/mw.rcfilters.dm.FiltersViewModel.js
M resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
M resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
A
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less
M resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js
A resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js
M tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
M tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
16 files changed, 502 insertions(+), 68 deletions(-)
Approvals:
jenkins-bot: Verified
Mooeypoo: Looks good to me, approved
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 9b1e04a..bea009b 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -1481,6 +1481,11 @@
"rcfilters-watchlist-showupdated": "Changes to pages you haven't
visited since the changes occurred are in <strong>bold</strong>, with solid
markers.",
"rcfilters-preference-label": "Hide the improved version of Recent
Changes",
"rcfilters-preference-help": "Rolls back the 2017 interface redesign
and all tools added then and since.",
+ "rcfilters-filter-showlinkedfrom-label": "Show changes on pages linked
from:",
+ "rcfilters-filter-showlinkedfrom-option-label": "Show changes on pages
linked FROM a page",
+ "rcfilters-filter-showlinkedto-label": "Show changes on pages linked
to:",
+ "rcfilters-filter-showlinkedto-option-label": "Show changes on pages
linked TO a page",
+ "rcfilters-target-page-placeholder": "Select a page",
"rcnotefrom": "Below {{PLURAL:$5|is the change|are the changes}} since
<strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
"rclistfromreset": "Reset date selection",
"rclistfrom": "Show new changes starting from $2, $3",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index 60cbed1..2e09de5 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -1676,6 +1676,11 @@
"rcfilters-watchlist-showupdated": "Message at the top of
[[Special:Watchlist]] when the Structured filters are enabled that describes
what unseen changes look like.\n\nCf. {{msg-mw|wlheader-showupdated}}",
"rcfilters-preference-label": "Option in RecentChanges tab of
[[Special:Preferences]].",
"rcfilters-preference-help": "Explanation for the option in the
RecentChanges tab of [[Special:Preferences]].",
+ "rcfilters-filter-showlinkedfrom-label": "Label that indicates that the
page is showing changes that link FROM the target page. Used on
[[Special:Recentchangeslinked]] when structured filters are enabled.",
+ "rcfilters-filter-showlinkedfrom-option-label": "Menu option to show
changes FROM the target page. Used on [[Special:Recentchangeslinked]] when
structured filters are enabled.",
+ "rcfilters-filter-showlinkedto-label": "Label that indicates that the
page is showing changes that link TO the target page. Used on
[[Special:Recentchangeslinked]] when structured filters are enabled.",
+ "rcfilters-filter-showlinkedto-option-label": "Menu option to show
changes TO the target page. Used on [[Special:Recentchangeslinked]] when
structured filters are enabled.",
+ "rcfilters-target-page-placeholder": "Placeholder text for the title
lookup [[Special:Recentchangeslinked]] when structured filters are enabled.",
"rcnotefrom": "This message is displayed at [[Special:RecentChanges]]
when viewing recentchanges from some specific time.\n\nThe corresponding
message is {{msg-mw|Rclistfrom}}.\n\nParameters:\n* $1 - the maximum number of
changes that are displayed\n* $2 - (Optional) a date and time\n* $3 - a date\n*
$4 - a time\n* $5 - Number of changes are displayed, for use with PLURAL",
"rclistfromreset": "Used on [[Special:RecentChanges]] to reset a
selection of a certain date range.",
"rclistfrom": "Used on [[Special:RecentChanges]]. Parameters:\n* $1 -
(Currently not use) date and time. The date and the time adds to the rclistfrom
description.\n* $2 - time. The time adds to the rclistfrom link description
(with split of date and time).\n* $3 - date. The date adds to the rclistfrom
link description (with split of date and time).\n\nThe corresponding message is
{{msg-mw|Rcnotefrom}}.",
diff --git a/resources/Resources.php b/resources/Resources.php
index 0665a2a..6b55ef9 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -1807,6 +1807,9 @@
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js',
+
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
@@ -1836,6 +1839,7 @@
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less',
+
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
],
'skinStyles' => [
@@ -1906,6 +1910,11 @@
'rcfilters-watchlist-markseen-button',
'rcfilters-watchlist-edit-watchlist-button',
'rcfilters-other-review-tools',
+ 'rcfilters-filter-showlinkedfrom-label',
+ 'rcfilters-filter-showlinkedfrom-option-label',
+ 'rcfilters-filter-showlinkedto-label',
+ 'rcfilters-filter-showlinkedto-option-label',
+ 'rcfilters-target-page-placeholder',
'blanknamespace',
'namespaces',
'tags-title',
@@ -1921,6 +1930,7 @@
'mediawiki.language',
'mediawiki.user',
'mediawiki.util',
+ 'mediawiki.widgets',
'mediawiki.rcfilters.filters.dm',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-moderation',
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 c6eb635..1950b93 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterGroup.js
@@ -150,6 +150,8 @@
// For this group type, parameter values are
direct
// We need to convert from a boolean to a
string ('1' and '0')
model.defaultParams[ filter.name ] = String(
Number( filter.default || 0 ) );
+ } else if ( model.getType() === 'any_value' ) {
+ model.defaultParams[ filter.name ] =
filter.default;
}
} );
@@ -578,7 +580,7 @@
if ( buildFromCurrentState ) {
// This means we have not been given a filter
representation
// so we are building one based on current state
- filterRepresentation[ item.getName() ] =
item.isSelected();
+ filterRepresentation[ item.getName() ] =
item.getValue();
} 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
@@ -598,7 +600,8 @@
// Build result
if (
this.getType() === 'send_unselected_if_any' ||
- this.getType() === 'boolean'
+ this.getType() === 'boolean' ||
+ this.getType() === 'any_value'
) {
// First, check if any of the items are selected at all.
// If none is selected, we're treating it as if they are
@@ -615,6 +618,8 @@
// Representation is straight-forward
and direct from
// the parameter value to the filter
state
result[ filterParamNames[ name ] ] =
String( Number( !!value ) );
+ } else if ( model.getType() === 'any_value' ) {
+ result[ filterParamNames[ name ] ] =
value;
}
} );
} else if ( this.getType() === 'string_options' ) {
@@ -665,7 +670,8 @@
paramRepresentation = paramRepresentation || {};
if (
this.getType() === 'send_unselected_if_any' ||
- this.getType() === 'boolean'
+ this.getType() === 'boolean' ||
+ this.getType() === 'any_value'
) {
// Go over param representation; map and check for
selections
this.getItems().forEach( function ( filterItem ) {
@@ -694,6 +700,8 @@
} else if ( model.getType() === 'boolean' ) {
// Straight-forward definition of state
result[ filterItem.getName() ] =
!!Number( paramRepresentation[ filterItem.getParamName() ] );
+ } else if ( model.getType() === 'any_value' ) {
+ result[ filterItem.getName() ] =
paramRepresentation[ filterItem.getParamName() ];
}
} );
} else if ( this.getType() === 'string_options' ) {
@@ -738,9 +746,9 @@
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
if ( result[ filterItem.getName() ] === undefined ) {
- result[ filterItem.getName() ] = false;
+ result[ filterItem.getName() ] =
this.getFalsyValue();
}
- } );
+ }.bind( this ) );
// Make sure that at least one option is selected in
// single_option groups, no matter what path was taken
@@ -763,6 +771,13 @@
};
/**
+ * @return {*} The appropriate falsy value for this group type
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getFalsyValue = function () {
+ return this.getType() === 'any_value' ? '' : false;
+ };
+
+ /**
* Get current selected state of all filter items in this group
*
* @return {Object} Selected state
diff --git
a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
index 4acbc55..8d22c23 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
@@ -385,7 +385,8 @@
$.each( this.groups, function ( group, groupModel ) {
if (
groupModel.getType() ===
'send_unselected_if_any' ||
- groupModel.getType() === 'boolean'
+ groupModel.getType() === 'boolean' ||
+ groupModel.getType() === 'any_value'
) {
// Individual filters
groupModel.getItems().forEach( function (
filterItem ) {
@@ -414,18 +415,18 @@
* @param {Object} params Parameters object
*/
mw.rcfilters.dm.FiltersViewModel.prototype.updateStateFromParams =
function ( params ) {
+ var filtersValue;
// For arbitrary numeric single_option values make sure the
values
// are normalized to fit within the limits
$.each( this.getFilterGroups(), function ( groupName,
groupModel ) {
params[ groupName ] =
groupModel.normalizeArbitraryValue( params[ groupName ] );
} );
- // Update filter states
- this.toggleFiltersSelected(
- this.getFiltersFromParameters(
- params
- )
- );
+ // Update filter values
+ filtersValue = this.getFiltersFromParameters( params );
+ Object.keys( filtersValue ).forEach( function ( filterName ) {
+ this.getItemByName( filterName ).setValue(
filtersValue[ filterName ] );
+ }.bind( this ) );
// Update highlight state
this.getItemsSupportingHighlights().forEach( function (
filterItem ) {
@@ -619,7 +620,7 @@
/**
* Get the current selected state of the filters
*
- * @param {boolean} onlySelected return an object containing only the
selected filters
+ * @param {boolean} [onlySelected] return an object containing only the
filters with a value
* @return {Object} Filters selected state
*/
mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function
( onlySelected ) {
@@ -628,8 +629,8 @@
result = {};
for ( i = 0; i < items.length; i++ ) {
- if ( !onlySelected || items[ i ].isSelected() ) {
- result[ items[ i ].getName() ] = items[ i
].isSelected();
+ if ( !onlySelected || items[ i ].getValue() ) {
+ result[ items[ i ].getName() ] = items[ i
].getValue();
}
}
@@ -739,7 +740,7 @@
// all filters (set to false)
this.getItems().forEach( function ( filterItem ) {
groupItemDefinition[ filterItem.getGroupName()
] = groupItemDefinition[ filterItem.getGroupName() ] || {};
- groupItemDefinition[ filterItem.getGroupName()
][ filterItem.getName() ] = !!filterDefinition[ filterItem.getName() ];
+ groupItemDefinition[ filterItem.getGroupName()
][ filterItem.getName() ] = filterItem.coerceValue( filterDefinition[
filterItem.getName() ] );
} );
}
diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
index 44b6c8c..d1e40ca 100644
--- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
+++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ItemModel.js
@@ -14,6 +14,7 @@
* with 'default' and 'inverted' as keys.
* @cfg {boolean} [active=true] The filter is active and affecting the
result
* @cfg {boolean} [selected] The item is selected
+ * @cfg {*} [value] The value of this item
* @cfg {string} [namePrefix='item_'] A prefix to add to the param name
to act as a unique
* identifier
* @cfg {string} [cssClass] The class identifying the results that
match this filter
@@ -34,7 +35,7 @@
this.label = config.label || this.name;
this.labelPrefixKey = config.labelPrefixKey;
this.description = config.description || '';
- this.selected = !!config.selected;
+ this.setValue( config.value || config.selected );
this.identifiers = config.identifiers || [];
@@ -151,7 +152,7 @@
* @return {boolean} Filter is selected
*/
mw.rcfilters.dm.ItemModel.prototype.isSelected = function () {
- return this.selected;
+ return !!this.value;
};
/**
@@ -161,10 +162,38 @@
* @fires update
*/
mw.rcfilters.dm.ItemModel.prototype.toggleSelected = function (
isSelected ) {
- isSelected = isSelected === undefined ? !this.selected :
isSelected;
+ isSelected = isSelected === undefined ? !this.isSelected() :
isSelected;
+ this.setValue( isSelected );
+ };
- if ( this.selected !== isSelected ) {
- this.selected = isSelected;
+ /**
+ * Get the value
+ *
+ * @return {*}
+ */
+ mw.rcfilters.dm.ItemModel.prototype.getValue = function () {
+ return this.value;
+ };
+
+ /**
+ * Convert a given value to the appropriate representation based on
group type
+ *
+ * @param {*} value
+ * @return {*}
+ */
+ mw.rcfilters.dm.ItemModel.prototype.coerceValue = function ( value ) {
+ return this.getGroupModel().getType() === 'any_value' ? value :
!!value;
+ };
+
+ /**
+ * Set the value
+ *
+ * @param {*} newValue
+ */
+ mw.rcfilters.dm.ItemModel.prototype.setValue = function ( newValue ) {
+ newValue = this.coerceValue( newValue );
+ if ( this.value !== newValue ) {
+ this.value = newValue;
this.emit( 'update' );
}
};
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
index 0bb6acf..ba54755 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
@@ -185,6 +185,37 @@
]
};
+ views.recentChangesLinked = {
+ groups: [
+ {
+ name: 'page',
+ type: 'any_value',
+ title: '',
+ hidden: true,
+ isSticky: false,
+ filters: [
+ {
+ name: 'target',
+ 'default': ''
+ }
+ ]
+ },
+ {
+ name: 'toOrFrom',
+ type: 'boolean',
+ title: '',
+ hidden: true,
+ isSticky: false,
+ filters: [
+ {
+ name: 'showlinkedto',
+ 'default': false
+ }
+ ]
+ }
+ ]
+ };
+
// Before we do anything, we need to see if we require
additional items 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:
@@ -521,6 +552,33 @@
};
/**
+ * Set the value of the 'showlinkedto' parameter
+ * @param {boolean} value
+ */
+ mw.rcfilters.Controller.prototype.setShowLinkedTo = function ( value ) {
+ var targetItem = this.filtersModel.getGroup( 'page'
).getItemByParamName( 'target' ),
+ showLinkedToItem = this.filtersModel.getGroup(
'toOrFrom' ).getItemByParamName( 'showlinkedto' );
+
+ this.filtersModel.toggleFilterSelected(
showLinkedToItem.getName(), value );
+ this.uriProcessor.updateURL();
+ // reload the results only when target is set
+ if ( targetItem.getValue() ) {
+ this.updateChangesList();
+ }
+ };
+
+ /**
+ * Set the target page
+ * @param {string} page
+ */
+ mw.rcfilters.Controller.prototype.setTargetPage = function ( page ) {
+ var targetItem = this.filtersModel.getGroup( 'page'
).getItemByParamName( 'target' );
+ targetItem.setValue( page );
+ this.uriProcessor.updateURL();
+ this.updateChangesList();
+ };
+
+ /**
* Set the highlight color for a filter item
*
* @param {string} filterName Name of the filter item
@@ -850,7 +908,7 @@
mw.rcfilters.Controller.prototype.updateStateFromUrl = function (
fetchChangesList ) {
fetchChangesList = fetchChangesList === undefined ? true :
!!fetchChangesList;
- this.uriProcessor.updateModelBasedOnQuery( new mw.Uri().query );
+ this.uriProcessor.updateModelBasedOnQuery();
// Update the sticky preferences, in case we received a value
// from the URL
@@ -1000,10 +1058,11 @@
};
}
- $parsed = $( '<div>' ).append( $(
$.parseHTML( data.content ) ) );
+ $parsed = $( '<div>' ).append( $(
$.parseHTML(
+ data ? data.content : ''
+ ) ) );
return this._extractChangesListInfo(
$parsed );
-
}.bind( this )
);
};
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
index 0392f34..3e1191f 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
@@ -57,38 +57,20 @@
/**
* Get an updated mw.Uri object based on the model state
*
- * @param {Object} [uriQuery] An external URI query to build the new uri
- * with. This is mainly for tests, to be able to supply external
parameters
- * and make sure they are retained.
+ * @param {mw.Uri} [uri] An external URI to build the new uri
+ * with. This is mainly for tests, to be able to supply external query
+ * parameters and make sure they are retained.
* @return {mw.Uri} Updated Uri
*/
- mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uriQuery
) {
- var titlePieces,
- uri = new mw.Uri(),
- unrecognizedParams = this.getUnrecognizedParams(
uriQuery || uri.query );
+ mw.rcfilters.UriProcessor.prototype.getUpdatedUri = function ( uri ) {
+ var normalizedUri = this._normalizeTargetInUri( uri || new
mw.Uri() ),
+ unrecognizedParams = this.getUnrecognizedParams(
normalizedUri.query );
- if ( uriQuery ) {
- // This is mainly for tests, to be able to give the
method
- // an initial URI Query and test that it retains
parameters
- uri.query = uriQuery;
- }
-
- // Normalize subpage to use &target= so we are always
- // consistent in Special:RecentChangesLinked between the
- // ?title=Special:RecentChangesLinked/TargetPage and
- // ?title=Special:RecentChangesLinked&target=TargetPage
- if ( uri.query.title && uri.query.title.indexOf( '/' ) !== -1 )
{
- titlePieces = uri.query.title.split( '/' );
-
- unrecognizedParams.title = titlePieces.shift();
- unrecognizedParams.target = titlePieces.join( '/' );
- }
-
- uri.query = this.filtersModel.getMinimizedParamRepresentation(
+ normalizedUri.query =
this.filtersModel.getMinimizedParamRepresentation(
$.extend(
true,
{},
- uri.query,
+ normalizedUri.query,
// The representation must be expanded so it can
// override the uri query params but we then
output
// a minimized version for the entire URI
representation
@@ -98,7 +80,44 @@
);
// Reapply unrecognized params and url version
- uri.query = $.extend( true, {}, uri.query, unrecognizedParams,
{ urlversion: '2' } );
+ normalizedUri.query = $.extend(
+ true,
+ {},
+ normalizedUri.query,
+ unrecognizedParams,
+ { urlversion: '2' }
+ );
+
+ return normalizedUri;
+ };
+
+ /**
+ * Move the subpage to the target parameter
+ *
+ * @param {mw.Uri} uri
+ * @return {mw.Uri}
+ * @private
+ */
+ mw.rcfilters.UriProcessor.prototype._normalizeTargetInUri = function (
uri ) {
+ var parts,
+ re = /^((?:\/.+\/)?.+:.+)\/(.+)$/; // matches
[namespace:]Title/Subpage
+
+ // target in title param
+ if ( uri.query.title ) {
+ parts = uri.query.title.match( re );
+ if ( parts ) {
+ uri.query.title = parts[ 1 ];
+ uri.query.target = parts[ 2 ];
+ }
+ }
+
+ // target in path
+ parts = uri.path.match( re );
+ if ( parts ) {
+ uri.path = parts[ 1 ];
+ uri.query.target = parts[ 2 ];
+ }
+
return uri;
};
@@ -154,15 +173,16 @@
* we consider the system synchronized, and the model serves
* as the source of truth for the URL.
*
- * This methods should only be called once on initialiation.
+ * This methods should only be called once on initialization.
* After initialization, the model updates the URL, not the
* other way around.
*
* @param {Object} [uriQuery] URI query
*/
mw.rcfilters.UriProcessor.prototype.updateModelBasedOnQuery = function
( uriQuery ) {
+ uriQuery = uriQuery || this._normalizeTargetInUri( new mw.Uri()
).query;
this.filtersModel.updateStateFromParams(
- this._getNormalizedQueryParams( uriQuery || new
mw.Uri().query )
+ this._getNormalizedQueryParams( uriQuery )
);
};
diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
index 10bbcf6..6ec1200 100644
--- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
+++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
@@ -9,9 +9,8 @@
*/
init: function () {
var $topLinks,
- rcTopSection,
+ topSection,
$watchlistDetails,
- wlTopSection,
namespaces,
savedQueriesPreferenceName = mw.config.get(
'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
daysPreferenceName = mw.config.get(
'wgStructuredChangeFiltersDaysPreferenceName' ),
@@ -75,25 +74,33 @@
controller.replaceUrl();
- if ( specialPage === 'Recentchanges' ||
- specialPage === 'Recentchangeslinked' ) {
+ if ( specialPage === 'Recentchanges' ) {
$topLinks = $( '.mw-recentchanges-toplinks'
).detach();
- rcTopSection = new
mw.rcfilters.ui.RcTopSectionWidget(
+ topSection = new
mw.rcfilters.ui.RcTopSectionWidget(
savedLinksListWidget, $topLinks
);
- filtersWidget.setTopSection(
rcTopSection.$element );
- } // end Special:RC
+ filtersWidget.setTopSection(
topSection.$element );
+ } // end Recentchanges
+
+ if ( specialPage === 'Recentchangeslinked' ) {
+ topSection = new
mw.rcfilters.ui.RclTopSectionWidget(
+ savedLinksListWidget, controller,
+ filtersModel.getGroup( 'toOrFrom'
).getItemByParamName( 'showlinkedto' ),
+ filtersModel.getGroup( 'page'
).getItemByParamName( 'target' )
+ );
+ filtersWidget.setTopSection(
topSection.$element );
+ } // end Recentchangeslinked
if ( specialPage === 'Watchlist' ) {
$( '#contentSub, form#mw-watchlist-resetbutton'
).detach();
$watchlistDetails = $( '.watchlistDetails'
).detach().contents();
- wlTopSection = new
mw.rcfilters.ui.WatchlistTopSectionWidget(
+ topSection = new
mw.rcfilters.ui.WatchlistTopSectionWidget(
controller, changesListModel,
savedLinksListWidget, $watchlistDetails
);
- filtersWidget.setTopSection(
wlTopSection.$element );
- } // end Special:WL
+ filtersWidget.setTopSection(
topSection.$element );
+ } // end Watchlist
/**
* Fired when initialization of the filtering interface
for changes list is complete.
diff --git
a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less
new file mode 100644
index 0000000..577c254
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RclToOrFromWidget.less
@@ -0,0 +1,11 @@
+.mw-rcfilters-ui-rclToOrFromWidget {
+ min-width: 340px;
+
+ // need to be very specific to override bg-color
+ &.oo-ui-dropdownWidget.oo-ui-widget-enabled {
+ .oo-ui-dropdownWidget-handle {
+ border: 0;
+ background-color: transparent;
+ }
+ }
+}
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 6aa335a..237a635 100644
--- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
@@ -133,6 +133,9 @@
this.$element.find( '.namespaceForm' ).detach();
this.$element.find( '.mw-tagfilter-label' ).closest( 'tr'
).detach();
+ // Hide Related Changes page name form
+ this.$element.find( '.targetForm' ).detach();
+
// misc: limit, days, watchlist info msg
this.$element.find( '.rclinks, .cldays, .wlinfo' ).detach();
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
new file mode 100644
index 0000000..d14681b
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTargetPageWidget.js
@@ -0,0 +1,73 @@
+( function ( mw ) {
+ /**
+ * Widget to select and display target page on
Special:RecentChangesLinked (AKA Related Changes)
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} targetPageModel
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.RclTargetPageWidget = function
MwRcfiltersUiRclTargetPageWidget(
+ controller, targetPageModel, config
+ ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.RclTargetPageWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = targetPageModel;
+
+ this.titleSearch = new mw.widgets.TitleInputWidget( {
+ validate: false,
+ placeholder: mw.msg(
'rcfilters-target-page-placeholder' )
+ } );
+
+ // Events
+ this.model.connect( this, { update: 'updateUiBasedOnModel' } );
+
+ this.titleSearch.$input.on( {
+ blur: this.onLookupInputBlur.bind( this )
+ } );
+
+ this.titleSearch.lookupMenu.connect( this, {
+ choose: 'onLookupMenuItemChoose'
+ } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rclTargetPageWidget' )
+ .append( this.titleSearch.$element );
+
+ this.updateUiBasedOnModel();
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.RclTargetPageWidget, OO.ui.Widget );
+
+ /* Methods */
+
+ /**
+ * Respond to the user choosing a title
+ */
+ mw.rcfilters.ui.RclTargetPageWidget.prototype.onLookupMenuItemChoose =
function () {
+ this.titleSearch.$input.blur();
+ };
+
+ /**
+ * Respond to titleSearch $input blur
+ */
+ mw.rcfilters.ui.RclTargetPageWidget.prototype.onLookupInputBlur =
function () {
+ this.controller.setTargetPage( this.titleSearch.getQueryValue()
);
+ };
+
+ /**
+ * Respond to the model being updated
+ */
+ mw.rcfilters.ui.RclTargetPageWidget.prototype.updateUiBasedOnModel =
function () {
+ this.titleSearch.setValue( this.model.getValue() );
+ };
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js
new file mode 100644
index 0000000..e91fe9b
--- /dev/null
+++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclToOrFromWidget.js
@@ -0,0 +1,73 @@
+( function ( mw ) {
+ /**
+ * Widget to select to view changes that link TO or FROM the target page
+ * on Special:RecentChangesLinked (AKA Related Changes)
+ *
+ * @extends OO.ui.DropdownWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel model this
widget is bound to
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.RclToOrFromWidget = function
MwRcfiltersUiRclToOrFromWidget(
+ controller, showLinkedToModel, config
+ ) {
+ config = config || {};
+
+ this.showLinkedFrom = new OO.ui.MenuOptionWidget( {
+ data: 'from', // default (showlinkedto=0)
+ label: mw.msg(
'rcfilters-filter-showlinkedfrom-option-label' )
+ } );
+ this.showLinkedTo = new OO.ui.MenuOptionWidget( {
+ data: 'to', // showlinkedto=1
+ label: mw.msg(
'rcfilters-filter-showlinkedto-option-label' )
+ } );
+
+ // Parent
+ mw.rcfilters.ui.RclToOrFromWidget.parent.call( this, $.extend( {
+ classes: [ 'mw-rcfilters-ui-rclToOrFromWidget' ],
+ menu: { items: [ this.showLinkedFrom, this.showLinkedTo
] }
+ }, config ) );
+
+ this.controller = controller;
+ this.model = showLinkedToModel;
+
+ this.getMenu().connect( this, { choose: 'onUserChooseItem' } );
+ this.model.connect( this, { update: 'onModelUpdate' } );
+
+ // force an initial update of the component based on the state
+ this.onModelUpdate();
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.RclToOrFromWidget,
OO.ui.DropdownWidget );
+
+ /* Methods */
+
+ /**
+ * Respond to the user choosing an item in the menu
+ *
+ * @param {OO.ui.MenuOptionWidget} chosenItem
+ */
+ mw.rcfilters.ui.RclToOrFromWidget.prototype.onUserChooseItem = function
( chosenItem ) {
+ this.controller.setShowLinkedTo( chosenItem.getData() === 'to'
);
+ };
+
+ /**
+ * Respond to model update
+ */
+ mw.rcfilters.ui.RclToOrFromWidget.prototype.onModelUpdate = function ()
{
+ this.getMenu().selectItem(
+ this.model.isSelected() ?
+ this.showLinkedTo :
+ this.showLinkedFrom
+ );
+ this.setLabel( mw.msg(
+ this.model.isSelected() ?
+ 'rcfilters-filter-showlinkedto-label' :
+ 'rcfilters-filter-showlinkedfrom-label'
+ ) );
+ };
+}( mediaWiki ) );
diff --git
a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js
new file mode 100644
index 0000000..2fdf365
--- /dev/null
+++
b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RclTopSectionWidget.js
@@ -0,0 +1,66 @@
+( function ( mw ) {
+ /**
+ * Top section (between page title and filters) on
Special:RecentChangesLinked (AKA RelatedChanges)
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+ * @param {mw.rcfilters.Controller} controller
+ * @param {mw.rcfilters.dm.FilterItem} showLinkedToModel Model for
'showlinkedto' parameter
+ * @param {mw.rcfilters.dm.FilterItem} targetPageModel Model for
'target' parameter
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.RclTopSectionWidget = function
MwRcfiltersUiRclTopSectionWidget(
+ savedLinksListWidget, controller, showLinkedToModel,
targetPageModel, config
+ ) {
+ var toOrFromWidget,
+ targetPage;
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.RclTopSectionWidget.parent.call( this, config );
+
+ this.controller = controller;
+
+ toOrFromWidget = new mw.rcfilters.ui.RclToOrFromWidget(
controller, showLinkedToModel );
+ targetPage = new mw.rcfilters.ui.RclTargetPageWidget(
controller, targetPageModel );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-rclTopSectionWidget' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass(
'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+
.addClass( 'mw-rcfilters-ui-cell' )
+
.append( toOrFromWidget.$element )
+ ),
+ $( '<div>' )
+ .addClass(
'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+
.addClass( 'mw-rcfilters-ui-cell' )
+
.append( targetPage.$element ),
+ $( '<div>' )
+
.addClass( 'mw-rcfilters-ui-table-placeholder' )
+
.addClass( 'mw-rcfilters-ui-cell' ),
+
!mw.user.isAnon() ?
+ $(
'<div>' )
+
.addClass( 'mw-rcfilters-ui-cell' )
+
.addClass( 'mw-rcfilters-ui-rclTopSectionWidget-savedLinks' )
+
.append( savedLinksListWidget.$element ) :
+ null
+ )
+ )
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.RclTopSectionWidget, OO.ui.Widget );
+}( mediaWiki ) );
diff --git
a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
index e106b12..674bf07 100644
--- a/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
+++ b/tests/qunit/suites/resources/mediawiki.rcfilters/UriProcessor.test.js
@@ -60,19 +60,24 @@
QUnit.test( 'getUpdatedUri', function ( assert ) {
var uriProcessor,
- filtersModel = new mw.rcfilters.dm.FiltersViewModel();
+ filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
+ makeUri = function ( queryParams ) {
+ var uri = new mw.Uri();
+ uri.query = queryParams;
+ return uri;
+ };
filtersModel.initializeFilters( mockFilterStructure );
uriProcessor = new mw.rcfilters.UriProcessor( filtersModel );
assert.deepEqual(
- ( uriProcessor.getUpdatedUri( {} ) ).query,
+ ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query,
{ urlversion: '2' },
'Empty model state with empty uri state, assumes the
given uri is already normalized, and adds urlversion=2'
);
assert.deepEqual(
- ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query,
+ ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } )
) ).query,
{ urlversion: '2', foo: 'bar' },
'Empty model state with unrecognized params retains
unrecognized params'
);
@@ -84,13 +89,13 @@
} );
assert.deepEqual(
- ( uriProcessor.getUpdatedUri( {} ) ).query,
+ ( uriProcessor.getUpdatedUri( makeUri( {} ) ) ).query,
{ urlversion: '2', filter2: '1', group3: 'filter5' },
'Model state is reflected in the updated URI'
);
assert.deepEqual(
- ( uriProcessor.getUpdatedUri( { foo: 'bar' } ) ).query,
+ ( uriProcessor.getUpdatedUri( makeUri( { foo: 'bar' } )
) ).query,
{ urlversion: '2', filter2: '1', group3: 'filter5',
foo: 'bar' },
'Model state is reflected in the updated URI with
existing uri params'
);
@@ -272,4 +277,38 @@
} );
} );
+ QUnit.test( '_normalizeTargetInUri', function ( assert ) {
+ var uriProcessor = new mw.rcfilters.UriProcessor( null ),
+ cases = [
+ {
+ input:
'http://host/wiki/Special:RecentChangesLinked/Moai',
+ output:
'http://host/wiki/Special:RecentChangesLinked?target=Moai',
+ message: 'Target as subpage in path'
+ },
+ {
+ input:
'http://host/wiki/Special:RecentChangesLinked/Category:Foo',
+ output:
'http://host/wiki/Special:RecentChangesLinked?target=Category:Foo',
+ message: 'Target as subpage in path
(with namespace)'
+ },
+ {
+ input:
'http://host/w/index.php?title=Special:RecentChangesLinked/Moai',
+ output:
'http://host/w/index.php?title=Special:RecentChangesLinked&target=Moai',
+ message: 'Target as subpage in title
param'
+ },
+ {
+ input:
'http://host/wiki/Special:Watchlist',
+ output:
'http://host/wiki/Special:Watchlist',
+ message: 'No target specified'
+ }
+ ];
+
+ cases.forEach( function ( testCase ) {
+ assert.equal(
+ uriProcessor._normalizeTargetInUri( new mw.Uri(
testCase.input ) ).toString(),
+ new mw.Uri( testCase.output ).toString(),
+ testCase.message
+ );
+ } );
+ } );
+
}( mediaWiki, jQuery ) );
diff --git
a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
index 271648f..18a2c9c 100644
--- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
+++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FilterItem.test.js
@@ -184,4 +184,22 @@
'Events emitted successfully.'
);
} );
+
+ QUnit.test( 'get/set boolean value', function ( assert ) {
+ var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type:
'boolean' } ),
+ item = new mw.rcfilters.dm.FilterItem( 'filter1', group
);
+
+ item.setValue( '1' );
+
+ assert.equal( item.getValue(), true, 'Value is coerced to
boolean' );
+ } );
+
+ QUnit.test( 'get/set any value', function ( assert ) {
+ var group = new mw.rcfilters.dm.FilterGroup( 'group1', { type:
'any_value' } ),
+ item = new mw.rcfilters.dm.FilterItem( 'filter1', group
);
+
+ item.setValue( '1' );
+
+ assert.equal( item.getValue(), '1', 'Value is kept as-is' );
+ } );
}( mediaWiki ) );
--
To view, visit https://gerrit.wikimedia.org/r/378103
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I96af7ba583d03e6ff9833ac3b5f4b80cfd0ee626
Gerrit-PatchSet: 13
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Sbisson <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
Gerrit-Reviewer: Sbisson <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits