Make Special:Contributions use OOUI
authorjdlrobson <jdlrobson@gmail.com>
Sat, 2 Apr 2016 08:03:42 +0000 (11:03 +0300)
committerJdlrobson <jrobson@wikimedia.org>
Mon, 23 Sep 2019 18:50:44 +0000 (18:50 +0000)
Changes:
* IP address/username is now a single label & input element combination
* Add page-specific styles in separate LESS file
* Remove no longer necessary CSS rule

Bug: T117736
Bug: T219238
Change-Id: I979078d8937898acae22bc28d5ed51da1d4ed627

RELEASE-NOTES-1.34
docs/hooks.txt
includes/specials/SpecialContributions.php
resources/Resources.php
resources/src/mediawiki.special.recentchanges.js
resources/src/mediawiki.special/contributions.less [new file with mode: 0644]
resources/src/mediawiki.special/special.less

index 508ba08..6b89ff6 100644 (file)
@@ -126,6 +126,9 @@ $wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false;
 * (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 ===
 
index 55ba06e..4261b0b 100644 (file)
@@ -3177,7 +3177,7 @@ $row: Revision information from the database
 '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().
index e8b85fa..3b3f2e9 100644 (file)
@@ -23,7 +23,6 @@
 
 use MediaWiki\Block\DatabaseBlock;
 use MediaWiki\MediaWikiServices;
-use MediaWiki\Widget\DateInputWidget;
 
 /**
  * Special:Contributions, show user contributions in a paged list
@@ -114,14 +113,23 @@ class SpecialContributions extends IncludableSpecialPage {
                }
 
                $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
@@ -462,48 +470,6 @@ class SpecialContributions extends IncludableSpecialPage {
         */
        protected function getForm() {
                $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',
@@ -511,15 +477,7 @@ class SpecialContributions extends IncludableSpecialPage {
                ] );
                $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 = [
@@ -527,7 +485,6 @@ class SpecialContributions extends IncludableSpecialPage {
                        'nsInvert',
                        'deletedOnly',
                        'target',
-                       'contribs',
                        'year',
                        'month',
                        'start',
@@ -543,207 +500,163 @@ class SpecialContributions extends IncludableSpecialPage {
                        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 )
-               );
-
-               $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,
-                       ] )
+                       [ $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;
+                       }
+               }
 
-               $submit = Xml::tags( 'div', [],
-                       Html::submitButton(
-                               $this->msg( 'sp-contributions-submit' )->text(),
-                               [ 'class' => 'mw-submit' ], [ 'mw-ui-progressive' ]
-                       )
-               );
+               $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',
+               ];
 
-               $form .= Xml::fieldset(
-                       $this->msg( 'sp-contributions-search' )->text(),
-                       $targetSelection .
-                       $namespaceSelection .
-                       $filterSelection .
-                       $extraOptions .
-                       $dateRangeSelection .
-                       $submit,
-                       [ 'class' => 'mw-contributions-table' ]
-               );
+               $htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
+               $htmlForm
+                       ->setMethod( 'get' )
+                       ->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 );
        }
 
        /**
index c8eae03..383c540 100644 (file)
@@ -1983,6 +1983,7 @@ return [
                        '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',
@@ -2146,9 +2147,11 @@ return [
        'mediawiki.special.contributions' => [
                'scripts' => 'resources/src/mediawiki.special.contributions.js',
                'dependencies' => [
+                       'oojs-ui',
                        'mediawiki.widgets.DateInputWidget',
                        'mediawiki.jqueryMsg',
-               ]
+               ],
+               'targets' => [ 'desktop', 'mobile' ],
        ],
        'mediawiki.special.edittags' => [
                'scripts' => 'resources/src/mediawiki.special.edittags.js',
@@ -2195,6 +2198,9 @@ return [
                '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' ],
        ],
index 310832d..c62acd9 100644 (file)
@@ -2,7 +2,7 @@
  * 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 );
+                       }
                }
        };
 
diff --git a/resources/src/mediawiki.special/contributions.less b/resources/src/mediawiki.special/contributions.less
new file mode 100644 (file)
index 0000000..cc0e538
--- /dev/null
@@ -0,0 +1,65 @@
+/*!
+ * 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;
+       }
+}
index 3f76cf0..40a2ec2 100644 (file)
        font-weight: bold;
 }
 
-.mw-contributions-form select {
-       vertical-align: middle;
-}
-
 /* Special:EditWatchlist */
 .watchlistredir {
        font-style: italic;