Merge "WLFilters: fix server-side tag filtering"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 31 Aug 2017 18:27:57 +0000 (18:27 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 31 Aug 2017 18:27:57 +0000 (18:27 +0000)
37 files changed:
includes/EditPage.php
includes/PageProps.php
includes/cache/LinkBatch.php
includes/deferred/CdnCacheUpdate.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/exception/DBConnectionError.php
includes/libs/rdbms/exception/DBError.php
includes/libs/rdbms/exception/DBExpectedError.php
includes/libs/rdbms/exception/DBQueryError.php
includes/libs/rdbms/exception/DBTransactionSizeError.php
includes/specials/SpecialWatchlist.php
includes/upload/UploadFromUrl.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/archives/patch-comment-table.sql
maintenance/postgres/archives/patch-comment-table.sql
maintenance/postgres/tables.sql
maintenance/sqlite/archives/patch-comment-table.sql
maintenance/tables.sql
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.UriProcessor.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less [new file with mode: 0644]
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js [new file with mode: 0644]
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js [new file with mode: 0644]
resources/src/mediawiki.toolbar/images/ksh/LICENSE
tests/phpunit/includes/CommentStoreTest.php

index 40913bb..4451867 100644 (file)
@@ -4086,7 +4086,7 @@ class EditPage {
         *   where bool indicates the checked status of the checkbox
         * @return array
         */
-       protected function getCheckboxesDefinition( $checked ) {
+       public function getCheckboxesDefinition( $checked ) {
                global $wgUser;
                $checkboxes = [];
 
index dac756e..ff8deee 100644 (file)
@@ -242,6 +242,8 @@ class PageProps {
        private function getGoodIDs( $titles ) {
                $result = [];
                if ( is_array( $titles ) ) {
+                       ( new LinkBatch( $titles ) )->execute();
+
                        foreach ( $titles as $title ) {
                                $pageID = $title->getArticleID();
                                if ( $pageID > 0 ) {
index 38cb6be..30d105b 100644 (file)
@@ -43,7 +43,7 @@ class LinkBatch {
        protected $caller;
 
        /**
-        * @param LinkTarget[] $arr Initial items to be added to the batch
+        * @param Traversable|LinkTarget[] $arr Initial items to be added to the batch
         */
        public function __construct( $arr = [] ) {
                foreach ( $arr as $item ) {
index 470086a..7fafc0e 100644 (file)
@@ -49,11 +49,12 @@ class CdnCacheUpdate implements DeferrableUpdate, MergeableUpdate {
        /**
         * Create an update object from an array of Title objects, or a TitleArray object
         *
-        * @param Traversable|array $titles
+        * @param Traversable|Title[] $titles
         * @param string[] $urlArr
         * @return CdnCacheUpdate
         */
        public static function newFromTitles( $titles, $urlArr = [] ) {
+               ( new LinkBatch( $titles ) )->execute();
                /** @var Title $title */
                foreach ( $titles as $title ) {
                        $urlArr = array_merge( $urlArr, $title->getCdnUrls() );
index bdac06c..fcfd937 100644 (file)
@@ -521,6 +521,10 @@ __INDEXATTR__;
        public function selectSQLText(
                $table, $vars, $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
        ) {
+               if ( is_string( $options ) ) {
+                       $options = [ $options ];
+               }
+
                // Change the FOR UPDATE option as necessary based on the join conditions. Then pass
                // to the parent function to get the actual SQL text.
                // In Postgres when using FOR UPDATE, only the main table and tables that are inner joined
@@ -532,12 +536,28 @@ __INDEXATTR__;
                        $forUpdateKey = array_search( 'FOR UPDATE', $options, true );
                        if ( $forUpdateKey !== false && $join_conds ) {
                                unset( $options[$forUpdateKey] );
+                               $options['FOR UPDATE'] = [];
+
+                               // All tables not in $join_conds are good
+                               foreach ( $table as $alias => $name ) {
+                                       if ( is_numeric( $alias ) ) {
+                                               $alias = $name;
+                                       }
+                                       if ( !isset( $join_conds[$alias] ) ) {
+                                               $options['FOR UPDATE'][] = $alias;
+                                       }
+                               }
 
                                foreach ( $join_conds as $table_cond => $join_cond ) {
                                        if ( 0 === preg_match( '/^(?:LEFT|RIGHT|FULL)(?: OUTER)? JOIN$/i', $join_cond[0] ) ) {
                                                $options['FOR UPDATE'][] = $table_cond;
                                        }
                                }
+
+                               // Quote alias names so $this->tableName() won't mangle them
+                               $options['FOR UPDATE'] = array_map( function ( $name ) use ( $table ) {
+                                       return isset( $table[$name] ) ? $this->addIdentifierQuotes( $name ) : $name;
+                               }, $options['FOR UPDATE'] );
                        }
 
                        if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) {
index 0091524..91d98dc 100644 (file)
@@ -28,7 +28,7 @@ class DBConnectionError extends DBExpectedError {
         * @param IDatabase $db Object throwing the error
         * @param string $error Error text
         */
-       function __construct( IDatabase $db = null, $error = 'unknown error' ) {
+       public function __construct( IDatabase $db = null, $error = 'unknown error' ) {
                $msg = 'Cannot access the database';
                if ( trim( $error ) != '' ) {
                        $msg .= ": $error";
index d65e2d3..2f7499b 100644 (file)
@@ -36,7 +36,7 @@ class DBError extends Exception {
         * @param IDatabase $db Object which threw the error
         * @param string $error A simple error message to be used for debugging
         */
-       function __construct( IDatabase $db = null, $error ) {
+       public function __construct( IDatabase $db = null, $error ) {
                $this->db = $db;
                parent::__construct( $error );
        }
index 4f65efa..31d8c27 100644 (file)
@@ -36,7 +36,7 @@ class DBExpectedError extends DBError implements MessageSpecifier, ILocalizedExc
        /** @var string[] Message parameters */
        protected $params;
 
-       function __construct( IDatabase $db = null, $error, array $params = [] ) {
+       public function __construct( IDatabase $db = null, $error, array $params = [] ) {
                parent::__construct( $db, $error );
                $this->params = $params;
        }
index 6a4076f..a8ea3ad 100644 (file)
@@ -41,7 +41,7 @@ class DBQueryError extends DBExpectedError {
         * @param string $sql
         * @param string $fname
         */
-       function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
+       public function __construct( IDatabase $db, $error, $errno, $sql, $fname ) {
                if ( $db instanceof Database && $db->wasConnectionError( $errno ) ) {
                        $message = "A connection error occured. \n" .
                                "Query: $sql\n" .
index e45b9f3..d2622e1 100644 (file)
@@ -25,7 +25,7 @@ namespace Wikimedia\Rdbms;
  * @ingroup Database
  */
 class DBTransactionSizeError extends DBTransactionError {
-       function getKey() {
+       public function getKey() {
                return 'transaction-duration-limit-exceeded';
        }
 }
index 07889b7..ba3cb87 100644 (file)
@@ -104,6 +104,10 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                                'wgStructuredChangeFiltersSavedQueriesPreferenceName',
                                'rcfilters-wl-saved-queries'
                        );
+                       $output->addJsConfigVars(
+                               'wgStructuredChangeFiltersEditWatchlistUrl',
+                               SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL()
+                       );
                }
        }
 
@@ -829,21 +833,29 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                $showUpdatedMarker = $this->getConfig()->get( 'ShowUpdatedMarker' );
 
                // Show watchlist header
-               $form .= "<p>";
+               $watchlistHeader = '';
                if ( $numItems == 0 ) {
-                       $form .= $this->msg( 'nowatchlist' )->parse() . "\n";
+                       $watchlistHeader = $this->msg( 'nowatchlist' )->parse();
                } else {
-                       $form .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
+                       $watchlistHeader .= $this->msg( 'watchlist-details' )->numParams( $numItems )->parse() . "\n";
                        if ( $this->getConfig()->get( 'EnotifWatchlist' )
                                && $user->getOption( 'enotifwatchlistpages' )
                        ) {
-                               $form .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
+                               $watchlistHeader .= $this->msg( 'wlheader-enotif' )->parse() . "\n";
                        }
                        if ( $showUpdatedMarker ) {
-                               $form .= $this->msg( 'wlheader-showupdated' )->parse() . "\n";
+                               $watchlistHeader .= $this->msg(
+                                       $this->isStructuredFilterUiEnabled() ?
+                                               'rcfilters-watchlist-showupdated' :
+                                               'wlheader-showupdated'
+                               )->parse() . "\n";
                        }
                }
-               $form .= "</p>";
+               $form .= Html::rawElement(
+                       'div',
+                       [ 'class' => 'watchlistDetails' ],
+                       $watchlistHeader
+               );
 
                if ( $numItems > 0 && $showUpdatedMarker ) {
                        $form .= Xml::openElement( 'form', [ 'method' => 'post',
index 7d697a1..f5367bb 100644 (file)
@@ -287,7 +287,7 @@ class UploadFromUrl extends UploadBase {
 
                wfDebugLog( 'fileupload', $status );
                if ( $status->isOK() ) {
-                       wfDebugLog( 'fileupload', 'Download by URL completed successfuly.' );
+                       wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
                } else {
                        wfDebugLog(
                                'fileupload',
index a44b3cf..b280488 100644 (file)
        "rcfilters-empty-filter": "No active filters. All contributions are shown.",
        "rcfilters-filterlist-title": "Filters",
        "rcfilters-filterlist-whatsthis": "How do these work?",
-       "rcfilters-filterlist-feedbacklink": "Provide feedback on the new (beta) filters",
+       "rcfilters-filterlist-feedbacklink": "Tell us what you think about these (new) filtering tools",
        "rcfilters-highlightbutton-title": "Highlight results",
        "rcfilters-highlightmenu-title": "Select a color",
        "rcfilters-highlightmenu-help": "Select a color to highlight this property",
        "rcfilters-liveupdates-button-title-on": "Turn off live updates",
        "rcfilters-liveupdates-button-title-off": "Display new changes as they happen",
        "rcfilters-watchlist-markSeen-button": "Mark all changes as seen",
+       "rcfilters-watchlist-editWatchlist-button": "Edit your list of watched pages",
+       "rcfilters-watchlist-showupdated": "Changes to pages you haven't visited since the changes occurred are in <strong>bold</strong>, with solid markers.",
        "rcnotefrom": "Below {{PLURAL:$5|is the change|are the changes}} since <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
        "rclistfromreset": "Reset date selection",
        "rclistfrom": "Show new changes starting from $2, $3",
index fb05de8..1105d15 100644 (file)
        "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.",
        "rcfilters-watchlist-markSeen-button": "Label for the button to mark all changes as seen on [[Special:Watchlist]] when using the structured filters interface.",
+       "rcfilters-watchlist-editWatchlist-button": "Label for the button to edit the watched pages on [[Special:Watchlist]] when using the structured filters interface.",
+       "rcfilters-watchlist-showupdated": "Message at the top of [[Special:Watchlist]] when the Structured filters are enabled that describes what unseen changes look like.",
        "rcnotefrom": "This message is displayed at [[Special:RecentChanges]] when viewing recentchanges from some specific time.\n\nThe corresponding message is {{msg-mw|Rclistfrom}}.\n\nParameters:\n* $1 - the maximum number of changes that are displayed\n* $2 - (Optional) a date and time\n* $3 - a date\n* $4 - a time\n* $5 - Number of changes are displayed, for use with PLURAL",
        "rclistfromreset": "Used on [[Special:RecentChanges]] to reset a selection of a certain date range.",
        "rclistfrom": "Used on [[Special:RecentChanges]]. Parameters:\n* $1 - (Currently not use) date and time. The date and the time adds to the rclistfrom description.\n* $2 - time. The time adds to the rclistfrom link description (with split of date and time).\n* $3 - date. The date adds to the rclistfrom link description (with split of date and time).\n\nThe corresponding message is {{msg-mw|Rcnotefrom}}.",
index 60c58d6..c8bf958 100644 (file)
@@ -9,7 +9,7 @@ CREATE TABLE /*_*/comment (
   comment_text BLOB NOT NULL,
   comment_data BLOB
 ) /*$wgDBTableOptions*/;
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
 
 CREATE TABLE /*_*/revision_comment_temp (
   revcomment_rev int unsigned NOT NULL,
index a84986f..243a3b3 100644 (file)
@@ -21,7 +21,7 @@ CREATE UNIQUE INDEX revcomment_rev ON revision_comment_temp (revcomment_rev);
 
 CREATE TABLE image_comment_temp (
        imgcomment_name       TEXT NOT NULL,
-       imgcomment_comment_id INTEGER NOT NULL,
-       PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+       imgcomment_description_id INTEGER NOT NULL,
+       PRIMARY KEY (imgcomment_name, imgcomment_description_id)
 );
-CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_rev);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
index 3516a3b..eea9e68 100644 (file)
@@ -358,10 +358,10 @@ CREATE INDEX img_sha1          ON image (img_sha1);
 
 CREATE TABLE image_comment_temp (
        imgcomment_name       TEXT NOT NULL,
-       imgcomment_comment_id INTEGER NOT NULL,
-       PRIMARY KEY (imgcomment_name, imgcomment_comment_id)
+       imgcomment_description_id INTEGER NOT NULL,
+       PRIMARY KEY (imgcomment_name, imgcomment_description_id)
 );
-CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_rev);
+CREATE UNIQUE INDEX imgcomment_name ON image_comment_temp (imgcomment_name);
 
 CREATE TABLE oldimage (
   oi_name          TEXT         NOT NULL,
index 2017eca..f743b55 100644 (file)
@@ -10,7 +10,7 @@ CREATE TABLE /*_*/comment (
   comment_text BLOB NOT NULL,
   comment_data BLOB
 ) /*$wgDBTableOptions*/;
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
 
 CREATE TABLE /*_*/revision_comment_temp (
   revcomment_rev int unsigned NOT NULL,
index 3836665..d6ef40c 100644 (file)
@@ -521,7 +521,7 @@ CREATE TABLE /*_*/comment (
   comment_data BLOB
 ) /*$wgDBTableOptions*/;
 -- Index used for deduplication.
-CREATE INDEX /*i*/comment_hash ON comment (comment_hash);
+CREATE INDEX /*i*/comment_hash ON /*_*/comment (comment_hash);
 
 
 --
index 1ebe210..7a95da8 100644 (file)
@@ -1803,6 +1803,8 @@ return [
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js',
                        'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MarkSeenButtonWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js',
+                       'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js',
                        'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
                        'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
                ],
@@ -1830,6 +1832,8 @@ return [
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListItemWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
                        'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less',
+                       'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less',
                ],
                'skinStyles' => [
                        'monobook' => [
@@ -1896,6 +1900,7 @@ return [
                        'rcfilters-liveupdates-button-title-on',
                        'rcfilters-liveupdates-button-title-off',
                        'rcfilters-watchlist-markSeen-button',
+                       'rcfilters-watchlist-editWatchlist-button',
                        'rcfilters-other-review-tools',
                        'blanknamespace',
                        'namespaces',
index a8ee06b..62ba002 100644 (file)
                return result;
        };
 
+       /**
+        * Get an array of currently applied highlight colors
+        *
+        * @return {string[]} Currently applied highlight colors
+        */
+       mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentlyUsedHighlightColors = function () {
+               var result = [];
+
+               this.getHighlightedItems().forEach( function ( filterItem ) {
+                       var color = filterItem.getHighlightColor();
+
+                       if ( result.indexOf( color ) === -1 ) {
+                               result.push( color );
+                       }
+               } );
+
+               return result;
+       };
+
        /**
         * Sanitize value group of a string_option groups type
         * Remove duplicates and make sure to only use valid
index a1ef981..1894b61 100644 (file)
                this.filtersModel.toggleInvertedNamespaces( !!Number( parameters.invert ) );
 
                // Update highlight state
-               this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
                this.filtersModel.getItems().forEach( function ( filterItem ) {
                        var color = parameters[ filterItem.getName() + '_color' ];
                        if ( color ) {
                                filterItem.clearHighlightColor();
                        }
                } );
+               this.filtersModel.toggleHighlight( !!Number( parameters.highlight ) );
 
                // Check all filter interactions
                this.filtersModel.reassessFilterInteractions();
index a6bce14..0819351 100644 (file)
@@ -8,10 +8,10 @@
                 * @private
                 */
                init: function () {
-                       var toplinksTitle,
-                               topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
-                               topLinksCookie = mw.cookie.get( topLinksCookieName ),
-                               topLinksCookieValue = topLinksCookie || 'collapsed',
+                       var $topLinks,
+                               rcTopSection,
+                               $watchlistDetails,
+                               wlTopSection,
                                savedQueriesPreferenceName = mw.config.get( 'wgStructuredChangeFiltersSavedQueriesPreferenceName' ),
                                filtersModel = new mw.rcfilters.dm.FiltersViewModel(),
                                changesListModel = new mw.rcfilters.dm.ChangesListViewModel(),
@@ -26,7 +26,9 @@
                                        .addClass( 'mw-rcfilters-ui-overlay' ),
                                filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
                                        controller, filtersModel, savedQueriesModel, changesListModel, { $overlay: $overlay } ),
-                               markSeenButton,
+                               savedLinksListWidget = new mw.rcfilters.ui.SavedLinksListWidget(
+                                       controller, savedQueriesModel, { $overlay: $overlay }
+                               ),
                                currentPage = mw.config.get( 'wgCanonicalNamespace' ) +
                                        ':' +
                                        mw.config.get( 'wgCanonicalSpecialPageName' );
                        controller.replaceUrl();
 
                        if ( currentPage === 'Special:Recentchanges' ) {
-                               toplinksTitle = new OO.ui.ButtonWidget( {
-                                       framed: false,
-                                       indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
-                                       flags: [ 'progressive' ],
-                                       label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
-                               } );
-                               $( '.mw-recentchanges-toplinks-title' ).replaceWith( toplinksTitle.$element );
-                               // Move the top links to a designated area so it's near the
-                               // 'saved filters' button and make it collapsible
-                               $( '.mw-recentchanges-toplinks' )
-                                       .addClass( 'mw-rcfilters-ui-ready' )
-                                       .makeCollapsible( {
-                                               collapsed: topLinksCookieValue === 'collapsed',
-                                               $customTogglers: toplinksTitle.$element
-                                       } )
-                                       .on( 'beforeExpand.mw-collapsible', function () {
-                                               mw.cookie.set( topLinksCookieName, 'expanded' );
-                                               toplinksTitle.setIndicator( 'up' );
-                                       } )
-                                       .on( 'beforeCollapse.mw-collapsible', function () {
-                                               mw.cookie.set( topLinksCookieName, 'collapsed' );
-                                               toplinksTitle.setIndicator( 'down' );
-                                       } )
-                                       .appendTo( '.mw-rcfilters-ui-filterWrapperWidget-top-placeholder' );
+                               $topLinks = $( '.mw-recentchanges-toplinks' ).detach();
+
+                               rcTopSection = new mw.rcfilters.ui.RcTopSectionWidget(
+                                       savedLinksListWidget, $topLinks
+                               );
+                               filtersWidget.setTopSection( rcTopSection.$element );
                        } // end Special:RC
 
                        if ( currentPage === 'Special:Watchlist' ) {
-                               markSeenButton = new mw.rcfilters.ui.MarkSeenButtonWidget( controller, changesListModel );
-                               $( 'form#mw-watchlist-resetbutton' ).detach();
-                               filtersWidget.prependToTopRow( markSeenButton );
+                               $( '#contentSub, form#mw-watchlist-resetbutton' ).detach();
+                               $watchlistDetails = $( '.watchlistDetails' ).detach().contents();
+
+                               wlTopSection = new mw.rcfilters.ui.WatchlistTopSectionWidget(
+                                       controller, changesListModel, savedLinksListWidget, $watchlistDetails
+                               );
+                               filtersWidget.setTopSection( wlTopSection.$element );
                        } // end Special:WL
                }
        };
index c667bac..e031552 100644 (file)
@@ -7,29 +7,6 @@
                border: 0;
        }
 
-       .mw-recentchanges-toplinks {
-               padding: 0 0.5em;
-
-               .oo-ui-widget-enabled.oo-ui-buttonElement.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
-                       padding: 0 2.5em 0 0.5em;
-               }
-
-               &-title,
-               .mw-collapsible-text {
-                       // Same as the legend
-                       font-size: 0.85em;
-               }
-
-               &:not( .mw-collapsed ) {
-                       // Same as the legend
-                       border: 1px solid #ddd;
-               }
-
-               &:not( .mw-rcfilters-ui-ready ) {
-                       display: none;
-               }
-       }
-
        .rcfilters-head {
                min-height: 200px;
 
                        .animation-delay( 0s );
                }
        }
+
+       #contentSub,
+       p.watchlistDetails,
+       form#mw-watchlist-resetbutton {
+               display: none;
+       }
 }
 
 .mw-rcfilters-staticfilters-selected {
index 27acd75..b4e3b0e 100644 (file)
        }
 }
 
+// A mixin just for changesListWrapperWidget page, to output the scope of the widget
+// so it is before the rest of the rule; we need the li& to be in
+// between the wrapper scope and the color-cX class, which doesn't
+// work if the rules are inside the above widget LESS scope
+.highlight-changesListWrapperWidget( @bgcolor ) {
+       .mw-rcfilters-ui-changesListWrapperWidget li&,
+               .mw-rcfilters-ui-changesListWrapperWidget & tr:first-child,
+               .mw-rcfilters-ui-changesListWrapperWidget tr&.mw-rcfilters-ui-changesListWrapperWidget-enhanced-toplevel:not(.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey) td:not( :nth-child( -n+2 ) ),
+               .mw-rcfilters-ui-changesListWrapperWidget tr&.mw-rcfilters-ui-changesListWrapperWidget-enhanced-nested:not(.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey) td:not( :nth-child( -n+3 ) ) {
+               background-color: @bgcolor;
+       }
+}
+
 // This mixin produces color mixes for two, three and four colors
 .highlight-color-mix( @color1, @color2, @color3: false, @color4: false ) {
        @highlight-color-class-var: ~'.mw-rcfilters-highlight-color-@{color1}.mw-rcfilters-highlight-color-@{color2}';
 
        // Two colors
        @{highlight-color-class-var} when ( @color3 = false ) and ( @color4 = false ) and not ( @color1 = false ), ( @color2 = false ) {
-               background-color: tint( average( @@c1var, @@c2var ), 50% );
+               .highlight-changesListWrapperWidget( tint( average( @@c1var, @@c2var ), 50% ) );
        }
        // Three colors
        @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3} when ( @color4 = false ) and not ( @color3 = false ) {
                @c3var: ~'highlight-@{color3}';
-               background-color: tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% );
+               .highlight-changesListWrapperWidget( tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% ) );
        }
 
        // Four colors
        @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3}.mw-rcfilters-highlight-color-@{color4} when not ( @color4 = false ) {
                @c3var: ~'highlight-@{color3}';
                @c4var: ~'highlight-@{color4}';
-               background-color: tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% );
+               .highlight-changesListWrapperWidget( tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% ) );
        }
 }
index 3fe9b4c..5ef60e0 100644 (file)
                }
        }
 
+       // Rule needs to be specific
+       // We want the expand button to appear outside the color
+       // to match the way the general highlight background appears
+       &-enhanced-grey td:not( :nth-child( -n+2 ) ) {
+               background-color: #dee0e3;
+       }
+
        h4:first-of-type {
                margin-top: 0;
                padding-top: 0;
@@ -77,8 +84,9 @@
                // and then plus the general margin
                width: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 )';
                // And we want to shift the entire block to the left of the li
-               position: absolute;
-               left: 0;
+               position: relative;
+               // Negative left margin of width + padding
+               margin-left: ~'calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * -5 - @{result-circle-general-margin} )';
 
                .mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
                        display: inline-block;
                        .result-circle( c5 );
                }
        }
+}
 
-       // One color
-       .mw-rcfilters-highlight-color-c1 {
-               background-color: tint( @highlight-c1, 70% );
-       }
+// One color
+.mw-rcfilters-highlight-color-c1 {
+       .highlight-changesListWrapperWidget( tint( @highlight-c1, 70% ); );
+}
 
-       .mw-rcfilters-highlight-color-c2 {
-               background-color: tint( @highlight-c2, 70% );
-       }
+.mw-rcfilters-highlight-color-c2 {
+       .highlight-changesListWrapperWidget( tint( @highlight-c2, 70% ); );
+}
 
-       .mw-rcfilters-highlight-color-c3 {
-               background-color: tint( @highlight-c3, 70% );
-       }
+.mw-rcfilters-highlight-color-c3 {
+       .highlight-changesListWrapperWidget( tint( @highlight-c3, 70% ); );
+}
 
-       .mw-rcfilters-highlight-color-c4 {
-               background-color: tint( @highlight-c4, 70% );
-       }
+.mw-rcfilters-highlight-color-c4 {
+       .highlight-changesListWrapperWidget( tint( @highlight-c4, 70% ); );
+}
 
-       .mw-rcfilters-highlight-color-c5 {
-               background-color: tint( @highlight-c5, 70% );
-       }
+.mw-rcfilters-highlight-color-c5 {
+       .highlight-changesListWrapperWidget( tint( @highlight-c5, 70% ); );
+}
 
-       // Two colors
-       .highlight-color-mix( c1, c2 );
-       // Overriding .highlight-color-mix( c1, c3 ); to produce
-       // a custom color rather than the computed tint
-       // see https://phabricator.wikimedia.org/T161267
-       .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c3 {
-               background-color: #ccdecc;
-       }
-       .highlight-color-mix( c1, c4 );
-       .highlight-color-mix( c1, c5 );
-       .highlight-color-mix( c2, c3 );
-       .highlight-color-mix( c2, c4 );
-       .highlight-color-mix( c2, c5 );
-       .highlight-color-mix( c3, c4 );
-       .highlight-color-mix( c3, c5 );
-       .highlight-color-mix( c4, c5 );
-
-       // Three colors
-       .highlight-color-mix( c1, c2, c3 );
-       .highlight-color-mix( c1, c2, c5 );
-       .highlight-color-mix( c1, c2, c4 );
-       .highlight-color-mix( c1, c3, c4 );
-       .highlight-color-mix( c1, c3, c5 );
-       .highlight-color-mix( c1, c4, c5 );
-       .highlight-color-mix( c2, c3, c4 );
-       .highlight-color-mix( c2, c3, c5 );
-       .highlight-color-mix( c2, c4, c5 );
-       .highlight-color-mix( c3, c4, c5 );
-
-       // Four colors
-       .highlight-color-mix( c1, c2, c3, c4 );
-       .highlight-color-mix( c1, c2, c3, c5 );
-       .highlight-color-mix( c1, c2, c4, c5 );
-       .highlight-color-mix( c1, c3, c4, c5 );
-       .highlight-color-mix( c2, c3, c4, c5 );
-
-       // Five colors:
-       .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
-               background-color: tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% );
-       }
+// Two colors
+.highlight-color-mix( c1, c2 );
+// Overriding .highlight-color-mix( c1, c3 ); to produce
+// a custom color rather than the computed tint
+// see https://phabricator.wikimedia.org/T161267
+.mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c3 {
+       .highlight-changesListWrapperWidget( #ccdecc );
+}
+.highlight-color-mix( c1, c4 );
+.highlight-color-mix( c1, c5 );
+.highlight-color-mix( c2, c3 );
+.highlight-color-mix( c2, c4 );
+.highlight-color-mix( c2, c5 );
+.highlight-color-mix( c3, c4 );
+.highlight-color-mix( c3, c5 );
+.highlight-color-mix( c4, c5 );
+
+// Three colors
+.highlight-color-mix( c1, c2, c3 );
+.highlight-color-mix( c1, c2, c5 );
+.highlight-color-mix( c1, c2, c4 );
+.highlight-color-mix( c1, c3, c4 );
+.highlight-color-mix( c1, c3, c5 );
+.highlight-color-mix( c1, c4, c5 );
+.highlight-color-mix( c2, c3, c4 );
+.highlight-color-mix( c2, c3, c5 );
+.highlight-color-mix( c2, c4, c5 );
+.highlight-color-mix( c3, c4, c5 );
+
+// Four colors
+.highlight-color-mix( c1, c2, c3, c4 );
+.highlight-color-mix( c1, c2, c3, c5 );
+.highlight-color-mix( c1, c2, c4, c5 );
+.highlight-color-mix( c1, c3, c4, c5 );
+.highlight-color-mix( c2, c3, c4, c5 );
+
+// Five colors:
+.mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
+       .highlight-changesListWrapperWidget( tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% ) );
 }
index a89b69c..bbd2c74 100644 (file)
@@ -7,17 +7,6 @@
                margin-top: 1em;
        }
 
-       &-top {
-               &-placeholder {
-                       width: 100%;
-               }
-
-               &-savedLinks {
-                       padding-left: 1em;
-                       vertical-align: bottom;
-               }
-       }
-
        &-bottom {
                margin-top: 1em;
 
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.RcTopSectionWidget.less
new file mode 100644 (file)
index 0000000..9d1cc23
--- /dev/null
@@ -0,0 +1,44 @@
+.mw-rcfilters-ui-rcTopSectionWidget {
+       &-topLinks {
+               &-table {
+                       width: 100%;
+               }
+
+               &-top {
+                       display: block;
+                       width: 100%;
+
+                       .mw-recentchanges-toplinks {
+                               margin-bottom: 0.5em;
+                       }
+               }
+       }
+
+       &-savedLinks {
+               vertical-align: bottom;
+               padding-left: 1em;
+       }
+
+       .mw-recentchanges-toplinks {
+               padding: 0 0.5em;
+
+               .oo-ui-widget-enabled.oo-ui-buttonElement.oo-ui-buttonElement-frameless .oo-ui-buttonElement-button {
+                       padding: 0 2.5em 0 0.5em;
+               }
+
+               &-title,
+               .mw-collapsible-text {
+                       // Same as the legend
+                       font-size: 0.85em;
+               }
+
+               &:not( .mw-collapsed ) {
+                       // Same as the legend
+                       border: 1px solid #ddd;
+               }
+
+               &:not( .mw-rcfilters-ui-ready ) {
+                       display: none;
+               }
+       }
+}
diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.WatchlistTopSectionWidget.less
new file mode 100644 (file)
index 0000000..5e1e118
--- /dev/null
@@ -0,0 +1,27 @@
+.mw-rcfilters-ui-watchlistTopSectionWidget {
+       &-watchlistDetails {
+               width: 100%;
+       }
+
+       &-editWatchlistButton {
+               vertical-align: bottom;
+
+               // actual button
+               .oo-ui-buttonWidget {
+                       margin-left: 3em;
+               }
+       }
+
+       &-savedLinks {
+               float: right;
+       }
+
+       .mw-rcfilters-ui-table {
+               margin-top: 1em;
+       }
+
+       &-separator {
+               margin-top: 1em;
+               border-top: 2px solid #eaecf0; // Base80 AAA
+       }
+}
index 0bee2f1..f7081af 100644 (file)
@@ -2,6 +2,10 @@
        &-table {
                display: table;
                width: 100%;
+
+               &-placeholder {
+                       width: 100%;
+               }
        }
 
        &-row {
index ec12783..f331f75 100644 (file)
                this.filtersViewModel = filtersViewModel;
                this.changesListViewModel = changesListViewModel;
                this.controller = controller;
+               this.highlightClasses = null;
+               this.filtersModelInitialized = false;
 
                // Events
                this.filtersViewModel.connect( this, {
                        itemUpdate: 'onItemUpdate',
-                       highlightChange: 'onHighlightChange'
+                       highlightChange: 'onHighlightChange',
+                       initialize: 'onFiltersModelInitialize'
                } );
                this.changesListViewModel.connect( this, {
                        invalidate: 'onModelInvalidate',
@@ -45,9 +48,6 @@
                        // We handle our own display/hide of the empty results message
                        .removeClass( 'mw-changeslist-empty' );
 
-               // Set up highlight containers
-               this.setupHighlightContainers( this.$element );
-
                this.setupNewChangesButtonContainer( this.$element );
        };
 
 
        OO.inheritClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.Widget );
 
+       /**
+        * Respond to filters model initialize event
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onFiltersModelInitialize = function () {
+               this.filtersModelInitialized = true;
+               // Set up highlight containers. We need to wait for the filters model
+               // to be initialized, so we can make sure we have all the css class definitions
+               // we get from the server with our filters
+               this.setupHighlightContainers( this.$element );
+       };
+
+       /**
+        * Get all available highlight classes
+        *
+        * @return {string[]} An array of available highlight class names
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
+               if ( !this.highlightClasses || !this.highlightClasses.length ) {
+                       this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
+                               .map( function ( filterItem ) {
+                                       return filterItem.getCssClass();
+                               } );
+               }
+
+               return this.highlightClasses;
+       };
+
        /**
         * Respond to the highlight feature being toggled on and off
         *
@@ -72,7 +99,7 @@
         * Respond to a filter item model update
         */
        mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
-               if ( this.filtersViewModel.isHighlightEnabled() ) {
+               if ( this.filtersModelInitialized && this.filtersViewModel.isHighlightEnabled() ) {
                        this.clearHighlight();
                        this.applyHighlight();
                }
         * @param {jQuery|string} $content The content of the updated changes list
         */
        mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
-               var highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
+               var $enhancedTopPageCell, $enhancedNestedPagesCell,
+                       widget = this,
+                       highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
                        $highlights = $( '<div>' )
                                .addClass( highlightClass )
                                .append(
                } );
 
                if ( this.inEnhancedMode() ) {
-                       // Enhanced RC
-                       $content.find( 'td.mw-enhanced-rc' )
-                               .parent()
+                       $enhancedTopPageCell = $content.find( 'table.mw-enhanced-rc.mw-collapsible' );
+                       $enhancedNestedPagesCell = $content.find( 'td.mw-enhanced-rc-nested' );
+
+                       // Enhanced RC highlight containers
+                       $content.find( 'table.mw-enhanced-rc tr:first-child' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-toplevel' )
                                .prepend(
                                        $( '<td>' )
                                                .append( $highlights.clone() )
                                );
+
+                       // We are adding and changing cells in a table that, despite having nested rows,
+                       // is actually all one big table. To do that right, we want to remove the 'placeholder'
+                       // cell from the top row, because we're actually adding that placeholder in the children
+                       // with the highlights.
+                       $content.find( 'table.mw-enhanced-rc tr:first-child td.mw-changeslist-line-prefix' )
+                               .detach();
+                       $content.find( 'table.mw-enhanced-rc tr:first-child td.mw-enhanced-rc' )
+                               .prop( 'colspan', '2' );
+
+                       $enhancedNestedPagesCell
+                               .before(
+                                       $( '<td>' )
+                                               .append( $highlights.clone().addClass( 'mw-enhanced-rc-nested' ) )
+                               );
+
+                       // We need to target the nested rows differently than the top rows so that the
+                       // LESS rules applies correctly. In top rows, the rule should highlight all but
+                       // the first 2 cells td:not( :nth-child( -n+2 ) and the nested rows, the rule
+                       // should highlight all but the first 3 cells td:not( :nth-child( -n+3 )
+                       $enhancedNestedPagesCell
+                               .closest( 'tr' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-nested' );
+
+                       // Go over pages that have sub results
+                       // HACK: We really only can collect those by targetting the collapsible class
+                       $enhancedTopPageCell.each( function () {
+                               var collectedClasses,
+                                       $table = $( this );
+
+                               // Go over <tr>s and pick up all recognized classes
+                               collectedClasses = widget.getHighlightClasses().filter( function ( className ) {
+                                       return $table.find( 'tr' ).hasClass( className );
+                               } );
+
+                               $table.find( 'tr:first-child' )
+                                       .addClass( collectedClasses.join( ' ' ) );
+                       } );
+
+                       $content.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhancedView' );
                } else {
                        // Regular RC
                        $content.find( 'ul.special li' )
                }
        };
 
+       /**
+        * In enhanced mode, we need to check whether the grouped results all have the
+        * same active highlights in order to see whether the "parent" of the group should
+        * be grey or highlighted normally.
+        *
+        * This is called every time highlights are applied.
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
+               var activeHighlightClasses,
+                       $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
+
+               activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
+                       return 'mw-rcfilters-highlight-color-' + color;
+               } );
+
+               // Go over top pages and their children, and figure out if all sub-pages have the
+               // same highlights between themselves. If they do, the parent should be highlighted
+               // with all colors. If classes are different, the parent should receive a grey
+               // background
+               $enhancedTopPageCell.each( function () {
+                       var firstChildClasses, $rowsWithDifferentHighlights,
+                               $table = $( this );
+
+                       // Collect the relevant classes from the first nested child
+                       firstChildClasses = activeHighlightClasses.filter( function ( className ) {
+                               return $table.find( 'tr:nth-child(2)' ).hasClass( className );
+                       } );
+                       // Filter the non-head rows and see if they all have the same classes
+                       // to the first row
+                       $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
+                               var classesInThisRow,
+                                       $this = $( this );
+
+                               classesInThisRow = activeHighlightClasses.filter( function ( className ) {
+                                       return $this.hasClass( className );
+                               } );
+
+                               return !OO.compare( firstChildClasses, classesInThisRow );
+                       } );
+
+                       // If classes are different, tag the row for using grey color
+                       $table.find( 'tr:first-child' )
+                               .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
+               } );
+       };
+
        /**
         * @return {boolean} Whether the changes are grouped by page
         */
                        }
                } );
 
+               if ( this.inEnhancedMode() ) {
+                       this.updateEnhancedParentHighlight();
+               }
+
                // Turn on highlights
                this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
        };
                this.$element.find( '[data-highlightedFilters]' )
                        .removeAttr( 'title' )
                        .removeAttr( 'data-highlightedFilters' );
+
+               // Remove grey from enhanced rows
+               this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
+                       .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
+
                // Turn off highlights
                this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
        };
index b57f5d7..db7acaa 100644 (file)
@@ -17,7 +17,7 @@
        mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
                controller, model, savedQueriesModel, changesListModel, config
        ) {
-               var $top, $bottom;
+               var $bottom;
                config = config || {};
 
                // Parent
                );
 
                // Initialize
-               this.$topRow = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-row' )
-                       .append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top-placeholder' )
-                       );
-               $top = $( '<div>' )
-                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' )
-                       .addClass( 'mw-rcfilters-ui-table' )
-                       .append( this.$topRow );
+               this.$top = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top' );
 
                $bottom = $( '<div>' )
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
                                this.dateWidget.$element
                        );
 
-               if ( !mw.user.isAnon() ) {
-                       this.savedLinksListWidget = new mw.rcfilters.ui.SavedLinksListWidget(
-                               this.controller,
-                               this.queriesModel,
-                               { $overlay: this.$overlay }
-                       );
-
-                       this.$topRow.append(
-                               $( '<div>' )
-                                       .addClass( 'mw-rcfilters-ui-cell' )
-                                       .addClass( 'mw-rcfilters-ui-filterWrapperWidget-top-savedLinks' )
-                                       .append( this.savedLinksListWidget.$element )
-                       );
-               }
-
                if ( mw.rcfilters.featureFlags.liveUpdate ) {
                        $bottom.append( this.liveUpdateButton.$element );
                }
                this.$element
                        .addClass( 'mw-rcfilters-ui-filterWrapperWidget' )
                        .append(
-                               $top,
+                               this.$top,
                                this.filterTagWidget.$element,
                                $bottom
                        );
        /* Methods */
 
        /**
-        * Add a widget at the beginning of the top row
+        * Set the content of the top section
         *
-        * @param {OO.ui.Widget} widget Any widget
+        * @param {jQuery} $topSectionElement
         */
-       mw.rcfilters.ui.FilterWrapperWidget.prototype.prependToTopRow = function ( widget ) {
-               this.$topRow.prepend(
-                       widget.$element
-                               .addClass( 'mw-rcfilters-ui-cell' )
-               );
+       mw.rcfilters.ui.FilterWrapperWidget.prototype.setTopSection = function ( $topSectionElement ) {
+               this.$top.append( $topSectionElement );
        };
-
 }( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.RcTopSectionWidget.js
new file mode 100644 (file)
index 0000000..f0e1241
--- /dev/null
@@ -0,0 +1,110 @@
+( function ( mw ) {
+       /**
+        * Top section (between page title and filters) on Special:Recentchanges
+        *
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+        * @param {jQuery} $topLinks Content of the community-defined links
+        * @param {Object} [config] Configuration object
+        */
+       mw.rcfilters.ui.RcTopSectionWidget = function MwRcfiltersUiRcTopSectionWidget(
+               savedLinksListWidget, $topLinks, config
+       ) {
+               var toplinksTitle,
+                       topLinksCookieName = 'rcfilters-toplinks-collapsed-state',
+                       topLinksCookie = mw.cookie.get( topLinksCookieName ),
+                       topLinksCookieValue = topLinksCookie || 'collapsed',
+                       widget = this;
+
+               config = config || {};
+
+               // Parent
+               mw.rcfilters.ui.RcTopSectionWidget.parent.call( this, config );
+
+               this.$topLinks = $topLinks;
+
+               toplinksTitle = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       indicator: topLinksCookieValue === 'collapsed' ? 'down' : 'up',
+                       flags: [ 'progressive' ],
+                       label: $( '<span>' ).append( mw.message( 'rcfilters-other-review-tools' ).parse() ).contents()
+               } );
+
+               this.$topLinks
+                       .addClass( 'mw-rcfilters-ui-ready' )
+                       .makeCollapsible( {
+                               collapsed: topLinksCookieValue === 'collapsed',
+                               $customTogglers: toplinksTitle.$element
+                       } )
+                       .on( 'beforeExpand.mw-collapsible', function () {
+                               mw.cookie.set( topLinksCookieName, 'expanded' );
+                               toplinksTitle.setIndicator( 'up' );
+                               widget.switchTopLinks( 'expanded' );
+                       } )
+                       .on( 'beforeCollapse.mw-collapsible', function () {
+                               mw.cookie.set( topLinksCookieName, 'collapsed' );
+                               toplinksTitle.setIndicator( 'down' );
+                               widget.switchTopLinks( 'collapsed' );
+                       } );
+
+               this.$topLinks.find( '.mw-recentchanges-toplinks-title' ).replaceWith( toplinksTitle.$element );
+
+               // Create two positions for the toplinks to toggle between
+               // in the table (first cell) or up above it
+               this.$top = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-top' );
+               this.$tableTopLinks = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-cell' )
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-topLinks-table' );
+
+               // Initialize
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-rcTopSectionWidget' )
+                       .append(
+                               this.$top,
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-table' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-row' )
+                                                       .append(
+                                                               this.$tableTopLinks,
+                                                               $( '<div>' )
+                                                                       .addClass( 'mw-rcfilters-ui-table-placeholder' )
+                                                                       .addClass( 'mw-rcfilters-ui-cell' ),
+                                                               !mw.user.isAnon() ?
+                                                                       $( '<div>' )
+                                                                               .addClass( 'mw-rcfilters-ui-cell' )
+                                                                               .addClass( 'mw-rcfilters-ui-rcTopSectionWidget-savedLinks' )
+                                                                               .append( savedLinksListWidget.$element ) :
+                                                                       null
+                                                       )
+                                       )
+                       );
+
+               // Initialize top links position
+               widget.switchTopLinks( topLinksCookieValue );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.RcTopSectionWidget, OO.ui.Widget );
+
+       /**
+        * Switch the top links widget from inside the table (when collapsed)
+        * to the 'top' (when open)
+        *
+        * @param {string} [state] The state of the top links widget: 'expanded' or 'collapsed'
+        */
+       mw.rcfilters.ui.RcTopSectionWidget.prototype.switchTopLinks = function ( state ) {
+               state = state || 'expanded';
+
+               if ( state === 'expanded' ) {
+                       this.$top.append( this.$topLinks );
+               } else {
+                       this.$tableTopLinks.append( this.$topLinks );
+               }
+       };
+}( mediaWiki ) );
diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.WatchlistTopSectionWidget.js
new file mode 100644 (file)
index 0000000..86c206b
--- /dev/null
@@ -0,0 +1,82 @@
+( function ( mw ) {
+       /**
+        * Top section (between page title and filters) on Special:Watchlist
+        *
+        * @extends OO.ui.Widget
+        *
+        * @constructor
+        * @param {mw.rcfilters.Controller} controller
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {mw.rcfilters.ui.SavedLinksListWidget} savedLinksListWidget
+        * @param {jQuery} $watchlistDetails Content of the 'details' section that includes watched pages count
+        * @param {Object} [config] Configuration object
+        */
+       mw.rcfilters.ui.WatchlistTopSectionWidget = function MwRcfiltersUiWatchlistTopSectionWidget(
+               controller, changesListModel, savedLinksListWidget, $watchlistDetails, config
+       ) {
+               var editWatchlistButton,
+                       markSeenButton,
+                       $topTable,
+                       $bottomTable,
+                       $separator;
+               config = config || {};
+
+               // Parent
+               mw.rcfilters.ui.WatchlistTopSectionWidget.parent.call( this, config );
+
+               editWatchlistButton = new OO.ui.ButtonWidget( {
+                       label: mw.msg( 'rcfilters-watchlist-editWatchlist-button' ),
+                       icon: 'edit',
+                       href: mw.config.get( 'wgStructuredChangeFiltersEditWatchlistUrl' )
+               } );
+               markSeenButton = new mw.rcfilters.ui.MarkSeenButtonWidget( controller, changesListModel );
+
+               $topTable = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-table' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-row' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-watchlistDetails' )
+                                                       .append( $watchlistDetails )
+                                       )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-editWatchlistButton' )
+                                                       .append( editWatchlistButton.$element )
+                                       )
+                       );
+
+               $bottomTable = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-table' )
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 'mw-rcfilters-ui-row' )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .append( markSeenButton.$element )
+                                       )
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-cell' )
+                                                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-savedLinks' )
+                                                       .append( savedLinksListWidget.$element )
+                                       )
+                       );
+
+               $separator = $( '<div>' )
+                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget-separator' );
+
+               this.$element
+                       .addClass( 'mw-rcfilters-ui-watchlistTopSectionWidget' )
+                       .append( $topTable, $separator, $bottomTable );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.rcfilters.ui.WatchlistTopSectionWidget, OO.ui.Widget );
+}( mediaWiki ) );
index 47ecfe4..640bbff 100644 (file)
@@ -1,7 +1,7 @@
 
 button_italic.png
 -------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
