$defaultPreferences['rcfilters-wl-saved-queries'] = [
'type' => 'api',
];
- $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
+ // Override RCFilters preferences for RecentChanges 'limit'
+ $defaultPreferences['rcfilters-limit'] = [
'type' => 'api',
];
- $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
+ $defaultPreferences['rcfilters-saved-queries-versionbackup'] = [
'type' => 'api',
];
- $defaultPreferences['rcfilters-rclimit'] = [
+ $defaultPreferences['rcfilters-wl-saved-queries-versionbackup'] = [
'type' => 'api',
];
-
if ( $config->get( 'RCWatchCategoryMembership' ) ) {
$defaultPreferences['hidecategorization'] = [
'type' => 'toggle',
$formData[$pref] = $user->getOption( $pref, null, true );
}
+ // If the user changed the rclimit preference, also change the rcfilters-rclimit preference
+ if (
+ isset( $formData['rclimit'] ) &&
+ intval( $formData[ 'rclimit' ] ) !== $user->getIntOption( 'rclimit' )
+ ) {
+ $formData['rcfilters-limit'] = $formData['rclimit'];
+ }
+
// Keep old preferences from interfering due to back-compat code, etc.
$user->resetOptions( 'unused', $form->getContext() );
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
}
// Apply change tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( $tagStatus->isOK() ) {
$requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
] );
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getUser() );
if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
}
$changes = [];
- if ( count( $params['change'] ) ) {
+ if ( $params['change'] ) {
foreach ( $params['change'] as $entry ) {
$array = explode( '=', $entry, 2 );
$changes[$array[0]] = isset( $array[1] ) ? $array[1] : null;
}
// Page protection filtering
- if ( count( $params['prtype'] ) || $params['prexpiry'] != 'all' ) {
+ if ( $params['prtype'] || $params['prexpiry'] != 'all' ) {
$this->addTables( 'page_restrictions' );
$this->addWhere( 'page_id=pr_page' );
$this->addWhere( "pr_expiry > {$db->addQuotes( $db->timestamp() )} OR pr_expiry IS NULL" );
- if ( count( $params['prtype'] ) ) {
+ if ( $params['prtype'] ) {
$this->addWhereFld( 'pr_type', $params['prtype'] );
if ( isset( $params['prlevel'] ) ) {
if ( count( $this->cont ) >= 2 ) {
$op = $this->params['dir'] == 'descending' ? '<' : '>';
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$this->addWhere(
"{$this->bl_from_ns} $op {$this->cont[0]} OR " .
"({$this->bl_from_ns} = {$this->cont[0]} AND " .
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
$sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
$orderBy = [];
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
$where = "{$this->bl_from} $op= {$this->cont[5]}";
// Don't bother with namespace, title, or from_namespace if it's
// otherwise constant in the where clause.
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$where = "{$this->bl_from_ns} $op {$this->cont[4]} OR " .
"({$this->bl_from_ns} = {$this->cont[4]} AND ($where))";
}
if ( count( $allRedirDBkey ) > 1 ) {
$orderBy[] = $this->bl_title . $sort;
}
- if ( count( $this->params['namespace'] ) > 1 ) {
+ if ( $this->params['namespace'] !== null && count( $this->params['namespace'] ) > 1 ) {
$orderBy[] = $this->bl_from_ns . $sort;
}
$orderBy[] = $this->bl_from . $sort;
}
} else {
$this->addWhereFld( "{$p}_from_namespace", $params['namespace'] );
- if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) {
+ if ( !empty( $settings['from_namespace'] )
+ && $params['namespace'] !== null && count( $params['namespace'] ) > 1
+ ) {
$sortby["{$p}_from_namespace"] = 'int';
}
}
* @param string|string[] $value Value; ignored if null or empty array;
*/
protected function addWhereFld( $field, $value ) {
- // Use count() to its full documented capabilities to simultaneously
- // test for null, empty array or empty countable object
- if ( count( $value ) ) {
+ if ( $value !== null && count( $value ) ) {
$this->where[$field] = $value;
}
}
// how to have efficient subcategory access :-) ~~~~ (oh well, domas)
$miser_ns = [];
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $miser_ns = $params['namespace'];
+ $miser_ns = $params['namespace'] ?: [];
} else {
$this->addWhereFld( 'page_namespace', $params['namespace'] );
}
$miser_ns = [];
if ( $this->getConfig()->get( 'MiserMode' ) ) {
- $miser_ns = $params['namespace'];
+ $miser_ns = $params['namespace'] ?: [];
} else {
$this->addWhereFld( 'page_namespace', $params['namespace'] );
}
}
} elseif ( $params['namespace'] ) {
$this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] );
- $multiNS = count( $params['namespace'] ) !== 1;
+ $multiNS = $params['namespace'] === null || count( $params['namespace'] ) !== 1;
}
if ( !is_null( $params['continue'] ) ) {
}
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$ableToTag->isOK() ) {
$this->dieStatus( $ableToTag );
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
// If change tagging was requested, check that the user is allowed to tag,
// and the tags are valid
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$tagStatus->isOK() ) {
$this->dieStatus( $tagStatus );
}
// Check if user can add tags
- if ( count( $params['tags'] ) ) {
+ if ( $params['tags'] ) {
$ableToTag = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
if ( !$ableToTag->isOk() ) {
$this->dieStatus( $ableToTag );
} else {
$expiry = [ 'infinity' ];
}
- if ( count( $expiry ) !== count( $params['add'] ) ) {
+ $add = (array)$params['add'];
+ if ( count( $expiry ) !== count( $add ) ) {
if ( count( $expiry ) === 1 ) {
- $expiry = array_fill( 0, count( $params['add'] ), $expiry[0] );
+ $expiry = array_fill( 0, count( $add ), $expiry[0] );
} else {
$this->dieWithError( [
'apierror-toofewexpiries',
count( $expiry ),
- count( $params['add'] )
+ count( $add )
] );
}
}
// Validate the expiries
$groupExpiries = [];
foreach ( $expiry as $index => $expiryValue ) {
- $group = $params['add'][$index];
+ $group = $add[$index];
$groupExpiries[$group] = UserrightsPage::expiryToTimestamp( $expiryValue );
if ( $groupExpiries[$group] === false ) {
$r['user'] = $user->getName();
$r['userid'] = $user->getId();
list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups(
- $user, (array)$params['add'], (array)$params['remove'],
+ $user, (array)$add, (array)$params['remove'],
$params['reason'], $tags, $groupExpiries
);
*/
protected static $savedQueriesPreferenceName;
+ /**
+ * Preference name for 'days'. Subclasses should override this.
+ * @var string
+ */
+ protected static $daysPreferenceName;
+
+ /**
+ * Preference name for 'limit'. Subclasses should override this.
+ * @var string
+ */
+ protected static $limitPreferenceName;
+
/** @var string */
protected $rcSubpage;
'wgStructuredChangeFiltersSavedQueriesPreferenceName',
static::$savedQueriesPreferenceName
);
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersLimitPreferenceName',
+ static::$limitPreferenceName
+ );
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersDaysPreferenceName',
+ static::$daysPreferenceName
+ );
$out->addJsConfigVars(
'StructuredChangeFiltersLiveUpdatePollingRate',
# Collapsible
$collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
$collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
+ # Enhanced mode
+ $enhancedMode = $this->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) );
+ $enhancedClass = $enhancedMode ? ' mw-enhanced' : '';
+
+ $legendClasses = $collapsedClass . $enhancedClass;
$legend =
- '<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
+ '<div class="mw-changeslist-legend mw-collapsible' . $legendClasses . '">' .
$legendHeading .
'<div class="mw-collapsible-content">' . $legend . '</div>' .
'</div>';
return true;
}
- if ( $this->getConfig()->get( 'StructuredChangeFiltersShowPreference' ) ) {
- return !$this->getUser()->getOption( 'rcenhancedfilters-disable' );
- } else {
- return $this->getUser()->getOption( 'rcenhancedfilters' );
- }
+ return self::checkStructuredFilterUiEnabled(
+ $this->getConfig(),
+ $this->getUser()
+ );
}
/**
}
}
- abstract function getDefaultLimit();
+ /**
+ * Static method to check whether StructuredFilter UI is enabled for the given user
+ *
+ * @since 1.31
+ * @param Config $config
+ * @param User $user User object
+ * @return bool
+ */
+ public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
+ if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
+ return !$user->getOption( 'rcenhancedfilters-disable' );
+ } else {
+ return $user->getOption( 'rcenhancedfilters' );
+ }
+ }
+
+ /**
+ * Get the default value of the number of changes to display when loading
+ * the result set.
+ *
+ * @since 1.30
+ * @return int
+ */
+ public function getDefaultLimit() {
+ return $this->getUser()->getIntOption( static::$limitPreferenceName );
+ }
/**
* Get the default value of the number of days to display when loading
* the result set.
* Supports fractional values, and should be cast to a float.
*
+ * @since 1.30
* @return float
*/
- abstract function getDefaultDays();
+ public function getDefaultDays() {
+ return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
+ }
}
class SpecialRecentChanges extends ChangesListSpecialPage {
protected static $savedQueriesPreferenceName = 'rcfilters-saved-queries';
+ protected static $daysPreferenceName = 'rcdays'; // Use general RecentChanges preference
+ protected static $limitPreferenceName = 'rcfilters-limit'; // Use RCFilters-specific preference
private $watchlistFilterGroupDefinition;
return 60 * 5;
}
- function getDefaultLimit() {
- return $this->getUser()->getIntOption( 'rclimit' );
- }
+ public function getDefaultLimit() {
+ $systemPrefValue = $this->getUser()->getIntOption( 'rclimit' );
+ // Prefer the RCFilters-specific preference if RCFilters is enabled
+ if ( $this->isStructuredFilterUiEnabled() ) {
+ return $this->getUser()->getIntOption( static::$limitPreferenceName, $systemPrefValue );
+ }
- function getDefaultDays() {
- return floatval( $this->getUser()->getOption( 'rcdays' ) );
+ // Otherwise, use the system rclimit preference value
+ return $systemPrefValue;
}
}
*/
class SpecialWatchlist extends ChangesListSpecialPage {
protected static $savedQueriesPreferenceName = 'rcfilters-wl-saved-queries';
+ protected static $daysPreferenceName = 'watchlistdays';
+ protected static $limitPreferenceName = 'wllimit';
private $maxDays;
}
}
- public function isStructuredFilterUiEnabled() {
- return $this->getRequest()->getBool( 'rcfilters' ) || (
- $this->getConfig()->get( 'StructuredChangeFiltersOnWatchlist' ) &&
- $this->getUser()->getOption( 'rcenhancedfilters' )
+ public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
+ return (
+ $config->get( 'StructuredChangeFiltersOnWatchlist' ) &&
+ $user->getOption( 'rcenhancedfilters' )
);
}
$count = $store->countWatchedItems( $this->getUser() );
return floor( $count / 2 );
}
-
- function getDefaultLimit() {
- return $this->getUser()->getIntOption( 'wllimit' );
- }
-
- function getDefaultDays() {
- return floatval( $this->getUser()->getOption( 'watchlistdays' ) );
- }
}
$warnings['was-deleted'] = $filename;
}
- $dupes = $this->checkAgainstExistingDupes( $hash );
+ // If a file with the same name exists locally then the local file has already been tested
+ // for duplication of content
+ $ignoreLocalDupes = isset( $warnings[ 'exists '] );
+ $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
if ( $dupes ) {
$warnings['duplicate'] = $dupes;
}
/**
* @param string $hash sha1 hash of the file to check
+ * @param bool $ignoreLocalDupes True to ignore local duplicates
*
* @return File[] Duplicate files, if found.
*/
- private function checkAgainstExistingDupes( $hash ) {
+ private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
$dupes = RepoGroup::singleton()->findBySha1( $hash );
$title = $this->getTitle();
- // Remove all matches against self
foreach ( $dupes as $key => $dupe ) {
- if ( $title->equals( $dupe->getTitle() ) ) {
+ if (
+ ( $dupe instanceof LocalFile ) &&
+ $ignoreLocalDupes &&
+ $title->equals( $dupe->getTitle() )
+ ) {
unset( $dupes[$key] );
}
}
"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',
* @cfg {string} [type='send_unselected_if_any'] Group type
* @cfg {string} [view='default'] Name of the display group this group
* is a part of.
- * @cfg {boolean} [isSticky] This group is using a 'sticky' default; meaning
- * that every time a value is changed, it becomes the new default
- * @cfg {boolean} [excludedFromSavedQueries] A specific requirement to exclude
- * this filter from saved queries. This is always true if the filter is 'sticky'
- * but can be used for non-sticky filters as an additional requirement. Similarly
- * to 'sticky' it works for the entire group as a whole.
+ * @cfg {boolean} [sticky] This group is 'sticky'. It is synchronized
+ * with a preference, does not participate in Saved Queries, and is
+ * not shown in the active filters area.
* @cfg {string} [title] Group title
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
+ * and the active filters area.
* @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 {number} [range] An object defining minimum and maximum values for numeric
this.name = name;
this.type = config.type || 'send_unselected_if_any';
this.view = config.view || 'default';
- this.sticky = !!config.isSticky;
- this.excludedFromSavedQueries = this.sticky || !!config.excludedFromSavedQueries;
+ this.sticky = !!config.sticky;
this.title = config.title || name;
this.hidden = !!config.hidden;
this.allowArbitrary = !!config.allowArbitrary;
// 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
*
return this.sticky;
};
- /**
- * Check whether the group value is excluded from saved queries
- *
- * @return {boolean} Group value is excluded from saved queries
- */
- mw.rcfilters.dm.FilterGroup.prototype.isExcludedFromSavedQueries = function () {
- return this.excludedFromSavedQueries;
- };
-
/**
* Normalize a value given to this group. This is mostly for correcting
* arbitrary values for 'single option' groups, given by the user settings
this.groups = {};
this.defaultParams = {};
- this.defaultFiltersEmpty = null;
this.highlightEnabled = false;
this.parameterMap = {};
this.emptyParameterState = null;
$.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 a representation of the full parameter list, including all base values
*
- * @param {Object} [parameters] A given parameter state to minimize. If not given the current
- * state of the system will be used.
- * @param {boolean} [removeExcluded] Remove excluded and sticky parameters
* @return {Object} Full parameter representation
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function ( parameters, removeExcluded ) {
- var result = {};
-
- parameters = parameters ? $.extend( true, {}, parameters ) : this.getCurrentParameterState();
-
- result = $.extend(
+ mw.rcfilters.dm.FiltersViewModel.prototype.getExpandedParamRepresentation = function () {
+ return $.extend(
true,
{},
this.getEmptyParameterState(),
- parameters
+ this.getCurrentParameterState()
);
-
- if ( removeExcluded ) {
- result = this.removeExcludedParams( result );
- }
-
- return result;
};
/**
* Get a parameter representation of the current state of the model
*
- * @param {boolean} [removeExcludedParams] Remove excluded filters from final result
+ * @param {boolean} [removeStickyParams] Remove sticky filters from final result
* @return {Object} Parameter representation of the current state of the model
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeExcludedParams ) {
- var excludedParams,
- state = this.getMinimizedParamRepresentation( $.extend(
- true,
- {},
- this.getParametersFromFilters( this.getSelectedState() ),
- this.getHighlightParameters()
- ) );
-
- if ( removeExcludedParams ) {
- excludedParams = this.getExcludedParams();
- // Delete all excluded filters
- $.each( state, function ( param ) {
- if ( excludedParams.indexOf( param ) > -1 ) {
- delete state[ param ];
- }
- } );
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentParameterState = function ( removeStickyParams ) {
+ var state = this.getMinimizedParamRepresentation( $.extend(
+ true,
+ {},
+ this.getParametersFromFilters( this.getSelectedState() ),
+ this.getHighlightParameters()
+ ) );
+
+ if ( removeStickyParams ) {
+ state = this.removeStickyParams( state );
}
return state;
};
/**
- * Delete excluded and sticky filters from given object. If object isn't given, output
- * the current filter state without the excluded values
- *
- * @param {Object} [filterState] Filter state
- * @return {Object} Filter state without excluded filters
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedFilters = function ( filterState ) {
- filterState = filterState !== undefined ?
- $.extend( true, {}, filterState ) :
- this.getFiltersFromParameters();
-
- // Remove excluded filters
- Object.keys( this.getExcludedFiltersState() ).forEach( function ( filterName ) {
- delete filterState[ filterName ];
- } );
-
- // Remove sticky filters
- Object.keys( this.getStickyFiltersState() ).forEach( function ( filterName ) {
- delete filterState[ filterName ];
- } );
-
- return filterState;
- };
- /**
- * Delete excluded and sticky parameters from given object. If object isn't given, output
- * the current param state without the excluded values
+ * Delete sticky parameters from given object.
*
- * @param {Object} [paramState] Parameter state
- * @return {Object} Parameter state without excluded filters
+ * @param {Object} paramState Parameter state
+ * @return {Object} Parameter state without sticky parameters
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.removeExcludedParams = function ( paramState ) {
- paramState = paramState !== undefined ?
- $.extend( true, {}, paramState ) :
- this.getCurrentParameterState();
-
- // Remove excluded filters
- this.getExcludedParams().forEach( function ( paramName ) {
- delete paramState[ paramName ];
- } );
-
- // Remove sticky filters
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeStickyParams = function ( paramState ) {
this.getStickyParams().forEach( function ( paramName ) {
delete paramState[ paramName ];
} );
return paramState;
};
- /**
- * Get the names of all available filters
- *
- * @return {string[]} An array of filter names
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getFilterNames = function () {
- return this.getItems().map( function ( item ) { return item.getName(); } );
- };
-
/**
* Turn the highlight feature on or off
*/
mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
return ( this.views[ view ] && this.views[ view ].trigger ) || '';
};
+
/**
* Get the value of a specific parameter
*
/**
* Get the current selected state of the 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 () {
+ mw.rcfilters.dm.FiltersViewModel.prototype.getSelectedState = function ( onlySelected ) {
var i,
items = this.getItems(),
result = {};
for ( i = 0; i < items.length; i++ ) {
- result[ items[ i ].getName() ] = items[ i ].isSelected();
+ if ( !onlySelected || items[ i ].getValue() ) {
+ result[ items[ i ].getName() ] = items[ i ].getValue();
+ }
}
return result;
/**
* Get an object representing default parameters state
*
- * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params
* @return {Object} Default parameter values
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function ( excludeHiddenParams ) {
- var result = {};
-
- // Get default filter state
- $.each( this.groups, function ( name, model ) {
- $.extend( true, result, model.getDefaultParams() );
- } );
-
- if ( excludeHiddenParams ) {
- Object.keys( this.getDefaultHiddenParams() ).forEach( function ( paramName ) {
- delete result[ paramName ];
- } );
- }
-
- return result;
- };
-
- /**
- * Get an object representing defaults for the hidden parameters state
- *
- * @return {Object} Default values for hidden parameters
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultHiddenParams = function () {
+ mw.rcfilters.dm.FiltersViewModel.prototype.getDefaultParams = function () {
var result = {};
// Get default filter state
$.each( this.groups, function ( name, model ) {
- if ( model.isHidden() ) {
+ if ( !model.isSticky() ) {
$.extend( true, result, model.getDefaultParams() );
}
} );
return result;
};
- /**
- * Get a filter representation of all sticky parameters
- *
- * @return {Object} Sticky filters values
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getStickyFiltersState = function () {
- var result = {};
-
- $.each( this.groups, function ( name, model ) {
- if ( model.isSticky() ) {
- $.extend( true, result, model.getSelectedState() );
- }
- } );
-
- return result;
- };
-
- /**
- * Get a filter representation of all parameters that are marked
- * as being excluded from saved query.
- *
- * @return {Object} Excluded filters values
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getExcludedFiltersState = function () {
- var result = {};
-
- $.each( this.groups, function ( name, model ) {
- if ( model.isExcludedFromSavedQueries() ) {
- $.extend( true, result, model.getSelectedState() );
- }
- } );
-
- return result;
- };
-
- /**
- * Get the parameter names that represent filters that are excluded
- * from saved queries.
- *
- * @return {string[]} Parameter names
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getExcludedParams = function () {
- var result = [];
-
- $.each( this.groups, function ( name, model ) {
- if ( model.isExcludedFromSavedQueries() ) {
- if ( model.isPerGroupRequestParameter() ) {
- result.push( name );
- } else {
- // Each filter is its own param
- result = result.concat( model.getItems().map( function ( filterItem ) {
- return filterItem.getParamName();
- } ) );
- }
- }
- } );
-
- return result;
- };
-
/**
* Analyze the groups and their filters and output an object representing
* the state of the parameters they represent.
// 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() ] );
} );
}
};
/**
- * Check whether the current filter state is set to all false.
+ * Check whether no visible filter is selected.
+ *
+ * Filter groups that are hidden or sticky are not shown in the
+ * active filters area and therefore not included in this check.
*
- * @return {boolean} Current filters are all empty
+ * @return {boolean} No visible filter is selected
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
+ mw.rcfilters.dm.FiltersViewModel.prototype.areVisibleFiltersEmpty = function () {
// Check if there are either any selected items or any items
// that have highlight enabled
return !this.getItems().some( function ( filterItem ) {
- return !filterItem.getGroupModel().isHidden() && ( filterItem.isSelected() || filterItem.isHighlighted() );
+ var visible = !filterItem.getGroupModel().isSticky() && !filterItem.getGroupModel().isHidden(),
+ active = ( filterItem.isSelected() || filterItem.isHighlighted() );
+ return visible && active;
} );
};
return allSelected;
};
+
/**
* Switch the current view
*
return this.views[ viewName ] && this.views[ viewName ].title;
};
- /**
- * Get an array of all available view names
- *
- * @return {string} Available view names
- */
- mw.rcfilters.dm.FiltersViewModel.prototype.getAvailableViews = function () {
- return Object.keys( this.views );
- };
-
/**
* Get the view that fits the given trigger
*
* 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' );
}
};
isDefault = String( savedQueries.default ) === String( id );
if ( normalizedData && normalizedData.params ) {
- // Backwards-compat fix: Remove excluded parameters from
+ // Backwards-compat fix: Remove sticky parameters from
// the given data, if they exist
- normalizedData.params = model.filtersModel.removeExcludedParams( normalizedData.params );
+ normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );
// Correct the invert state for effective selection
if ( normalizedData.params.invert && !normalizedData.params.namespaces ) {
/**
* Get the full data representation of the default query, if it exists
*
- * @param {boolean} [excludeHiddenParams] Exclude hidden parameters in the result
* @return {Object|null} Representation of the default params if exists.
* Null if default doesn't exist or if the user is not logged in.
*/
- mw.rcfilters.dm.SavedQueriesModel.prototype.getDefaultParams = function ( excludeHiddenParams ) {
- var data = ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
-
- if ( excludeHiddenParams ) {
- Object.keys( this.filtersModel.getDefaultHiddenParams() ).forEach( function ( paramName ) {
- delete data[ paramName ];
- } );
- }
-
- return data;
+ mw.rcfilters.dm.SavedQueriesModel.prototype.getDefaultParams = function () {
+ return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
};
/**
* @return {Object} Full param representation
*/
mw.rcfilters.dm.SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
- // Merge saved filter state with sticky filter values
- var savedFilters;
-
data = data || {};
-
- // In order to merge sticky filters with the data, we have to
- // transform this to filters first, merge, and then back to
- // parameters
- savedFilters = $.extend(
- true, {},
- this.filtersModel.getFiltersFromParameters( data.params ),
- this.filtersModel.getStickyFiltersState()
- );
-
// Return parameter representation
return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
- this.filtersModel.getParametersFromFilters( savedFilters ),
+ data.params,
data.highlights
) );
};
* @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
* @param {Object} config Additional configuration
* @cfg {string} savedQueriesPreferenceName Where to save the saved queries
+ * @cfg {string} daysPreferenceName Preference name for the days filter
+ * @cfg {string} limitPreferenceName Preference name for the limit filter
*/
mw.rcfilters.Controller = function MwRcfiltersController( filtersModel, changesListModel, savedQueriesModel, config ) {
this.filtersModel = filtersModel;
this.changesListModel = changesListModel;
this.savedQueriesModel = savedQueriesModel;
this.savedQueriesPreferenceName = config.savedQueriesPreferenceName;
+ this.daysPreferenceName = config.daysPreferenceName;
+ this.limitPreferenceName = config.limitPreferenceName;
this.requestCounter = {};
this.baseFilterState = {};
separator: ';',
fullCoverage: true,
filters: items
- },
- {
- name: 'invertGroup',
- type: 'boolean',
- hidden: true,
- filters: [ {
- name: 'invert',
- 'default': '0'
- } ]
} ]
};
+ views.invert = {
+ groups: [
+ {
+ name: 'invertGroup',
+ type: 'boolean',
+ hidden: true,
+ filters: [ {
+ name: 'invert',
+ 'default': '0'
+ } ]
+ } ]
+ };
}
if ( tagList ) {
views.tags = {
max: 1000
},
sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
- 'default': displayConfig.limitDefault,
- // Temporarily making this not sticky until we resolve the problem
- // with the misleading preference. Note that if this is to be permanent
- // we should remove all sticky behavior methods completely
- // See T172156
- // isSticky: true,
- excludedFromSavedQueries: true,
+ 'default': mw.user.options.get( this.limitPreferenceName, displayConfig.limitDefault ),
+ sticky: true,
filters: displayConfig.limitArray.map( function ( num ) {
return controller._createFilterDataFromNumber( num, num );
} )
( Number( i ) * 24 ).toFixed( 2 ) :
Number( i );
},
- 'default': displayConfig.daysDefault,
- // Temporarily making this not sticky while limit is not sticky, see above
- // isSticky: true,
- excludedFromSavedQueries: true,
+ 'default': mw.user.options.get( this.daysPreferenceName, displayConfig.daysDefault ),
+ sticky: true,
filters: [
// Hours (1, 2, 6, 12)
0.04166, 0.0833, 0.25, 0.5
type: 'boolean',
title: '', // Because it's a hidden group, this title actually appears nowhere
hidden: true,
- isSticky: true,
+ sticky: true,
filters: [
{
name: 'enhanced',
]
};
+ 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:
* @return {boolean} Defaults are all false
*/
mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
- return $.isEmptyObject( this._getDefaultParams( true ) );
+ return $.isEmptyObject( this._getDefaultParams() );
};
/**
*/
mw.rcfilters.Controller.prototype.toggleInvertedNamespaces = function () {
this.filtersModel.toggleInvertedNamespaces();
-
if (
this.filtersModel.getFiltersByView( 'namespaces' ).filter(
function ( filterItem ) { return filterItem.isSelected(); }
) {
// Only re-fetch results if there are namespace items that are actually selected
this.updateChangesList();
+ } else {
+ this.uriProcessor.updateURL();
+ }
+ };
+
+ /**
+ * 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
*
/**
* Update the limit default value
*
- * param {number} newValue New value
+ * @param {number} newValue New value
*/
- mw.rcfilters.Controller.prototype.updateLimitDefault = function ( /* newValue */ ) {
- // HACK: Temporarily remove this from being sticky
- // See T172156
-
- /*
- if ( !$.isNumeric( newValue ) ) {
- return;
- }
-
- newValue = Number( newValue );
-
- if ( mw.user.options.get( 'rcfilters-rclimit' ) !== newValue ) {
- // Save the preference
- new mw.Api().saveOption( 'rcfilters-rclimit', newValue );
- // Update the preference for this session
- mw.user.options.set( 'rcfilters-rclimit', newValue );
- }
- */
- return;
+ mw.rcfilters.Controller.prototype.updateLimitDefault = function ( newValue ) {
+ this.updateNumericPreference( this.limitPreferenceName, newValue );
};
/**
* Update the days default value
*
- * param {number} newValue New value
+ * @param {number} newValue New value
*/
- mw.rcfilters.Controller.prototype.updateDaysDefault = function ( /* newValue */ ) {
- // HACK: Temporarily remove this from being sticky
- // See T172156
-
- /*
- if ( !$.isNumeric( newValue ) ) {
- return;
- }
-
- newValue = Number( newValue );
-
- if ( mw.user.options.get( 'rcdays' ) !== newValue ) {
- // Save the preference
- new mw.Api().saveOption( 'rcdays', newValue );
- // Update the preference for this session
- mw.user.options.set( 'rcdays', newValue );
- }
- */
- return;
+ mw.rcfilters.Controller.prototype.updateDaysDefault = function ( newValue ) {
+ this.updateNumericPreference( this.daysPreferenceName, newValue );
};
/**
* Update the group by page default value
*
- * @param {number} newValue New value
+ * @param {boolean} newValue New value
*/
mw.rcfilters.Controller.prototype.updateGroupByPageDefault = function ( newValue ) {
+ this.updateNumericPreference( 'usenewrc', Number( newValue ) );
+ };
+
+ /**
+ * Update a numeric preference with a new value
+ *
+ * @param {string} prefName Preference name
+ * @param {number|string} newValue New value
+ */
+ mw.rcfilters.Controller.prototype.updateNumericPreference = function ( prefName, newValue ) {
if ( !$.isNumeric( newValue ) ) {
return;
}
newValue = Number( newValue );
- if ( mw.user.options.get( 'usenewrc' ) !== newValue ) {
+ if ( mw.user.options.get( prefName ) !== newValue ) {
// Save the preference
- new mw.Api().saveOption( 'usenewrc', newValue );
+ new mw.Api().saveOption( prefName, newValue );
// Update the preference for this session
- mw.user.options.set( 'usenewrc', newValue );
+ mw.user.options.set( prefName, newValue );
}
};
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
* Get an object representing the default parameter state, whether
* it is from the model defaults or from the saved queries.
*
- * @param {boolean} [excludeHiddenParams] Exclude hidden and sticky params
* @return {Object} Default parameters
*/
- mw.rcfilters.Controller.prototype._getDefaultParams = function ( excludeHiddenParams ) {
+ mw.rcfilters.Controller.prototype._getDefaultParams = function () {
if ( this.savedQueriesModel.getDefault() ) {
- return this.savedQueriesModel.getDefaultParams( excludeHiddenParams );
+ return this.savedQueriesModel.getDefaultParams();
} else {
- return this.filtersModel.getDefaultParams( excludeHiddenParams );
+ return this.filtersModel.getDefaultParams();
}
};
};
}
- $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 )
);
};
// wiki default.
// Any subsequent change of the URL through the RCFilters
// system will receive 'urlversion=2'
- var hiddenParamDefaults = this.filtersModel.getDefaultHiddenParams(),
- base = this.getVersion( uriQuery ) === 2 ?
- {} :
- this.filtersModel.getDefaultParams();
+ var base = this.getVersion( uriQuery ) === 2 ?
+ {} :
+ this.filtersModel.getDefaultParams();
return $.extend(
true,
{},
this.filtersModel.getMinimizedParamRepresentation(
- $.extend( true, {}, hiddenParamDefaults, base, uriQuery )
+ $.extend( true, {}, base, uriQuery )
),
{ urlversion: '2' }
);
*/
init: function () {
var $topLinks,
- rcTopSection,
+ topSection,
$watchlistDetails,
- wlTopSection,
namespaces,
savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
+ daysPreferenceName = mw.config.get( 'wgStructuredChangeFiltersDaysPreferenceName' ),
+ limitPreferenceName = mw.config.get( 'wgStructuredChangeFiltersLimitPreferenceName' ),
filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
changesListModel = new mw.rcfilters.dm.ChangesListViewModel(),
savedQueriesModel = new mw.rcfilters.dm.SavedQueriesModel( filtersModel ),
controller = new mw.rcfilters.Controller(
filtersModel, changesListModel, savedQueriesModel,
{
- savedQueriesPreferenceName: savedQueriesPreferenceName
+ savedQueriesPreferenceName: savedQueriesPreferenceName,
+ daysPreferenceName: daysPreferenceName,
+ limitPreferenceName: limitPreferenceName
}
),
$overlay = $( '<div>' )
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.
}
}
+ &-enhancedView {
+ .mw-changeslist-legend {
+ z-index: 1;
+
+ &:not( .mw-collapsed ) {
+ .box-shadow( 0 1px 1px rgba( 0, 0, 0, 0.15 ) );
+ }
+ }
+ }
+
.mw-changeslist-legend {
background-color: @background-color-base;
+ border: 1px solid @colorGray12;
}
// Correction for Enhanced RC
--- /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;
+ }
+ }
+}
*/
mw.rcfilters.ui.ChangesLimitAndDateButtonWidget.prototype.onPopupGroupByPage = function ( isGrouped ) {
this.controller.toggleFilterSelect( this.groupByPageItemModel.getName(), isGrouped );
- this.controller.updateGroupByPageDefault( Number( isGrouped ) );
+ this.controller.updateGroupByPageDefault( isGrouped );
this.button.popup.toggle( false );
};
// Regular RC
$content.find( 'ul.special li' )
.prepend( $highlights.clone() );
+
+ $content.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhancedView' );
}
};
* Respond to click event on the reset button
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
- if ( this.model.areCurrentFiltersEmpty() ) {
+ if ( this.model.areVisibleFiltersEmpty() ) {
// Reset to default filters
this.controller.resetToDefaults();
} else {
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
var defaultsAreEmpty = this.controller.areDefaultsEmpty(),
- currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
+ currFiltersAreEmpty = this.model.areVisibleFiltersEmpty(),
hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
this.resetButton.setIcon(
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 ) );
.mw-changeslist-legend.mw-collapsed .mw-collapsible-content {
display: none;
}
+
+/* Prevent pushing down of content if legend is initially collapsed */
+.mw-changeslist-legend.mw-collapsed ~ ul.special > li:first-child {
+ clear: right;
+}
+
+/* Absolute positioning to avoid table overlap with floating elements */
+.mw-changeslist-legend.mw-enhanced {
+ background-color: #fff;
+ position: absolute;
+ right: 0;
+ margin-top: -0.5em;
+}
{ name: 'filter5', cssClass: 'filter5class' },
{ name: 'filter6' } // Not supporting highlights
]
+ }, {
+ name: 'group4',
+ title: 'Group 4',
+ type: 'boolean',
+ sticky: true,
+ filters: [
+ { name: 'stickyFilter7', cssClass: 'filter7class' },
+ { name: 'stickyFilter8', cssClass: 'filter8class' }
+ ]
} ],
minimalDefaultParams = {
filter1: '1',
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 ) );
name: 'group2',
type: 'send_unselected_if_any',
fullCoverage: true,
- excludedFromSavedQueries: true,
conflicts: [ { group: 'group1', filter: 'filter1' } ],
filters: [
{ name: 'filter4', label: 'group2filter4-label', description: 'group2filter4-desc', cssClass: 'filter4class' },
}, {
name: 'group4',
type: 'single_option',
+ hidden: true,
default: 'option2',
filters: [
// NOTE: The entire group has no highlight supported
}, {
name: 'group6',
type: 'boolean',
- isSticky: true,
+ sticky: true,
filters: [
{ name: 'group6option1', label: 'group6option1-label', description: 'group6option1-desc', cssClass: 'group6opt1class' },
{ name: 'group6option2', label: 'group6option2-label', description: 'group6option2-desc', default: true, cssClass: 'group6opt2class' },
}, {
name: 'group7',
type: 'single_option',
- isSticky: true,
+ sticky: true,
default: 'group7option2',
filters: [
{ name: 'group7option1', label: 'group7option1-label', description: 'group7option1-desc', cssClass: 'group7opt1class' },
{ name: 'group7option3', label: 'group7option3-label', description: 'group7option3-desc', cssClass: 'group7opt3class' }
]
} ],
+ shortFilterDefinition = [ {
+ name: 'group1',
+ type: 'send_unselected_if_any',
+ filters: [ { name: 'filter1' }, { name: 'filter2' } ]
+ }, {
+ name: 'group2',
+ type: 'boolean',
+ hidden: true,
+ filters: [ { name: 'filter3' }, { name: 'filter4' } ]
+ }, {
+ name: 'group3',
+ type: 'string_options',
+ sticky: true,
+ default: 'filter6',
+ filters: [ { name: 'filter5' }, { name: 'filter6' }, { name: 'filter7' } ]
+ } ],
viewsDefinition = {
namespaces: {
label: 'Namespaces',
group3: 'filter8',
group4: 'option2',
group5: 'option1',
- group6option1: '0',
- group6option2: '1',
- group6option3: '1',
- group7: 'group7option2',
namespace: ''
},
baseParamRepresentation = {
assert.deepEqual(
model.getDefaultParams(),
defaultParameters,
- 'Default parameters are stored properly per filter and group'
- );
-
- // Change sticky filter
- model.toggleFiltersSelected( {
- group7__group7option1: true
- } );
-
- // Make sure defaults have changed
- assert.deepEqual(
- model.getDefaultParams(),
- $.extend( true, {}, defaultParameters, {
- group7: 'group7option1'
- } ),
- 'Default parameters are stored properly per filter and group'
+ 'Default parameters are stored properly per filter and group (sticky groups are ignored)'
);
} );
{
input: {
filter1: '1', // Regular (do not strip)
- group6option1: '1', // Sticky
- filter4: '1', // Excluded
- filter5: '0' // Excluded
+ group6option1: '1' // Sticky
},
result: { filter1: '1' },
- msg: 'Valid input strips all sticky and excluded params regardless of value'
+ msg: 'Valid input strips all sticky params regardless of value'
}
];
cases.forEach( function ( test ) {
assert.deepEqual(
- model.removeExcludedParams( test.input ),
+ model.removeStickyParams( test.input ),
test.result,
test.msg
);
'Items without a specified class identifier are not highlighted.'
);
} );
+
+ QUnit.test( 'emptyAllFilters', function ( assert ) {
+ var model = new mw.rcfilters.dm.FiltersViewModel();
+
+ model.initializeFilters( shortFilterDefinition, null );
+
+ model.toggleFiltersSelected( {
+ group1__filter1: true,
+ group2__filter4: true, // hidden
+ group3__filter5: true // sticky
+ } );
+
+ model.emptyAllFilters();
+
+ assert.deepEqual(
+ model.getSelectedState( true ),
+ {
+ group3__filter5: true,
+ group3__filter6: true
+ },
+ 'Emptying filters does not affect sticky filters'
+ );
+ } );
+
+ QUnit.test( 'areVisibleFiltersEmpty', function ( assert ) {
+ var model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( shortFilterDefinition, null );
+
+ model.emptyAllFilters();
+ assert.ok( model.areVisibleFiltersEmpty() );
+
+ model.toggleFiltersSelected( {
+ group3__filter5: true // sticky
+ } );
+ assert.ok( model.areVisibleFiltersEmpty() );
+
+ model.toggleFiltersSelected( {
+ group1__filter1: true
+ } );
+ assert.notOk( model.areVisibleFiltersEmpty() );
+ } );
}( mediaWiki, jQuery ) );
}, {
name: 'group3',
type: 'boolean',
- isSticky: true,
+ sticky: true,
filters: [
{ name: 'group3option1', cssClass: 'filter1class' },
{ name: 'group3option2', cssClass: 'filter1class' },