* @ingroup SpecialPage
*/
use MediaWiki\Logger\LoggerFactory;
+ use Wikimedia\Rdbms\DBQueryTimeoutError;
use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
$this->considerActionsForDefaultSavedQuery();
- $rows = $this->getRows();
$opts = $this->getOptions();
- if ( $rows === false ) {
- $rows = new FakeResultWrapper( [] );
- }
+ try {
+ $rows = $this->getRows();
+ if ( $rows === false ) {
+ $rows = new FakeResultWrapper( [] );
+ }
- // Used by Structured UI app to get results without MW chrome
- if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
- $this->getOutput()->setArticleBodyOnly( true );
- }
+ // Used by Structured UI app to get results without MW chrome
+ if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
+ $this->getOutput()->setArticleBodyOnly( true );
+ }
- // Used by "live update" and "view newest" to check
- // if there's new changes with minimal data transfer
- if ( $this->getRequest()->getBool( 'peek' ) ) {
+ // Used by "live update" and "view newest" to check
+ // if there's new changes with minimal data transfer
+ if ( $this->getRequest()->getBool( 'peek' ) ) {
$code = $rows->numRows() > 0 ? 200 : 204;
- $this->getOutput()->setStatusCode( $code );
- return;
- }
+ $this->getOutput()->setStatusCode( $code );
+ return;
+ }
- $batch = new LinkBatch;
- foreach ( $rows as $row ) {
- $batch->add( NS_USER, $row->rc_user_text );
- $batch->add( NS_USER_TALK, $row->rc_user_text );
- $batch->add( $row->rc_namespace, $row->rc_title );
- if ( $row->rc_source === RecentChange::SRC_LOG ) {
- $formatter = LogFormatter::newFromRow( $row );
- foreach ( $formatter->getPreloadTitles() as $title ) {
- $batch->addObj( $title );
+ $batch = new LinkBatch;
+ foreach ( $rows as $row ) {
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ $batch->add( $row->rc_namespace, $row->rc_title );
+ if ( $row->rc_source === RecentChange::SRC_LOG ) {
+ $formatter = LogFormatter::newFromRow( $row );
+ foreach ( $formatter->getPreloadTitles() as $title ) {
+ $batch->addObj( $title );
+ }
}
}
- }
- $batch->execute();
+ $batch->execute();
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+ $this->webOutput( $rows, $opts );
- $this->setHeaders();
- $this->outputHeader();
- $this->addModules();
- $this->webOutput( $rows, $opts );
+ $rows->free();
+ } catch ( DBQueryTimeoutError $timeoutException ) {
+ MWExceptionHandler::logException( $timeoutException );
- $rows->free();
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+
+ $this->getOutput()->setStatusCode( 500 );
+ $this->webOutputHeader( 0, $opts );
+ $this->outputTimeout();
+ }
if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
// Clean up any bad page entries for titles showing up in RC
if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) {
// Only load queries that are 'version' 2, since those
// have parameter representation
- if ( $savedQueries[ 'version' ] === '2' ) {
+ if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) {
$savedQueryDefaultID = $savedQueries[ 'default' ];
$defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ];
);
}
+ /**
+ * Add the "timeout" message to the output
+ */
+ protected function outputTimeout() {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-timeout">' .
+ $this->msg( 'recentchanges-timeout' )->parse() .
+ '</div>'
+ );
+ }
+
/**
* Get the database result for this special page instance. Used by ApiFeedRecentChanges.
*
return $unstructuredGroupDefinition;
}
+ /**
+ * @return array The legacy show/hide toggle filters
+ */
+ protected function getLegacyShowHideFilters() {
+ $filters = [];
+ foreach ( $this->filterGroups as $group ) {
+ if ( $group instanceof ChangesListBooleanFilterGroup ) {
+ foreach ( $group->getFilters() as $key => $filter ) {
+ if ( $filter->displaysOnUnstructuredUi( $this ) ) {
+ $filters[ $key ] = $filter;
+ }
+ }
+ }
+ }
+ return $filters;
+ }
+
/**
* Register all the filters, including legacy hook-driven ones.
* Then create a FormOptions object with options as specified by the user
// If urlversion=2 is set, ignore the filter defaults and set them all to false/empty
$useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2;
- // Add all filters
/** @var ChangesListFilterGroup $filterGroup */
foreach ( $this->filterGroups as $filterGroup ) {
- // URL parameters can be per-group, like 'userExpLevel',
- // or per-filter, like 'hideminor'.
- if ( $filterGroup->isPerGroupRequestParameter() ) {
- $opts->add( $filterGroup->getName(), $useDefaults ? $filterGroup->getDefault() : '' );
- } else {
- /** @var ChangesListBooleanFilter $filter */
- foreach ( $filterGroup->getFilters() as $filter ) {
- $opts->add( $filter->getName(), $useDefaults ? $filter->getDefault( $structuredUI ) : false );
- }
- }
+ $filterGroup->addOptions( $opts, $useDefaults, $structuredUI );
}
$opts->add( 'namespace', '', FormOptions::STRING );
// or per-filter, like 'hideminor'.
foreach ( $this->filterGroups as $filterGroup ) {
- if ( $filterGroup->isPerGroupRequestParameter() ) {
+ if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) {
$stringParameterNameSet[$filterGroup->getName()] = true;
- } elseif ( $filterGroup->getType() === ChangesListBooleanFilterGroup::TYPE ) {
+ } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) {
foreach ( $filterGroup->getFilters() as $filter ) {
$hideParameterNameSet[$filter->getName()] = true;
}
* @param FormOptions $opts
*/
public function validateOptions( FormOptions $opts ) {
- if ( $this->fixContradictoryOptions( $opts ) ) {
+ $isContradictory = $this->fixContradictoryOptions( $opts );
+ $isReplaced = $this->replaceOldOptions( $opts );
+
+ if ( $isContradictory || $isReplaced ) {
$query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) );
$this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
}
return false;
}
+ /**
+ * Replace old options 'hideanons' or 'hideliu' with structured UI equivalent
+ *
+ * @param FormOptions $opts
+ * @return bool True if the change was made
+ */
+ public function replaceOldOptions( FormOptions $opts ) {
+ if ( !$this->isStructuredFilterUiEnabled() ) {
+ return false;
+ }
+
+ // At this point 'hideanons' and 'hideliu' cannot be both true,
+ // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case
+ if ( $opts[ 'hideanons' ] ) {
+ $opts->reset( 'hideanons' );
+ $opts[ 'userExpLevel' ] = 'registered';
+ return true;
+ }
+
+ if ( $opts[ 'hideliu' ] ) {
+ $opts->reset( 'hideliu' );
+ $opts[ 'userExpLevel' ] = 'unregistered';
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Convert parameters values from true/false to 1/0
* so they are not omitted by wfArrayToCgi()
$dbr = $this->getDB();
$isStructuredUI = $this->isStructuredFilterUiEnabled();
+ /** @var ChangesListFilterGroup $filterGroup */
foreach ( $this->filterGroups as $filterGroup ) {
- // URL parameters can be per-group, like 'userExpLevel',
- // or per-filter, like 'hideminor'.
- if ( $filterGroup->isPerGroupRequestParameter() ) {
- $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
- $query_options, $join_conds, $opts[$filterGroup->getName()] );
- } else {
- foreach ( $filterGroup->getFilters() as $filter ) {
- if ( $filter->isActive( $opts, $isStructuredUI ) ) {
- $filter->modifyQuery( $dbr, $this, $tables, $fields, $conds,
- $query_options, $join_conds );
- }
- }
- }
+ $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds,
+ $query_options, $join_conds, $opts, $isStructuredUI );
}
// Namespace filtering
}
/**
- * Send output to the OutputPage object, only called if not used feeds
+ * Send header output to the OutputPage object, only called if not using feeds
*
- * @param ResultWrapper $rows Database rows
+ * @param int $rowCount Number of database rows
* @param FormOptions $opts
*/
- public function webOutput( $rows, $opts ) {
+ private function webOutputHeader( $rowCount, $opts ) {
if ( !$this->including() ) {
$this->outputFeedLinks();
- $this->doHeader( $opts, $rows->numRows() );
+ $this->doHeader( $opts, $rowCount );
}
+ }
+
+ /**
+ * Send output to the OutputPage object, only called if not used feeds
+ *
+ * @param ResultWrapper $rows Database rows
+ * @param FormOptions $opts
+ */
+ public function webOutput( $rows, $opts ) {
+ $this->webOutputHeader( $rows->numRows(), $opts );
$this->outputChangesList( $rows, $opts );
}
"anonpreviewwarning": "<em>You are not logged in. Saving will record your IP address in this page's edit history.</em>",
"missingsummary": "<strong>Reminder:</strong> You have not provided an edit summary.\nIf you click \"$1\" again, your edit will be saved without one.",
"selfredirect": "<strong>Warning:</strong> You are redirecting this page to itself.\nYou may have specified the wrong target for the redirect, or you may be editing the wrong page.\nIf you click \"$1\" again, the redirect will be created anyway.",
- "missingcommenttext": "Please enter a comment below.",
+ "missingcommenttext": "Please enter a comment.",
"missingcommentheader": "<strong>Reminder:</strong> You have not provided a subject for this comment.\nIf you click \"$1\" again, your edit will be saved without one.",
"summary-preview": "Preview of edit summary:",
"subject-preview": "Preview of subject:",
"timezoneregion-europe": "Europe",
"timezoneregion-indian": "Indian Ocean",
"timezoneregion-pacific": "Pacific Ocean",
- "allowemail": "Enable email from other users",
- "email-blacklist-label": "Prohibit these users from sending emails to me:",
+ "allowemail": "Allow other users to email me",
+ "email-blacklist-label": "Prohibit these users from emailing me:",
"prefs-searchoptions": "Search",
"prefs-namespaces": "Namespaces",
"default": "default",
"recentchanges-summary": "Track the most recent changes to the wiki on this page.",
"recentchangestext": "-",
"recentchanges-noresult": "No changes during the given period match these criteria.",
+ "recentchanges-timeout": "This search has timed out. You may wish to try different search parameters.",
"recentchanges-feed-description": "Track the most recent changes to the wiki in this feed.",
"recentchanges-label-newpage": "This edit created a new page",
"recentchanges-label-minor": "This is a minor edit",
"recentchanges-submit": "Show",
"rcfilters-tag-remove": "Remove '$1'",
"rcfilters-legend-heading": "<strong>List of abbreviations:</strong>",
- "rcfilters-other-review-tools": "<strong>Other review tools</strong>",
+ "rcfilters-other-review-tools": "Other review tools",
"rcfilters-group-results-by-page": "Group results by page",
"rcfilters-grouping-title": "Grouping",
"rcfilters-activefilters": "Active filters",
"rcfilters-days-show-hours": "$1 {{PLURAL:$1|hour|hours}}",
"rcfilters-highlighted-filters-list": "Highlighted: $1",
"rcfilters-quickfilters": "Saved filters",
- "rcfilters-quickfilters-placeholder-title": "No links saved yet",
+ "rcfilters-quickfilters-placeholder-title": "No filters saved yet",
"rcfilters-quickfilters-placeholder-description": "To save your filter settings and reuse them later, click the bookmark icon in the Active Filter area, below.",
"rcfilters-savedqueries-defaultlabel": "Saved filters",
"rcfilters-savedqueries-rename": "Rename",
"rcfilters-filter-user-experience-level-unregistered-label": "Unregistered",
"rcfilters-filter-user-experience-level-unregistered-description": "Editors who aren't logged-in.",
"rcfilters-filter-user-experience-level-newcomer-label": "Newcomers",
- "rcfilters-filter-user-experience-level-newcomer-description": "Registered editors with fewer than 10 edits and 4 days of activity.",
+ "rcfilters-filter-user-experience-level-newcomer-description": "Registered editors who have fewer than 10 edits or 4 days of activity.",
"rcfilters-filter-user-experience-level-learner-label": "Learners",
"rcfilters-filter-user-experience-level-learner-description": "Registered editors whose experience falls between \"Newcomers\" and \"Experienced users.\"",
"rcfilters-filter-user-experience-level-experienced-label": "Experienced users",
"rcfilters-view-namespaces-tooltip": "Filter results by namespace",
"rcfilters-view-tags-tooltip": "Filter results using edit tags",
"rcfilters-view-return-to-default-tooltip": "Return to main filter menu",
- "rcfilters-view-tags-help-icon-tooltip": "Learn more about Tagged Edits",
+ "rcfilters-view-tags-help-icon-tooltip": "Learn more about Tagged edits",
"rcfilters-liveupdates-button": "Live updates",
"rcfilters-liveupdates-button-title-on": "Turn off live updates",
"rcfilters-liveupdates-button-title-off": "Display new changes as they happen",
"uploaded-script-svg": "Found scriptable element \"$1\" in the uploaded SVG file.",
"uploaded-hostile-svg": "Found unsafe CSS in the style element of uploaded SVG file.",
"uploaded-event-handler-on-svg": "Setting event-handler attributes <code>$1=\"$2\"</code> is not allowed in SVG files.",
- "uploaded-href-attribute-svg": "href attributes in SVG files are only allowed to link to http:// or https:// targets, found <code><$1 $2=\"$3\"></code>.",
+ "uploaded-href-attribute-svg": "<a> elements can only link (href) to data: (embedded file), http:// or https://, or fragment (#, same-document) targets. For other elements, such as <image>, only data: and fragment are allowed. Try embedding images when exporting your SVG. Found <code><$1 $2=\"$3\"></code>.",
"uploaded-href-unsafe-target-svg": "Found href to unsafe data: URI target <code><$1 $2=\"$3\"></code> in the uploaded SVG file.",
"uploaded-animate-svg": "Found \"animate\" tag that might be changing href, using the \"from\" attribute <code><$1 $2=\"$3\"></code> in the uploaded SVG file.",
"uploaded-setting-event-handler-svg": "Setting event-handler attributes is blocked, found <code><$1 $2=\"$3\"></code> in the uploaded SVG file.",
"period-pm": "Text indicating the second period of the day when using a 12-hour calendar.",
"pagecategories": "Used in the categories section of pages.\n\nFollowed by a colon and a list of categories.\n\nParameters:\n* $1 - number of categories\n{{Identical|Category}}",
"pagecategorieslink": "{{notranslate}}",
- "category_header": "In category description page. Parameters:\n* $1 - category name\nSee also:\n* {{msg-mw|Category-media-header}}",
+ "category_header": "In category description page, be aware that this is not about biology, but about bibliography. Parameters:\n* $1 - category name\nSee also:\n* {{msg-mw|Category-media-header}}",
"subcategories": "Used as a header on category pages that have subcategories.\n{{Identical|Subcategory}}",
"category-media-header": "In category description page. Parameters:\n* $1 - category name\nSee also:\n* {{msg-mw|Category header}}",
"category-empty": "The text displayed in category page when that category is empty",
"recentchanges-summary": "Summary of [[Special:RecentChanges]].",
"recentchangestext": "Text in [[Special:RecentChanges]]",
"recentchanges-noresult": "Used in [[Special:RecentChanges]], [[Special:RecentChangesLinked]], and [[Special:Watchlist]] when there are no changes to be shown.",
+ "recentchanges-timeout": "Used in [[Special:RecentChanges]], [[Special:RecentChangesLinked]], and [[Special:Watchlist]] when a query times out.",
"recentchanges-feed-description": "Used in feed of RecentChanges. See example [{{canonicalurl:Special:RecentChanges|feed=atom}} feed].",
"recentchanges-label-newpage": "# Used as tooltip for {{msg-mw|Newpageletter}}.\n# Also used as legend. Preceded by {{msg-mw|Newpageletter}} and followed by {{msg-mw|Recentchanges-legend-newpage}}.",
"recentchanges-label-minor": "# Used as tooltip for {{msg-mw|Minoreditletter}}\n# Also used as legend. Preceded by {{msg-mw|Minoreditletter}}",
"recentchanges-legend-unpatrolled": "Used as legend on [[Special:RecentChanges]] and [[Special:Watchlist]].\n\nRefers to {{msg-mw|Recentchanges-label-unpatrolled}}.",
"recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with a number for the legend.",
"recentchanges-submit": "Label for submit button in [[Special:RecentChanges]]\n{{Identical|Show}}",
- "rcfilters-tag-remove": "A tooltip for the button that removes a filter from the active filters area in [[Special:RecentChanges]] and [[Special:Watchlist]] when RCFilters are enabled. \n\nParameters: $1 - Tag label",
+ "rcfilters-tag-remove": "A tooltip for the button that removes a filter from the active filters area in [[Special:RecentChanges]] and [[Special:Watchlist]] when RCFilters are enabled. \n\nParameters: $1 - Tag label\n{{Identical|Remove}}",
"rcfilters-legend-heading": "Used as a heading for legend box on [[Special:RecentChanges]] and [[Special:Watchlist]] when RCFilters are enabled.",
"rcfilters-other-review-tools": "Used as a heading for the community collection of other links on [[Special:RecentChanges]] when RCFilters are enabled.",
"rcfilters-group-results-by-page": "A label for the checkbox describing whether the results in [[Special:RecentChanges]] are grouped by page when RCFilters are enabled.",
"rcfilters-view-namespaces-tooltip": "Tooltip for the button that loads the namespace view in [[Special:RecentChanges]]",
"rcfilters-view-tags-tooltip": "Tooltip for the button that loads the tags view in [[Special:RecentChanges]]",
"rcfilters-view-return-to-default-tooltip": "Tooltip for the button that returns to the default filter view in [[Special:RecentChanges]]",
- "rcfilters-view-tags-help-icon-tooltip": "Tooltip for the help button that leads user to [[mw:Special:MyLanguage/Help:New_filters_for_edit_review/Advanced_filters#tags|Help page]] for Tagged Edits",
+ "rcfilters-view-tags-help-icon-tooltip": "Tooltip for the help button that leads user to Special:Tags page",
"rcfilters-liveupdates-button": "Label for the button to enable or disable live updates on [[Special:RecentChanges]]",
"rcfilters-liveupdates-button-title-on": "Title for the button to enable or disable live updates on [[Special:RecentChanges]] when the feature is ON.",
"rcfilters-liveupdates-button-title-off": "Title for the button to enable or disable live updates on [[Special:RecentChanges]] when the feature is OFF.",
/* jQuery */
'jquery' => [
- 'scripts' => ( $GLOBALS['wgUsejQueryThree'] ?
- [
- 'resources/lib/jquery/jquery3.js',
- 'resources/lib/jquery/jquery.migrate.js',
- ] : [
- 'resources/lib/jquery/jquery.js',
- ]
- ),
+ 'scripts' => [
+ 'resources/lib/jquery/jquery3.js',
+ 'resources/lib/jquery/jquery.migrate.js',
+ ],
'raw' => true,
'targets' => [ 'desktop', 'mobile' ],
],
'targets' => [ 'desktop', 'mobile' ],
],
'jquery.badge' => [
+ 'deprecated' => 'Please use Notifications instead.',
'scripts' => 'resources/src/jquery/jquery.badge.js',
'styles' => 'resources/src/jquery/jquery.badge.css',
'dependencies' => 'mediawiki.language',
'mediawiki.page.watch.ajax' => [
'scripts' => 'resources/src/mediawiki/page/watch.js',
'dependencies' => [
- 'mediawiki.page.startup',
'mediawiki.api.watch',
'mediawiki.notify',
'mediawiki.util',
],
'mediawiki.rcfilters.filters.ui' => [
'scripts' => [
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.GroupWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CheckboxInputWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ItemMenuOptionWidget.js',
'namespaces',
'invert',
'recentchanges-noresult',
+ 'recentchanges-timeout',
'quotation-marks',
],
'dependencies' => [
'jquery.makeCollapsible',
'mediawiki.language',
'mediawiki.user',
+ 'mediawiki.util',
'mediawiki.rcfilters.filters.dm',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-moderation',
* @param {Object} [tagList] Tag definition
*/
mw.rcfilters.Controller.prototype.initialize = function ( filterStructure, namespaceStructure, tagList ) {
- var parsedSavedQueries,
+ var parsedSavedQueries, pieces,
displayConfig = mw.config.get( 'StructuredChangeFiltersDisplayConfig' ),
defaultSavedQueryExists = mw.config.get( 'wgStructuredChangeFiltersDefaultSavedQueryExists' ),
controller = this,
views = {},
items = [],
- uri = new mw.Uri(),
- $changesList = $( '.mw-changeslist' ).first().contents();
+ uri = new mw.Uri();
// Prepare views
if ( namespaceStructure ) {
separator: ';',
fullCoverage: true,
filters: items
+ },
+ {
+ name: 'invertGroup',
+ type: 'boolean',
+ hidden: true,
+ filters: [ {
+ name: 'invert',
+ 'default': '0'
+ } ]
} ]
};
}
// again
this.updateStateFromUrl( false );
+ pieces = this._extractChangesListInfo( $( '#mw-content-text' ) );
+
// Update the changes list with the existing data
// so it gets processed
this.changesListModel.update(
- $changesList.length ? $changesList : 'NO_RESULTS',
- $( 'fieldset.cloptions' ).first(),
+ pieces.changes,
+ pieces.fieldset,
+ pieces.noResultsDetails === 'NO_RESULTS_TIMEOUT',
true // We're using existing DOM elements
);
}
}
};
+ /**
+ * Extracts information from the changes list DOM
+ *
+ * @param {jQuery} $root Root DOM to find children from
+ * @return {Object} Information about changes list
+ * @return {Object|string} return.changes Changes list, or 'NO_RESULTS' if there are no results
+ * (either normally or as an error)
+ * @return {string} [return.noResultsDetails] 'NO_RESULTS_NORMAL' for a normal 0-result set,
+ * 'NO_RESULTS_TIMEOUT' for no results due to a timeout, or omitted for more than 0 results
+ * @return {jQuery} return.fieldset Fieldset
+ */
+ mw.rcfilters.Controller.prototype._extractChangesListInfo = function ( $root ) {
+ var info, isTimeout,
+ $changesListContents = $root.find( '.mw-changeslist' ).first().contents(),
+ areResults = !!$changesListContents.length;
+
+ info = {
+ changes: $changesListContents.length ? $changesListContents : 'NO_RESULTS',
+ fieldset: $root.find( 'fieldset.cloptions' ).first()
+ };
+
+ if ( !areResults ) {
+ isTimeout = !!$root.find( '.mw-changeslist-timeout' ).length;
+ info.noResultsDetails = isTimeout ? 'NO_RESULTS_TIMEOUT' : 'NO_RESULTS_NORMAL';
+ }
+
+ return info;
+ };
+
/**
* Create filter data from a number, for the filters that are numerical value
*
* Reset to default filters
*/
mw.rcfilters.Controller.prototype.resetToDefaults = function () {
- this.uriProcessor.updateModelBasedOnQuery( this._getDefaultParams() );
+ this.filtersModel.updateStateFromParams( this._getDefaultParams() );
this.updateChangesList();
};
* @return {boolean} Defaults are all false
*/
mw.rcfilters.Controller.prototype.areDefaultsEmpty = function () {
- var defaultParams = this._getDefaultParams(),
- defaultFilters = this.filtersModel.getFiltersFromParameters( defaultParams );
-
- this._deleteExcludedValuesFromFilterState( defaultFilters );
-
- if ( Object.keys( defaultParams ).some( function ( paramName ) {
- return paramName.endsWith( '_color' ) && defaultParams[ paramName ] !== null;
- } ) ) {
- // There are highlights in the defaults, they're definitely
- // not empty
- return false;
- }
-
- // Defaults can change in a session, so we need to do this every time
- return Object.keys( defaultFilters ).every( function ( filterName ) {
- return !defaultFilters[ filterName ];
- } );
+ return $.isEmptyObject( this._getDefaultParams( true ) );
};
/**
.getHighlightedItems()
.map( function ( filterItem ) { return { name: filterItem.getName() }; } );
- this.filtersModel.emptyAllFilters();
- this.filtersModel.clearAllHighlightColors();
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ this.filtersModel.updateStateFromParams( {} );
this.updateChangesList();
*/
mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) {
var filterItem = this.filtersModel.getItemByName( filterName ),
- isHighlighted = filterItem.isHighlighted();
+ isHighlighted = filterItem.isHighlighted(),
+ isSelected = filterItem.isSelected();
- if ( filterItem.isSelected() || isHighlighted ) {
+ if ( isSelected || isHighlighted ) {
this.filtersModel.clearHighlightColor( filterName );
this.filtersModel.toggleFilterSelected( filterName, false );
- this.updateChangesList();
+
+ if ( isSelected ) {
+ // Only update the changes list if the filter changed
+ // its selection state. If it only changed its highlight
+ // then don't reload
+ this.updateChangesList();
+ }
+
this.filtersModel.reassessFilterInteractions( filterItem );
// Log filter grouping
*/
mw.rcfilters.Controller.prototype.toggleHighlight = function () {
this.filtersModel.toggleHighlight();
- this._updateURL();
+ this.uriProcessor.updateURL();
if ( this.filtersModel.isHighlightEnabled() ) {
mw.hook( 'RcFilters.highlight.enable' ).fire();
*/
mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
this.filtersModel.setHighlightColor( filterName, color );
- this._updateURL();
+ this.uriProcessor.updateURL();
this._trackHighlight( 'set', { name: filterName, color: color } );
};
*/
mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
this.filtersModel.clearHighlightColor( filterName );
- this._updateURL();
+ this.uriProcessor.updateURL();
this._trackHighlight( 'clear', filterName );
};
* @param {boolean} [setAsDefault=false] This query should be set as the default
*/
mw.rcfilters.Controller.prototype.saveCurrentQuery = function ( label, setAsDefault ) {
- var highlightedItems = {},
- highlightEnabled = this.filtersModel.isHighlightEnabled(),
- selectedState = this.filtersModel.getSelectedState();
-
- // Prepare highlights
- this.filtersModel.getHighlightedItems().forEach( function ( item ) {
- highlightedItems[ item.getName() + '_color' ] = highlightEnabled ?
- item.getHighlightColor() : null;
- } );
-
- // Delete all excluded filters
- this._deleteExcludedValuesFromFilterState( selectedState );
-
// Add item
this.savedQueriesModel.addNewQuery(
label || mw.msg( 'rcfilters-savedqueries-defaultlabel' ),
- {
- params: $.extend(
- true,
- {
- invert: String( Number( this.filtersModel.areNamespacesInverted() ) ),
- highlight: String( Number( this.filtersModel.isHighlightEnabled() ) )
- },
- this.filtersModel.getParametersFromFilters( selectedState )
- ),
- highlights: highlightedItems
- },
+ this.filtersModel.getCurrentParameterState( true ),
setAsDefault
);
* @param {string} queryID Query id
*/
mw.rcfilters.Controller.prototype.applySavedQuery = function ( queryID ) {
- var highlights,
- queryItem = this.savedQueriesModel.getItemByID( queryID ),
- data = this.savedQueriesModel.getItemFullData( queryID ),
- currentMatchingQuery = this.findQueryMatchingCurrentState();
+ var currentMatchingQuery,
+ params = this.savedQueriesModel.getItemParams( queryID );
+
+ currentMatchingQuery = this.findQueryMatchingCurrentState();
if (
- queryItem &&
- (
- // If there's already a query, don't reload it
- // if it's the same as the one that already exists
- !currentMatchingQuery ||
- currentMatchingQuery.getID() !== queryItem.getID()
- )
+ currentMatchingQuery &&
+ currentMatchingQuery.getID() === queryID
) {
- highlights = data.highlights;
-
- // Update model state from filters
- this.filtersModel.toggleFiltersSelected(
- // Merge filters with excluded values
- $.extend(
- true,
- {},
- this.filtersModel.getFiltersFromParameters( data.params ),
- this.filtersModel.getExcludedFiltersState()
- )
- );
-
- // Update namespace inverted property
- this.filtersModel.toggleInvertedNamespaces( !!Number( data.params.invert ) );
-
- // Update highlight state
- this.filtersModel.toggleHighlight( !!Number( data.params.highlight ) );
- this.filtersModel.getItems().forEach( function ( filterItem ) {
- var color = highlights[ filterItem.getName() + '_color' ];
- if ( color ) {
- filterItem.setHighlightColor( color );
- } else {
- filterItem.clearHighlightColor();
- }
- } );
+ // If the query we want to load is the one that is already
+ // loaded, don't reload it
+ return;
+ }
- // Check all filter interactions
- this.filtersModel.reassessFilterInteractions();
+ // Apply parameters to model
+ this.filtersModel.updateStateFromParams( params );
- this.updateChangesList();
+ this.updateChangesList();
- // Log filter grouping
- this.trackFilterGroupings( 'savedfilters' );
- }
+ // Log filter grouping
+ this.trackFilterGroupings( 'savedfilters' );
};
/**
* Check whether the current filter and highlight state exists
* in the saved queries model.
*
- * @return {boolean} Query exists
+ * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
*/
mw.rcfilters.Controller.prototype.findQueryMatchingCurrentState = function () {
- var highlightedItems = {},
- selectedState = this.filtersModel.getSelectedState();
-
- // Prepare highlights of the current query
- this.filtersModel.getItemsSupportingHighlights().forEach( function ( item ) {
- highlightedItems[ item.getName() + '_color' ] = item.getHighlightColor();
- } );
-
- // Remove anything that should be excluded from the saved query
- // this includes sticky filters and filters marked with 'excludedFromSavedQueries'
- this._deleteExcludedValuesFromFilterState( selectedState );
-
return this.savedQueriesModel.findMatchingQuery(
- {
- params: $.extend(
- true,
- {
- highlight: String( Number( this.filtersModel.isHighlightEnabled() ) ),
- invert: String( Number( this.filtersModel.areNamespacesInverted() ) )
- },
- this.filtersModel.getParametersFromFilters( selectedState )
- ),
- highlights: highlightedItems
- }
+ this.filtersModel.getCurrentParameterState( true )
);
};
- /**
- * Delete sticky filters from given object
- *
- * @param {Object} filterState Filter state
- */
- mw.rcfilters.Controller.prototype._deleteExcludedValuesFromFilterState = function ( filterState ) {
- // Remove excluded filters
- $.each( this.filtersModel.getExcludedFiltersState(), function ( filterName ) {
- delete filterState[ filterName ];
- } );
- };
-
/**
* Save the current state of the saved queries model with all
* query item representation in the user settings.
* without adding an history entry.
*/
mw.rcfilters.Controller.prototype.replaceUrl = function () {
- mw.rcfilters.UriProcessor.static.replaceState( this._getUpdatedUri() );
+ this.uriProcessor.replaceUpdatedUri();
};
/**
updateMode = updateMode === undefined ? this.FILTER_CHANGE : updateMode;
if ( updateMode === this.FILTER_CHANGE ) {
- this._updateURL( params );
+ this.uriProcessor.updateURL( params );
}
if ( updateMode === this.FILTER_CHANGE || updateMode === this.SHOW_NEW_CHANGES ) {
this.changesListModel.invalidate();
this.changesListModel.update(
$changesListContent,
$fieldset,
+ pieces.noResultsDetails === 'NO_RESULTS_TIMEOUT',
false,
// separator between old and new changes
updateMode === this.SHOW_NEW_CHANGES || updateMode === this.LIVE_UPDATE
* 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 () {
- var savedFilters,
- data = ( !mw.user.isAnon() && this.savedQueriesModel.getItemFullData( this.savedQueriesModel.getDefault() ) ) || {};
-
- if ( !$.isEmptyObject( data ) ) {
- // Merge saved filter state with sticky filter values
- savedFilters = $.extend(
- true, {},
- this.filtersModel.getFiltersFromParameters( data.params ),
- this.filtersModel.getStickyFiltersState()
- );
-
- // Return parameter representation
- return $.extend( true, {},
- this.filtersModel.getParametersFromFilters( savedFilters ),
- data.highlights,
- { highlight: data.params.highlight, invert: data.params.invert }
- );
- }
- return this.filtersModel.getDefaultParams();
- };
-
- /**
- * Update the URL of the page to reflect current filters
- *
- * This should not be called directly from outside the controller.
- * If an action requires changing the URL, it should either use the
- * highlighting actions below, or call #updateChangesList which does
- * the uri corrections already.
- *
- * @param {Object} [params] Extra parameters to add to the API call
- */
- mw.rcfilters.Controller.prototype._updateURL = function ( params ) {
- var currentUri = new mw.Uri(),
- updatedUri = this._getUpdatedUri();
-
- updatedUri.extend( params || {} );
-
- if (
- this.uriProcessor.getVersion( currentUri.query ) !== 2 ||
- this.uriProcessor.isNewState( currentUri.query, updatedUri.query )
- ) {
- mw.rcfilters.UriProcessor.static.replaceState( updatedUri );
+ mw.rcfilters.Controller.prototype._getDefaultParams = function ( excludeHiddenParams ) {
+ if ( this.savedQueriesModel.getDefault() ) {
+ return this.savedQueriesModel.getDefaultParams( excludeHiddenParams );
+ } else {
+ return this.filtersModel.getDefaultParams( excludeHiddenParams );
}
};
- /**
- * Get an updated mw.Uri object based on the model state
- *
- * @return {mw.Uri} Updated Uri
- */
- mw.rcfilters.Controller.prototype._getUpdatedUri = function () {
- var uri = new mw.Uri();
-
- // Minimize url
- uri.query = this.uriProcessor.minimizeQuery(
- $.extend(
- true,
- {},
- // We want to retain unrecognized params
- // The uri params from model will override
- // any recognized value in the current uri
- // query, retain unrecognized params, and
- // the result will then be minimized
- uri.query,
- this.uriProcessor.getUriParametersFromModel(),
- { urlversion: '2' }
- )
- );
-
- return uri;
- };
-
/**
* Query the list of changes from the server for the current filters
*
* @return {jQuery.Promise} Promise object resolved with { content, status }
*/
mw.rcfilters.Controller.prototype._queryChangesList = function ( counterId, params ) {
- var uri = this._getUpdatedUri(),
- stickyParams = this.filtersModel.getStickyParams(),
+ var uri = this.uriProcessor.getUpdatedUri(),
+ stickyParams = this.filtersModel.getStickyParamsValues(),
requestId,
latestRequest;
return this._queryChangesList( 'updateChangesList' )
.then(
function ( data ) {
- var $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) ),
- pieces = {
- // Changes list
- changes: $parsed.find( '.mw-changeslist' ).first().contents(),
- // Fieldset
- fieldset: $parsed.find( 'fieldset.cloptions' ).first()
- };
-
- if ( pieces.changes.length === 0 ) {
- pieces.changes = 'NO_RESULTS';
- }
+ var $parsed = $( '<div>' ).append( $( $.parseHTML( data.content ) ) );
- return pieces;
- }
+ return this._extractChangesListInfo( $parsed );
+
+ }.bind( this )
);
};
@import 'mediawiki.mixins.animation';
+@import 'mediawiki.ui/variables';
@import 'mw.rcfilters.mixins';
@rcfilters-spinner-width: 70px;
min-height: @rcfilters-wl-head-min-height;
}
- body:not( .mw-rcfilters-ui-initialized ) {
- .mw-recentchanges-toplinks-content.mw-rcfilters-toplinks-collapsed {
- display: none;
+ .mw-recentchanges-toplinks {
+ margin-bottom: 0.5em;
+ padding: 0 0.5em 0.5em 0.5em;
+ border: 1px solid transparent;
+
+ &:not( .mw-recentchanges-toplinks-collapsed ) {
+ // Same as the legend
+ border: 1px solid @colorGray12;
}
+ }
- .mw-recentchanges-toplinks-title.mw-rcfilters-toplinks-collapsed {
- // Hide, but keep the placement so we don't jump
- visibility: hidden;
+ body:not( .mw-rcfilters-ui-initialized ) {
+ .mw-recentchanges-toplinks.mw-recentchanges-toplinks-collapsed {
+ // Similar to the watchlist-details hack, we are going to make this float left
+ // while loading to prevent jumpiness in the min-height calculation
+ float: left;
+
+ .mw-recentchanges-toplinks-content {
+ display: none;
+ }
}
.rcfilters-head {
}
.mw-changeslist {
- &-empty {
- // Hide the 'empty' message when we load rcfilters
- // since we replace it anyways with a specific
- // message of our own
- display: none;
- }
-
// Reserve space for the highlight circles
ul,
table.mw-enhanced-rc {
}
}
+ // Temporarily hide any 'empty' or 'timeout' message while we
+ // load rcfilters.
+ .mw-changeslist-empty,
+ .mw-changeslist-timeout {
+ display: none;
+ }
+
body.mw-rcfilters-ui-loading .mw-changeslist {
opacity: 0.5;
}
display: inline-block;
width: 12px;
height: 12px;
- background-color: #c8ccd1;
+ background-color: @colorGray12;
border-radius: 100%;
.animation( rcfiltersBouncedelay 1.5s ease-in-out -0.16s infinite both );
}
transform: scale( 0.7 );
}
40% {
- background-color: #a2a9b1;
+ background-color: @colorGray10;
-webkit-transform: scale( 1 );
transform: scale( 1 );
}
transform: scale( 0.7 );
}
40% {
- background-color: #a2a9b1;
+ background-color: @colorGray10;
-moz-transform: scale( 0.7 );
transform: scale( 1 );
}
transform: scale( 0.7 );
}
40% {
- background-color: #a2a9b1;
+ background-color: @colorGray10;
transform: scale( 1 );
}
}