+Source : https://commons.wikimedia.org/wiki/Image:Button_S_italic.png
 License: Public domain
-Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
+Author : Purodha Blissenbach, https://ksh.wikipedia.org/wiki/User:Purodha
 
index 6dd0925..b65136a 100644 (file)
@@ -363,6 +363,7 @@ class CommentStoreTest extends MediaWikiLangTestCase {
                                $this->assertArrayNotHasKey( "{$key}_id", $fields, "new field, stage=$writeStage" );
                        }
 
+                       $extraFields[$pk] = $this->db->nextSequenceValue( "{$table}_{$pk}_seq" );
                        $this->db->insert( $table, $extraFields + $fields, __METHOD__ );
                        $id = $this->db->insertId();
                        if ( $usesTemp ) {
@@ -404,17 +405,25 @@ class CommentStoreTest extends MediaWikiLangTestCase {
        }
 
        public static function provideInsertRoundTrip() {
+               $db = wfGetDB( DB_REPLICA ); // for timestamps
+
                $msgComment = new Message( 'parentheses', [ 'message comment' ] );
                $textCommentMsg = new RawMessage( '$1', [ 'text comment' ] );
                $nestedMsgComment = new Message( [ 'parentheses', 'rawmessage' ], [ new Message( 'mainpage' ) ] );
                $ipbfields = [
                        'ipb_range_start' => '',
                        'ipb_range_end' => '',
+                       'ipb_by' => 0,
+                       'ipb_timestamp' => $db->timestamp(),
+                       'ipb_expiry' => $db->getInfinity(),
                ];
                $revfields = [
                        'rev_page' => 42,
                        'rev_text_id' => 42,
                        'rev_len' => 0,
+                       'rev_user' => 0,
+                       'rev_user_text' => '',
+                       'rev_timestamp' => $db->timestamp(),
                ];
                $comStoreComment = new CommentStoreComment(
                        null, 'comment store comment', null, [ 'foo' => 'bar' ]