* (T222388) API modules can now be specified as an ObjectFactory spec,
allowing the construction of modules that require services to be injected
in their constructor.
+* (T117736) The function signature of SpecialContributions::getForm::filters
+ has changed. It now expects definitions of additional filter fields as array
+ rather than string.
=== External library changes in 1.34 ===
'SpecialContributions::getForm::filters': Called with a list of filters to render
on Special:Contributions.
$sp: SpecialContributions object, for context
-&$filters: List of filters rendered as HTML
+&$filters: List of filter object definitions (compatible with OOUI form)
'SpecialListusersDefaultQuery': Called right before the end of
UsersPager::getDefaultQuery().
* Return the HTML for a message box.
* @since 1.31
* @param string $html of contents of box
- * @param string $className corresponding to box
+ * @param string|array $className corresponding to box
* @param string $heading (optional)
* @return string of HTML representing a box.
*/
/**
* Return a warning box.
* @since 1.31
+ * @since 1.34 $className optional parameter added
* @param string $html of contents of box
+ * @param string $className (optional) corresponding to box
* @return string of HTML representing a warning box.
*/
- public static function warningBox( $html ) {
- return self::messageBox( $html, 'warningbox' );
+ public static function warningBox( $html, $className = '' ) {
+ return self::messageBox( $html, [ 'warningbox', $className ] );
}
/**
* Return an error box.
* @since 1.31
+ * @since 1.34 $className optional parameter added
* @param string $html of contents of error box
* @param string $heading (optional)
+ * @param string $className (optional) corresponding to box
* @return string of HTML representing an error box.
*/
- public static function errorBox( $html, $heading = '' ) {
- return self::messageBox( $html, 'errorbox', $heading );
+ public static function errorBox( $html, $heading = '', $className = '' ) {
+ return self::messageBox( $html, [ 'errorbox', $className ], $heading );
}
/**
* Return a success box.
* @since 1.31
+ * @since 1.34 $className optional parameter added
* @param string $html of contents of box
+ * @param string $className (optional) corresponding to box
* @return string of HTML representing a success box.
*/
- public static function successBox( $html ) {
- return self::messageBox( $html, 'successbox' );
+ public static function successBox( $html, $className = '' ) {
+ return self::messageBox( $html, [ 'successbox', $className ] );
}
/**
* 'content'- whether the actual content of the slots should be
* preloaded.
* @param int $queryFlags
- * @param Title|null $title
+ * @param Title|null $title The title to which all the revision rows belong, if there
+ * is such a title and the caller has it handy, so we don't have to look it up again.
+ * If this parameter is given and any of the rows has a rev_page_id that is different
+ * from $title->getArticleID(), an InvalidArgumentException is thrown.
+ *
* @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
* and an array of errors for the revisions failed to fetch.
*/
// since the message may not be compatible.
if ( !in_array( $this->msg, LoginHelper::getValidErrorMessages() ) ) {
parent::report();
+ return;
}
- // Message is valid. Redirec to Special:Userlogin
+ // Message is valid. Redirect to Special:Userlogin
$context = RequestContext::getMain();
* - originalRequest Information about the original request (as a WebRequest object or
* an associative array with 'ip' and 'userAgent').
* @codingStandardsIgnoreStart
- * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:WebRequest|array{ip:string,userAgent:string}} $options
+ * @phan-param array{timeout?:int|string,connectTimeout?:int|string,postData?:array,proxy?:string,noProxy?:bool,sslVerifyHost?:bool,sslVerifyCert?:bool,caInfo?:string,maxRedirects?:int,followRedirects?:bool,userAgent?:string,method?:string,logger?:\Psr\Log\LoggerInterface,username?:string,password?:string,originalRequest?:\WebRequest|array{ip:string,userAgent:string}} $options
* @codingStandardsIgnoreEnd
* @param string $caller The method making this request, for profiling
* @throws RuntimeException
* @return MWHttpRequest
* @see MWHttpRequest::__construct
- * @suppress PhanUndeclaredTypeParameter
*/
public function create( $url, array $options = [], $caller = __METHOD__ ) {
if ( !Http::$httpEngine ) {
$this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
$this->parent->getHelpBox( 'config-db-web-help' );
if ( $noCreateMsg ) {
- $s .= $this->parent->getWarningBox( wfMessage( $noCreateMsg )->plain() );
+ $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' );
} else {
$s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
}
);
}
$text = $msg->useDatabase( false )->plain();
- $this->output->addHTML( $this->getErrorBox( $text ) );
+ $box = Html::errorBox( $text, '', 'config-error-box' );
+ $this->output->addHTML( $box );
}
/**
$text = $status->getWikiText();
if ( $status->isOK() ) {
- $box = $this->getWarningBox( $text );
+ $box = Html::warningBox( $text, 'config-warning-box' );
} else {
- $box = $this->getErrorBox( $text );
+ $box = Html::errorBox( $text, '', 'config-error-box' );
}
$this->output->addHTML( $box );
}
} else {
$skinHtml .=
- $this->parent->getWarningBox( wfMessage( 'config-skins-missing' )->plain() ) .
+ Html::warningBox( wfMessage( 'config-skins-missing' )->plain(), 'config-warning-box' ) .
Html::hidden( 'config_wgDefaultSkin', $chosenSkinName );
}
}
$this->startForm();
- $s = $this->parent->getWarningBox( wfMessage( 'config-help-restart' )->plain() );
+ $s = Html::warningBox( wfMessage( 'config-help-restart' )->plain(), '', 'config-warning-box' );
$this->addHTML( $s );
$this->endForm( 'restart' );
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\MediaWikiServices;
-use MediaWiki\Widget\DateInputWidget;
/**
* Special:Contributions, show user contributions in a paged list
$out = $this->getOutput();
// Modules required for viewing the list of contributions (also when included on other pages)
$out->addModuleStyles( [
+ 'jquery.makeCollapsible.styles',
'mediawiki.interface.helpers.styles',
'mediawiki.special',
'mediawiki.special.changeslist',
] );
- $out->addModules( 'mediawiki.special.recentchanges' );
+ $out->addModules( [
+ 'mediawiki.special.recentchanges',
+ // Certain skins e.g. Minerva might have disabled this module.
+ 'mediawiki.page.ready'
+ ] );
$this->addHelpLink( 'Help:User contributions' );
$this->opts = [];
if ( !strlen( $target ) ) {
if ( !$this->including() ) {
- $out->addHTML( $this->getForm() );
+ $out->addHTML( $this->getForm( $this->opts ) );
}
return;
if ( ExternalUserNames::isExternal( $target ) ) {
$userObj = User::newFromName( $target, false );
if ( !$userObj ) {
- $out->addHTML( $this->getForm() );
+ $out->addHTML( $this->getForm( $this->opts ) );
return;
}
} else {
$nt = Title::makeTitleSafe( NS_USER, $target );
if ( !$nt ) {
- $out->addHTML( $this->getForm() );
+ $out->addHTML( $this->getForm( $this->opts ) );
return;
}
$userObj = User::newFromName( $nt->getText(), false );
if ( !$userObj ) {
- $out->addHTML( $this->getForm() );
+ $out->addHTML( $this->getForm( $this->opts ) );
return;
}
$id = $userObj->getId();
}
$ns = $request->getVal( 'namespace', null );
- if ( $ns !== null && $ns !== '' ) {
+ if ( $ns !== null && $ns !== '' && $ns !== 'all' ) {
$this->opts['namespace'] = intval( $ns );
} else {
$this->opts['namespace'] = '';
}
+ // Backwards compatibility: Before using OOUI form the old HTML form had
+ // fields for nsInvert and associated. These have now been replaced with the
+ // wpFilters query string parameters. These are retained to keep old URIs working.
$this->opts['associated'] = $request->getBool( 'associated' );
$this->opts['nsInvert'] = (bool)$request->getVal( 'nsInvert' );
+ $nsFilters = $request->getArray( 'wpfilters', null );
+ if ( $nsFilters !== null ) {
+ $this->opts['associated'] = in_array( 'associated', $nsFilters );
+ $this->opts['nsInvert'] = in_array( 'nsInvert', $nsFilters );
+ }
+
$this->opts['tagfilter'] = (string)$request->getVal( 'tagfilter' );
// Allows reverts to have the bot flag in recent changes. It is just here to
"<div class=\"mw-negative-namespace-not-supported error\">\n\$1\n</div>",
[ 'negative-namespace-not-supported' ]
);
- $out->addHTML( $this->getForm() );
+ $out->addHTML( $this->getForm( $this->opts ) );
return;
}
$this->addFeedLinks( $feedParams );
if ( Hooks::run( 'SpecialContributionsBeforeMainOutput', [ $id, $userObj, $this ] ) ) {
- if ( !$this->including() ) {
- $out->addHTML( $this->getForm() );
- }
$pager = new ContribsPager( $this->getContext(), [
'target' => $target,
'namespace' => $this->opts['namespace'],
'nsInvert' => $this->opts['nsInvert'],
'associated' => $this->opts['associated'],
], $this->getLinkRenderer() );
+ if ( !$this->including() ) {
+ $out->addHTML( $this->getForm( $this->opts ) );
+ }
if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
// Valid range, but outside CIDR limit.
/**
* Generates the namespace selector form with hidden attributes.
+ * @param array $pagerOptions with keys contribs, user, deletedOnly, limit, target, topOnly,
+ * newOnly, hideMinor, namespace, associated, nsInvert, tagfilter, year, start, end
* @return string HTML fragment
*/
- protected function getForm() {
+ protected function getForm( array $pagerOptions ) {
$this->opts['title'] = $this->getPageTitle()->getPrefixedText();
- if ( !isset( $this->opts['target'] ) ) {
- $this->opts['target'] = '';
- } else {
- $this->opts['target'] = str_replace( '_', ' ', $this->opts['target'] );
- }
-
- if ( !isset( $this->opts['namespace'] ) ) {
- $this->opts['namespace'] = '';
- }
-
- if ( !isset( $this->opts['nsInvert'] ) ) {
- $this->opts['nsInvert'] = '';
- }
-
- if ( !isset( $this->opts['associated'] ) ) {
- $this->opts['associated'] = false;
- }
-
- if ( !isset( $this->opts['start'] ) ) {
- $this->opts['start'] = '';
- }
-
- if ( !isset( $this->opts['end'] ) ) {
- $this->opts['end'] = '';
- }
-
- if ( !isset( $this->opts['tagfilter'] ) ) {
- $this->opts['tagfilter'] = '';
- }
-
- if ( !isset( $this->opts['topOnly'] ) ) {
- $this->opts['topOnly'] = false;
- }
-
- if ( !isset( $this->opts['newOnly'] ) ) {
- $this->opts['newOnly'] = false;
- }
-
- if ( !isset( $this->opts['hideMinor'] ) ) {
- $this->opts['hideMinor'] = false;
- }
-
// Modules required only for the form
$this->getOutput()->addModules( [
'mediawiki.userSuggest',
] );
$this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
$this->getOutput()->enableOOUI();
-
- $form = Html::openElement(
- 'form',
- [
- 'method' => 'get',
- 'action' => wfScript(),
- 'class' => 'mw-contributions-form'
- ]
- );
+ $fields = [];
# Add hidden params for tracking except for parameters in $skipParameters
$skipParameters = [
'nsInvert',
'deletedOnly',
'target',
- 'contribs',
'year',
'month',
'start',
if ( in_array( $name, $skipParameters ) ) {
continue;
}
- $form .= "\t" . Html::hidden( $name, $value ) . "\n";
- }
-
- $tagFilter = ChangeTags::buildTagFilterSelector(
- $this->opts['tagfilter'], false, $this->getContext() );
-
- if ( $tagFilter ) {
- $filterSelection = Html::rawElement(
- 'div',
- [],
- implode( "\u{00A0}", $tagFilter )
- );
- } else {
- $filterSelection = Html::rawElement( 'div', [], '' );
- }
-
- $labelUsername = Xml::label(
- $this->msg( 'sp-contributions-username' )->text(),
- 'mw-target-user-or-ip'
- );
- $input = Html::input(
- 'target',
- $this->opts['target'],
- 'text',
- [
- 'id' => 'mw-target-user-or-ip',
- 'size' => '40',
- 'class' => [
- 'mw-input',
- 'mw-ui-input-inline',
- 'mw-autocomplete-user', // used by mediawiki.userSuggest
- ],
- ] + (
- // Only autofocus if target hasn't been specified
- $this->opts['target'] ? [] : [ 'autofocus' => true ]
- )
- );
-
- $targetSelection = Html::rawElement(
- 'div',
- [],
- $labelUsername . ' ' . $input . ' '
- );
- $hidden = $this->opts['namespace'] === '' ? ' mw-input-hidden' : '';
- $namespaceSelection = Xml::tags(
- 'div',
- [],
- Xml::label(
- $this->msg( 'namespace' )->text(),
- 'namespace'
- ) . "\u{00A0}" .
- Html::namespaceSelector(
- [ 'selected' => $this->opts['namespace'], 'all' => '', 'in-user-lang' => true ],
- [
- 'name' => 'namespace',
- 'id' => 'namespace',
- 'class' => 'namespaceselector',
- ]
- ) . "\u{00A0}" .
- Html::rawElement(
- 'span',
- [ 'class' => 'mw-input-with-label' . $hidden ],
- Xml::checkLabel(
- $this->msg( 'invert' )->text(),
- 'nsInvert',
- 'nsinvert',
- $this->opts['nsInvert'],
- [
- 'title' => $this->msg( 'tooltip-invert' )->text(),
- 'class' => 'mw-input'
- ]
- ) . "\u{00A0}"
- ) .
- Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' . $hidden ],
- Xml::checkLabel(
- $this->msg( 'namespace_association' )->text(),
- 'associated',
- 'nsassociated',
- $this->opts['associated'],
- [
- 'title' => $this->msg( 'tooltip-namespace_association' )->text(),
- 'class' => 'mw-input'
- ]
- ) . "\u{00A0}"
- )
- );
+ $fields[$name] = [
+ 'name' => $name,
+ 'type' => 'hidden',
+ 'default' => $value,
+ ];
+ }
+
+ $target = $this->opts['target'] ?? null;
+ $fields['target'] = [
+ 'type' => 'text',
+ 'cssclass' => 'mw-autocomplete-user mw-ui-input-inline mw-input',
+ 'default' => $target ?
+ str_replace( '_', ' ', $target ) : '' ,
+ 'label' => $this->msg( 'sp-contributions-username' )->text(),
+ 'name' => 'target',
+ 'id' => 'mw-target-user-or-ip',
+ 'size' => 40,
+ 'autofocus' => !$target,
+ 'section' => 'contribs-top',
+ ];
- $filters = [];
+ $ns = $this->opts['namespace'] ?? null;
+ $fields['namespace'] = [
+ 'type' => 'namespaceselect',
+ 'label' => $this->msg( 'namespace' )->text(),
+ 'name' => 'namespace',
+ 'cssclass' => 'namespaceselector',
+ 'default' => $ns,
+ 'id' => 'namespace',
+ 'section' => 'contribs-top',
+ ];
+ $request = $this->getRequest();
+ $nsFilters = $request->getArray( 'wpfilters' );
+ $fields['nsFilters'] = [
+ 'class' => 'HTMLMultiSelectField',
+ 'label' => '',
+ 'name' => 'wpfilters',
+ 'flatlist' => true,
+ // Only shown when namespaces are selected.
+ 'cssclass' => $ns === '' ?
+ 'contribs-ns-filters mw-input-with-label mw-input-hidden' :
+ 'contribs-ns-filters mw-input-with-label',
+ // `contribs-ns-filters` class allows these fields to be toggled on/off by JavaScript.
+ // See resources/src/mediawiki.special.recentchanges.js
+ 'infusable' => true,
+ 'options' => [
+ $this->msg( 'invert' )->text() => 'nsInvert',
+ $this->msg( 'namespace_association' )->text() => 'associated',
+ ],
+ 'default' => $nsFilters,
+ 'section' => 'contribs-top',
+ ];
+ $fields['tagfilter'] = [
+ 'type' => 'tagfilter',
+ 'cssclass' => 'mw-tagfilter-input',
+ 'id' => 'tagfilter',
+ 'label-message' => [ 'tag-filter', 'parse' ],
+ 'name' => 'tagfilter',
+ 'size' => 20,
+ 'section' => 'contribs-top',
+ ];
if ( MediaWikiServices::getInstance()
- ->getPermissionManager()
- ->userHasRight( $this->getUser(), 'deletedhistory' )
+ ->getPermissionManager()
+ ->userHasRight( $this->getUser(), 'deletedhistory' )
) {
- $filters[] = Html::rawElement(
- 'span',
- [ 'class' => 'mw-input-with-label' ],
- Xml::checkLabel(
- $this->msg( 'history-show-deleted' )->text(),
- 'deletedOnly',
- 'mw-show-deleted-only',
- $this->opts['deletedOnly'],
- [ 'class' => 'mw-input' ]
- )
- );
- }
-
- $filters[] = Html::rawElement(
- 'span',
- [ 'class' => 'mw-input-with-label' ],
- Xml::checkLabel(
- $this->msg( 'sp-contributions-toponly' )->text(),
- 'topOnly',
- 'mw-show-top-only',
- $this->opts['topOnly'],
- [ 'class' => 'mw-input' ]
- )
- );
- $filters[] = Html::rawElement(
- 'span',
- [ 'class' => 'mw-input-with-label' ],
- Xml::checkLabel(
- $this->msg( 'sp-contributions-newonly' )->text(),
- 'newOnly',
- 'mw-show-new-only',
- $this->opts['newOnly'],
- [ 'class' => 'mw-input' ]
- )
- );
- $filters[] = Html::rawElement(
- 'span',
- [ 'class' => 'mw-input-with-label' ],
- Xml::checkLabel(
- $this->msg( 'sp-contributions-hideminor' )->text(),
- 'hideMinor',
- 'mw-hide-minor-edits',
- $this->opts['hideMinor'],
- [ 'class' => 'mw-input' ]
- )
- );
+ $fields['deletedOnly'] = [
+ 'type' => 'check',
+ 'id' => 'mw-show-deleted-only',
+ 'label' => $this->msg( 'history-show-deleted' )->text(),
+ 'name' => 'deletedOnly',
+ 'section' => 'contribs-top',
+ ];
+ }
+
+ $fields['topOnly'] = [
+ 'type' => 'check',
+ 'id' => 'mw-show-top-only',
+ 'label' => $this->msg( 'sp-contributions-toponly' )->text(),
+ 'name' => 'topOnly',
+ 'section' => 'contribs-top',
+ ];
+ $fields['newOnly'] = [
+ 'type' => 'check',
+ 'id' => 'mw-show-new-only',
+ 'label' => $this->msg( 'sp-contributions-newonly' )->text(),
+ 'name' => 'newOnly',
+ 'section' => 'contribs-top',
+ ];
+ $fields['hideMinor'] = [
+ 'type' => 'check',
+ 'cssclass' => 'mw-hide-minor-edits',
+ 'id' => 'mw-show-new-only',
+ 'label' => $this->msg( 'sp-contributions-hideminor' )->text(),
+ 'name' => 'hideMinor',
+ 'section' => 'contribs-top',
+ ];
+ // Allow additions at this point to the filters.
+ $rawFilters = [];
Hooks::run(
'SpecialContributions::getForm::filters',
- [ $this, &$filters ]
- );
-
- $extraOptions = Html::rawElement(
- 'div',
- [],
- implode( '', $filters )
+ [ $this, &$rawFilters ]
);
+ foreach ( $rawFilters as $filter ) {
+ // Backwards compatibility support for previous hook function signature.
+ if ( is_string( $filter ) ) {
+ $fields[] = [
+ 'type' => 'info',
+ 'default' => $filter,
+ 'raw' => true,
+ 'section' => 'contribs-top',
+ ];
+ wfDeprecated(
+ __METHOD__ .
+ ' returning string[]',
+ '1.33'
+ );
+ } else {
+ // Preferred append method.
+ $fields[] = $filter;
+ }
+ }
- $dateRangeSelection = Html::rawElement(
- 'div',
- [],
- Xml::label( $this->msg( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' .
- new DateInputWidget( [
- 'infusable' => true,
- 'id' => 'mw-date-start',
- 'name' => 'start',
- 'value' => $this->opts['start'],
- 'longDisplayFormat' => true,
- ] ) . '<br>' .
- Xml::label( $this->msg( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' .
- new DateInputWidget( [
- 'infusable' => true,
- 'id' => 'mw-date-end',
- 'name' => 'end',
- 'value' => $this->opts['end'],
- 'longDisplayFormat' => true,
- ] )
- );
+ $fields['start'] = [
+ 'type' => 'date',
+ 'default' => '',
+ 'id' => 'mw-date-start',
+ 'label' => $this->msg( 'date-range-from' )->text(),
+ 'name' => 'start',
+ 'section' => 'contribs-date',
+ ];
+ $fields['end'] = [
+ 'type' => 'date',
+ 'default' => '',
+ 'id' => 'mw-date-end',
+ 'label' => $this->msg( 'date-range-to' )->text(),
+ 'name' => 'end',
+ 'section' => 'contribs-date',
+ ];
- $submit = Xml::tags( 'div', [],
- Html::submitButton(
- $this->msg( 'sp-contributions-submit' )->text(),
- [ 'class' => 'mw-submit' ], [ 'mw-ui-progressive' ]
+ $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+ $htmlForm
+ ->setMethod( 'get' )
+ // When offset is defined, the user is paging through results
+ // so we hide the form by default to allow users to focus on browsing
+ // rather than defining search parameters
+ ->setCollapsibleOptions(
+ ( $pagerOptions['target'] ?? null ) ||
+ ( $pagerOptions['start'] ?? null ) ||
+ ( $pagerOptions['end'] ?? null )
)
- );
-
- $form .= Xml::fieldset(
- $this->msg( 'sp-contributions-search' )->text(),
- $targetSelection .
- $namespaceSelection .
- $filterSelection .
- $extraOptions .
- $dateRangeSelection .
- $submit,
- [ 'class' => 'mw-contributions-table' ]
- );
+ ->setAction( wfScript() )
+ ->setSubmitText( $this->msg( 'sp-contributions-submit' )->text() )
+ ->setWrapperLegend( $this->msg( 'sp-contributions-search' )->text() );
$explain = $this->msg( 'sp-contributions-explain' );
if ( !$explain->isBlank() ) {
- $form .= Html::rawElement(
- 'p', [ 'id' => 'mw-sp-contributions-explain' ], $explain->parse()
- );
+ $htmlForm->addFooterText( "<p id='mw-sp-contributions-explain'>{$explain->parse()}</p>" );
}
- $form .= Xml::closeElement( 'form' );
+ $htmlForm->loadData();
- return $form;
+ return $htmlForm->getHTML( false );
}
/**
function getQueryInfo() {
$opts = $this->opts;
$conds = [];
- $imgQuery = LocalFile::getQueryInfo();
- $tables = $imgQuery['tables'];
- $fields = [ 'img_name', 'img_timestamp' ] + $imgQuery['fields'];
+ $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
+ $tables = [ 'image' ] + $actorQuery['tables'];
+ $fields = [ 'img_name', 'img_timestamp' ] + $actorQuery['fields'];
$options = [];
- $jconds = $imgQuery['joins'];
+ $jconds = $actorQuery['joins'];
$user = $opts->getValue( 'user' );
if ( $user !== '' ) {
'LEFT JOIN',
[
'ug_group' => $groupsWithBotPermission,
- 'ug_user = ' . $imgQuery['fields']['img_user'],
+ 'ug_user = ' . $actorQuery['fields']['img_user'],
'ug_expiry IS NULL OR ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() )
]
];
'JOIN',
[
'rc_title = img_name',
- 'rc_actor = ' . $imgQuery['fields']['img_actor'],
+ 'rc_actor = ' . $actorQuery['fields']['img_actor'],
'rc_timestamp = img_timestamp'
]
];
"server name detection may fail in command line scripts.", false, true );
$this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
// This is named --mwdebug, because --debug would conflict in the phpunit.php CLI script.
- $this->addOption( 'mwdebug', 'Enable built-in MediaWiki development settings', false, true );
+ $this->addOption( 'mwdebug', 'Enable built-in MediaWiki development settings', false, false );
# Save generic options to display them separately in help
$this->mGenericParameters = $this->mParams;
$this->output(
sprintf( "%s %s: %6.2f%% done on %s; ETA %s [%d/%d] %.2f/sec <%.2f%% updated>\n",
- wfWikiID(),
+ WikiMap::getCurrentWikiDbDomain()->getId(),
wfTimestamp( TS_DB, intval( $now ) ),
$portion * 100.0,
$this->table,
* @return bool
*/
private function checkDupes( $doDelete = false ) {
+ $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
if ( $this->hasUniqueIndex() ) {
- echo wfWikiID() . " already has a unique index on its user table.\n";
+ echo "$dbDomain already has a unique index on its user table.\n";
return true;
}
$dupes = $this->getDupes();
$count = count( $dupes );
- $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" );
+ $this->out( "Found $count accounts with duplicate records on $dbDomain.\n" );
$this->trimmed = 0;
$this->reassigned = 0;
$this->failed = 0;
if ( $this->trimmed > 0 ) {
if ( $doDelete ) {
- $this->out( "$this->trimmed duplicate user records were deleted from "
- . wfWikiID() . ".\n" );
+ $this->out(
+ "$this->trimmed duplicate user records were deleted from $dbDomain.\n" );
} else {
- $this->out( "$this->trimmed duplicate user accounts were found on "
- . wfWikiID() . " which can be removed safely.\n" );
+ $this->out(
+ "$this->trimmed duplicate user accounts were found on $dbDomain " .
+ "which can be removed safely.\n"
+ );
}
}
'resources/src/mediawiki.special/special.less',
'resources/src/mediawiki.special/apisandbox.css',
'resources/src/mediawiki.special/comparepages.less',
+ 'resources/src/mediawiki.special/contributions.less',
'resources/src/mediawiki.special/edittags.css',
'resources/src/mediawiki.special/movePage.css',
'resources/src/mediawiki.special/newpages.less',
'mediawiki.special.contributions' => [
'scripts' => 'resources/src/mediawiki.special.contributions.js',
'dependencies' => [
+ 'jquery.makeCollapsible',
+ 'oojs-ui',
'mediawiki.widgets.DateInputWidget',
'mediawiki.jqueryMsg',
- ]
+ ],
+ 'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.edittags' => [
'scripts' => 'resources/src/mediawiki.special.edittags.js',
'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.less',
],
'mediawiki.special.recentchanges' => [
+ 'dependencies' => [
+ 'mediawiki.widgets'
+ ],
'scripts' => 'resources/src/mediawiki.special.recentchanges.js',
'targets' => [ 'desktop', 'mobile' ],
],
font-weight: bold;
}
+// on smaller screen, set .watchlistDetail to full width
+// so that the spinner doesn't appear beside it. T225127#5518870
+@media screen and ( max-width: @width-breakpoint-tablet ) {
+ .client-js {
+ /* stylelint-disable-next-line selector-class-pattern */
+ .watchlistDetails {
+ float: none;
+ width: auto;
+ }
+ }
+}
+
@-webkit-keyframes rcfiltersBouncedelay {
// 50% equals 800ms
0%,
border-top: 2px solid @colorGray14;
}
}
+
+// On small screens, remove the table properties from the
+// top section. T225127#5518870
+@media screen and ( max-width: @width-breakpoint-tablet ) {
+ .mw-rcfilters-ui-watchlistTopSectionWidget {
+ .mw-rcfilters-ui-table,
+ .mw-rcfilters-ui-row,
+ .mw-rcfilters-ui-cell {
+ display: block;
+ }
+
+ &-editWatchlistButton {
+ margin-top: 1em;
+ }
+ }
+}
* JavaScript for Special:RecentChanges
*/
( function () {
- var rc, $checkboxes, $select;
+ var rc, $checkboxes, $select, namespaceDropdown;
/**
* @class mw.special.recentchanges
*/
updateCheckboxes: function () {
// The option element for the 'all' namespace has an empty value
- var isAllNS = $select.val() === '';
+ var value = $select.val(),
+ isAllNS = value === 'all' || value === '';
// Iterates over checkboxes and propagate the selected option
$checkboxes.toggleClass( 'mw-input-hidden', isAllNS );
},
init: function () {
- $select = $( '#namespace' );
- $checkboxes = $( '#nsassociated, #nsinvert' ).closest( '.mw-input-with-label' );
+ $select = $( 'select#namespace' );
+ $checkboxes = $( '#nsassociated, #nsinvert, .contribs-ns-filters' )
+ .closest( '.mw-input-with-label' );
- // Bind to change event of the checkboxes.
- // The initial state is already set in HTML.
- $select.on( 'change', rc.updateCheckboxes );
+ if ( $select.length === 0 ) {
+ $select = $( '#namespace select' );
+ if ( $select.length > 0 ) {
+ namespaceDropdown = OO.ui.infuse( $( '#namespace' ).closest( '[data-ooui]' ) );
+ namespaceDropdown.on( 'change', rc.updateCheckboxes );
+ }
+ } else {
+ // Bind to change event of the checkboxes.
+ // The initial state is already set in HTML.
+ $select.on( 'change', rc.updateCheckboxes );
+ }
}
};
--- /dev/null
+/*!
+ * Styling for Special:Contributions
+ */
+@import 'mediawiki.ui/variables.less';
+
+// OOUIHTMLForm styles.
+@ooui-font-size-browser: 16; // Assumed browser default of `16px`.
+@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`.
+
+@ooui-spacing-small: 8 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `0.57142857em`≈`8px`.
+@ooui-spacing-medium: 12 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `0.8571429em`≈`12px`.
+@ooui-spacing-large: 16 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.1428571em`≈`16px`.
+
+.oo-ui-fieldsetLayout-group {
+ max-width: 50em;
+
+ .oo-ui-panelLayout-padded.oo-ui-panelLayout-framed {
+ margin: 0;
+ border: 0;
+ padding: 0;
+ }
+
+ // Hide extra `legend`s when grouping form in sections.
+ .oo-ui-fieldsetLayout.oo-ui-labelElement > .oo-ui-fieldsetLayout-header {
+ display: none;
+ }
+}
+
+.mw-autocomplete-user.oo-ui-fieldLayout {
+ margin-top: @ooui-spacing-small;
+}
+
+// Higher specificity needed to override OOUIHTMLForm styles.
+.mw-htmlform-field-HTMLMultiSelectField.mw-htmlform-flatlist.oo-ui-fieldLayout {
+ margin-top: @ooui-spacing-small;
+}
+
+.mw-htmlform-field-HTMLTagFilter ~ .mw-htmlform-field-HTMLCheckField.oo-ui-fieldLayout {
+ display: inline-block;
+ padding-right: @ooui-spacing-large;
+}
+
+// Clearfix for floated `.mw-htmlform-field-HTMLDateTimeField` below.
+#mw-htmlform-contribs-date:after {
+ content: '';
+ clear: both;
+ display: block;
+}
+
+.mw-htmlform-field-HTMLDateTimeField {
+ margin-right: @ooui-spacing-large;
+ margin-bottom: @ooui-spacing-small;
+
+ .oo-ui-fieldLayout.oo-ui-labelElement&:first-child {
+ margin-top: @ooui-spacing-medium;
+ }
+}
+
+@media all and ( min-width: @width-breakpoint-tablet ) {
+ .mw-htmlform-field-HTMLDateTimeField {
+ float: left;
+ // Same `width` as DateInputWidget.
+ width: 21em;
+ }
+}
font-weight: bold;
}
-.mw-contributions-form select {
- vertical-align: middle;
-}
-
/* Special:EditWatchlist */
.watchlistredir {
font-style: italic;
// Highlight matching parts of link suggestion
if ( config.query ) {
- this.setHighlightedQuery( config.data, config.query, config.compare );
+ this.setHighlightedQuery( config.data, config.query, config.compare, true );
}
this.$label.attr( 'title', config.data );
'<div class="errorbox">err</div>'
);
$this->assertEquals(
- Html::errorBox( 'err', 'heading' ),
- '<div class="errorbox"><h2>heading</h2>err</div>'
+ Html::errorBox( 'err', 'heading', 'errorbox-custom-class' ),
+ '<div class="errorbox errorbox-custom-class"><h2>heading</h2>err</div>'
);
$this->assertEquals(
- Html::errorBox( 'err', '0' ),
+ Html::errorBox( 'err', '0', '' ),
'<div class="errorbox"><h2>0</h2>err</div>'
);
}
[ 'test.load.circleB', '0', [ 'test.load.circleC' ] ],
[ 'test.load.circleC', '0', [ 'test.load.circleA' ] ]
] );
- this.sandbox.stub( mw, 'track', function ( topic, data ) {
+ this.sandbox.stub( mw, 'trackError', function ( topic, data ) {
capture.push( {
topic: topic,
error: data.exception && data.exception.message,
mw.loader.register( [
[ 'test.load.circleDirect', '0', [ 'test.load.circleDirect' ] ]
] );
- this.sandbox.stub( mw, 'track', function ( topic, data ) {
+ this.sandbox.stub( mw, 'trackError', function ( topic, data ) {
capture.push( {
topic: topic,
error: data.exception && data.exception.message,
// Regression test for T36853
QUnit.test( '.load() - Error: Missing dependency', function ( assert ) {
var capture = [];
- this.sandbox.stub( mw, 'track', function ( topic, data ) {
+ this.sandbox.stub( mw, 'trackError', function ( topic, data ) {
capture.push( {
topic: topic,
error: data.exception && data.exception.message,
this.useStubClock();
// Don't actually emit an error event
- this.sandbox.stub( mw, 'track' );
+ this.sandbox.stub( mw, 'trackError' );
mw.loader.register( [
[ 'test.module1', '0' ],
}, {}, {} );
this.tick();
- assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
- assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
- assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
+ assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'State of test.module1' );
+ assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'State of test.module2' );
+ assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'State of test.module3' );
- assert.strictEqual( mw.track.callCount, 1 );
+ assert.strictEqual( mw.trackError.callCount, 1 );
} );
QUnit.test( 'Out-of-order implementation', function ( assert ) {
mw.loader.implement( 'test.module4', function () {} );
this.tick();
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'State of test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'State of test.module6' );
mw.loader.implement( 'test.module6', function () {} );
this.tick();
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'State of test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'State of test.module6' );
mw.loader.implement( 'test.module5', function () {} );
this.tick();
- assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
- assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
- assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
+ assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'State of test.module4' );
+ assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'State of test.module5' );
+ assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'State of test.module6' );
} );
QUnit.test( 'Missing dependency', function ( assert ) {