"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",
"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}}.",
'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',
'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' => [
'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',
'mediawiki.language',
'mediawiki.user',
'mediawiki.util',
+ 'mediawiki.widgets',
'mediawiki.rcfilters.filters.dm',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-moderation',
// 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;
}
} );
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
// 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
// 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' ) {
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 ) {
} 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' ) {
// 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
return result;
};
+ /**
+ * @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
*
$.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 ) {
* @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 ) {
/**
* 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 ) {
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();
}
}
// 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() ] );
} );
}
* 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
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 || [];
* @return {boolean} Filter is selected
*/
mw.rcfilters.dm.ItemModel.prototype.isSelected = function () {
- return this.selected;
+ return !!this.value;
};
/**
* @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 );
+ };
+
+ /**
+ * 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;
+ };
- if ( this.selected !== isSelected ) {
- this.selected = isSelected;
+ /**
+ * 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' );
}
};
]
};
+ 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:
}
};
+ /**
+ * 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
*
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
};
}
- $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) );
+ $parsed = $( '<div>' ).append( $( $.parseHTML(
+ data ? data.content : ''
+ ) ) );
return this._extractChangesListInfo( $parsed );
-
}.bind( this )
);
};
/**
* 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
);
// 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;
};
* 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 )
);
};
*/
init: function () {
var $topLinks,
- rcTopSection,
+ topSection,
$watchlistDetails,
- wlTopSection,
namespaces,
savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
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.
--- /dev/null
+.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;
+ }
+ }
+}
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();
--- /dev/null
+( 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 ) );
--- /dev/null
+( 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 ) );
--- /dev/null
+( 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 ) );
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'
);
} );
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'
);
} );
} );
+ 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 ) );
'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 ) );