jenkins-bot has submitted this change and it was merged.

Change subject: Suggestions: Public task lists based on category and their 
paging
......................................................................


Suggestions: Public task lists based on category and their paging

A script to create a suggestion list based on a category is added.

Pages that exist in the source language and not in the target language
will be added to that list. There is no UI to create or manage
this list. For now it's just a maintenance script.

Category based task lists are presented as collapsed list in the UI
with just two items. The expanded list can be collapsed, too.

Since we are getting more lists, infinite scroll behavior has changed
significantly according to T115008.

When you scroll, if there is any expanded list present, more items from
it will be loaded. If there is no expanded list, when you scroll, just
the scroll happens.

There is no infinite scroll for the default suggestions
(the featured list suggestions, also called "fallback suggestions").
Instead there is a trigger to refresh that list with new items.

Since lists need to manage their own offset and continue params,
they were removed from class level and added as member of list objects.

Bug: T115008
Bug: T115006
Change-Id: I3eb29a2337f2ddbb2bc9bcf86f8efcfa532b0984
---
M api/ApiQueryContentTranslationSuggestions.php
M extension.json
M i18n/en.json
M i18n/qqq.json
M includes/SuggestionList.php
M includes/SuggestionListManager.php
M modules/dashboard/ext.cx.suggestionlist.js
M modules/dashboard/styles/ext.cx.suggestionlist.less
M scripts/manage-lists.php
9 files changed, 412 insertions(+), 119 deletions(-)

Approvals:
  Santhosh: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/api/ApiQueryContentTranslationSuggestions.php 
b/api/ApiQueryContentTranslationSuggestions.php
index 79e10bc..d3d2ac1 100644
--- a/api/ApiQueryContentTranslationSuggestions.php
+++ b/api/ApiQueryContentTranslationSuggestions.php
@@ -38,45 +38,59 @@
                if ( !$config->get( 'ContentTranslationEnableSuggestions' ) ) {
                        $this->dieUsage( 'Suggestions not enabled for this 
wiki', 'suggestionsdisabled' );
                }
+
                $params = $this->extractRequestParams();
                $result = $this->getResult();
                $user = $this->getUser();
+
                $from = $params['from'];
                $to = $params['to'];
                if ( $from === $to ) {
                        $this->dieUsage(
-                               'Source and target languages cannot be the 
same. Use from, to API params.',
+                               'Source and target languages cannot be the 
same',
                                'invalidparam'
                        );
                }
-
+               $data = null;
                $translator = new Translator( $user );
                $manager = new SuggestionListManager();
-               // Get non-personalized suggestions
-               $data = $manager->getPublicSuggestions(
-                       $from,
-                       $to,
-                       $params['limit'],
-                       $params['offset'],
-                       $params['seed']
-               );
 
                // Get personalized suggestions.
                // We do not want to send personalized suggestions in paginated 
results
                // other than the first page. Hence checking offset.
-               if ( $params['offset'] === null ) {
+               if ( $params['listid'] !== null ) {
+                       $list = $manager->getListById( $params['listid'] );
+                       if ( $list === null ) {
+                               $this->dieUsage( 'Invalid list id', 
'invalidparam' );
+                       }
+
+                       $suggestions = $manager->getSuggestionsInList(
+                               $list->getId(),
+                               $from,
+                               $to,
+                               $params['limit'],
+                               $params['offset'],
+                               $params['seed']
+                       );
+                       $data = array(
+                               'lists' => array( $list ),
+                               'suggestions' => $suggestions,
+                       );
+               } else {
                        $personalizedSuggestions = 
$manager->getPersonalizedSuggestions(
                                $translator->getGlobalUserId(),
                                $from,
                                $to
                        );
+                       // Get non-personalized suggestions
+                       $publicSuggestions = $manager->getPublicSuggestions( 
$from, $to, $params['limit'] );
                        if ( count( $personalizedSuggestions['lists'] ) ) {
                                // Merge the personal lists to public lists. 
There won't be duplicates
                                // because the list of lists is an associative 
array with listId as a key.
-                               $data['lists'] = array_merge( $data['lists'], 
$personalizedSuggestions['lists'] );
+                               $data['lists'] = array_merge( 
$personalizedSuggestions['lists'], $publicSuggestions['lists'] );
                                $data['suggestions'] = array_merge(
-                                       $data['suggestions'],
-                                       $personalizedSuggestions['suggestions']
+                                       $personalizedSuggestions['suggestions'],
+                                       $publicSuggestions['suggestions']
                                );
                        }
                }
@@ -218,6 +232,9 @@
                                ApiBase::PARAM_TYPE => 'string',
                                ApiBase::PARAM_REQUIRED => true,
                        ),
+                       'listid' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                       ),
                        'limit' => array(
                                ApiBase::PARAM_DFLT => 10,
                                ApiBase::PARAM_TYPE => 'limit',
@@ -227,11 +244,9 @@
                        ),
                        'offset' => array(
                                ApiBase::PARAM_TYPE => 'string',
-                               ApiBase::PARAM_DFLT => null,
                        ),
                        'seed' => array(
                                ApiBase::PARAM_TYPE => 'integer',
-                               ApiBase::PARAM_DFLT => null,
                        ),
                );
                return $allowedParams;
diff --git a/extension.json b/extension.json
index fd8a700..1ac3c57 100644
--- a/extension.json
+++ b/extension.json
@@ -827,12 +827,13 @@
                                "jquery.uls.data"
                        ],
                        "messages": [
-                               "cx-suggestionlist-favorite",
-                               "cx-suggestionlist-title",
-                               "cx-suggestionlist-empty-title",
+                               "cx-suggestionlist-collapse",
                                "cx-suggestionlist-empty-desc",
-                               "cx-suggestionlist-view-source-page",
-                               "cx-suggestionlist-featured"
+                               "cx-suggestionlist-empty-title",
+                               "cx-suggestionlist-expand",
+                               "cx-suggestionlist-featured",
+                               "cx-suggestionlist-refresh",
+                               "cx-suggestionlist-view-source-page"
                        ]
                },
                "ext.cx.translation.conflict": {
diff --git a/i18n/en.json b/i18n/en.json
index 9c68356..88d040f 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -197,5 +197,8 @@
        "mw-pageselector-missing": "Page does not exist",
        "cx-draft-restoring": "Loading the saved translation...",
        "cx-draft-restored": "Saved translation loaded.",
-       "cx-draft-restore-failed": "Failed to load the saved translation!"
+       "cx-draft-restore-failed": "Failed to load the saved translation!",
+       "cx-suggestionlist-expand": "View all",
+       "cx-suggestionlist-collapse": "View less",
+       "cx-suggestionlist-refresh": "Refresh suggestions"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index e2c4c24..424d6e7 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -202,5 +202,8 @@
        "mw-pageselector-missing": "Message shown in page selector when the 
search did not fetch any result",
        "cx-draft-restoring": "Message indicating the saved translation being 
fetched.",
        "cx-draft-restored": "Message indicating that the saved translation was 
fetched.",
-       "cx-draft-restore-failed": "Message indicating that the fetching of 
saved translation failed."
+       "cx-draft-restore-failed": "Message indicating that the fetching of 
saved translation failed.",
+       "cx-suggestionlist-expand": "Label for the suggestions list expand 
trigger",
+       "cx-suggestionlist-collapse": "Label for the suggestions list collapse 
trigger",
+       "cx-suggestionlist-refresh": "Label for the suggestions list refresh 
trigger"
 }
diff --git a/includes/SuggestionList.php b/includes/SuggestionList.php
index e6baa46..754074c 100644
--- a/includes/SuggestionList.php
+++ b/includes/SuggestionList.php
@@ -3,10 +3,13 @@
 namespace ContentTranslation;
 
 class SuggestionList {
+       // List types. Make sure to add them in ext.cx.suggestionlist.js to
+       // achieve wanted display order.
        const TYPE_DEFAULT = 0;
        const TYPE_FEATURED = 1;
        const TYPE_DISCARDED = 2;
        const TYPE_FAVORITE = 3;
+       const TYPE_CATEGORY = 4;
 
        protected $id;
        protected $name;
@@ -75,12 +78,16 @@
                return $this->name;
        }
 
+       /**
+        * @return \Message
+        */
        public function getDisplayNameMessage( \IContextSource $context ) {
-               if ( $this->getType() === self::TYPE_FEATURED ) {
-                       return $context->msg( 'cx-suggestionlist-featured' );
+               $message = $context->msg( $this->getName() );
+               if ( $message->exists() ) {
+                       return $message;
+               } else {
+                       return new \RawMessage( \Title::newFromText( 
$this->getName() )->getText() );
                }
-
-               return new \RawMessage( $this->getName() );
        }
 
        public function getInfo() {
@@ -112,7 +119,7 @@
                        return self::TYPE_DEFAULT;
                }
 
-               return $this->type;
+               return (int)$this->type;
        }
 
        /**
diff --git a/includes/SuggestionListManager.php 
b/includes/SuggestionListManager.php
index 5b06185..a3fc87e 100644
--- a/includes/SuggestionListManager.php
+++ b/includes/SuggestionListManager.php
@@ -65,22 +65,33 @@
                );
        }
 
-       public function getListByName( $name, $owner = 0 ) {
+       protected function getListByConds( array $conds ) {
                $dbr = Database::getConnection( DB_MASTER );
-               $row = $dbr->selectRow(
-                       'cx_lists',
-                       '*',
-                       array(
-                               'cxl_name' => $name,
-                               'cxl_owner' => $owner,
-                       ),
-                       __METHOD__
-               );
+               $row = $dbr->selectRow( 'cx_lists', '*', $conds, __METHOD__ );
 
                if ( $row ) {
                        return SuggestionList::newFromRow( $row );
                }
+
                return null;
+       }
+
+       public function getListByName( $name, $owner = 0 ) {
+               $conds = array(
+                       'cxl_name' => $name,
+                       'cxl_owner' => $owner,
+               );
+
+               return $this->getListByConds( $conds );
+       }
+
+       public function getListById( $id ) {
+               $conds = array(
+                       'cxl_id' => $id,
+                       'cxl_owner' => 0,
+               );
+
+               return $this->getListByConds( $conds );
        }
 
        /**
@@ -241,7 +252,30 @@
         * @param int $seed Seed to use with randomizing of results.
         * @return array Lists and suggestions
         */
-       public function getPublicSuggestions( $from, $to, $limit, $offset, 
$seed ) {
+       public function getPublicSuggestions( $from, $to, $limit ) {
+               return $this->getSuggestionsByType(
+                       array(
+                               SuggestionList::TYPE_CATEGORY,
+                               SuggestionList::TYPE_FEATURED
+                       ),
+                       $from,
+                       $to,
+                       $limit
+                );
+       }
+
+       /**
+        * Get public suggestions by list type
+        *
+        * @param string $type List type.
+        * @param string $from Source language code.
+        * @param string $to Target language code.
+        * @param int $limit How many suggestions to fetch.
+        * @param int $offset Offset from the beginning to fetch.
+        * @param int $seed Seed to use with randomizing of results.
+        * @return array Lists and suggestions
+        */
+       public function getSuggestionsByType( $type, $from, $to, $limit, 
$offset = null, $seed = null ) {
                $dbw = Database::getConnection( DB_MASTER );
 
                $lists = array();
@@ -251,40 +285,22 @@
                        'cx_lists',
                        '*',
                        array(
-                               'cxl_type' => SuggestionList::TYPE_FEATURED,
+                               'cxl_type' => $type,
                                'cxl_public' => true,
                        ),
-                       __METHOD__
+                       __METHOD__,
+                       array(
+                               'ORDER BY' => 'cxl_type desc'
+                       )
                );
 
                foreach ( $res as $row ) {
                        $list = SuggestionList::newFromRow( $row );
                        $lists[$list->getId()] = $list;
-               }
-
-               if ( count( $lists ) ) {
-                       $conds = array(
-                               'cxs_list_id' => array_keys( $lists ),
-                               'cxs_source_language' => $from,
-                               'cxs_target_language' => array( $to, '*' ),
+                       $suggestions = array_merge(
+                               $suggestions,
+                               $this->getSuggestionsInList( $list->getId(), 
$from, $to, $limit, $offset, $seed )
                        );
-
-                       $seed = (int)$seed;
-
-                       $options = array(
-                               'LIMIT' => $limit,
-                               'ORDER BY' => "RAND( $seed )"
-                       );
-
-                       if ( $offset ) {
-                               $options['OFFSET'] = $offset;
-                       }
-
-                       $res = $dbw->select( 'cx_suggestions', '*', $conds, 
__METHOD__, $options );
-
-                       foreach ( $res as $row ) {
-                               $suggestions[] = Suggestion::newFromRow( $row );
-                       }
                }
 
                return array(
@@ -292,4 +308,49 @@
                        'suggestions' => $suggestions,
                );
        }
+
+       /**
+        * Get the suggestions by list id
+        *
+        * @param string $listId List id.
+        * @param string $from Source language code.
+        * @param string $to Target language code.
+        * @param int $limit How many suggestions to fetch.
+        * @param int $offset Offset from the beginning to fetch.
+        * @param int $seed Seed to use with randomizing of results.
+        * @return Suggestion[] Suggestions
+        */
+       public function getSuggestionsInList( $listId, $from, $to, $limit, 
$offset, $seed ) {
+               $suggestions = array();
+               $dbr = Database::getConnection( DB_MASTER );
+
+               $seed = (int)$seed;
+
+               $options = array(
+                       'LIMIT' => $limit,
+                       'ORDER BY' => "RAND( $seed )"
+               );
+
+               if ( $offset ) {
+                       $options['OFFSET'] = $offset;
+               }
+
+               $res = $dbr->select(
+                       array( 'cx_suggestions' ),
+                       '*',
+                       array(
+                               'cxs_source_language' => $from,
+                               'cxs_target_language' => $to,
+                               'cxs_list_id' => $listId
+                       ),
+                       __METHOD__,
+                       $options
+               );
+
+               foreach ( $res as $row ) {
+                       $suggestions[] = Suggestion::newFromRow( $row );
+               }
+
+               return $suggestions;
+       }
 }
diff --git a/modules/dashboard/ext.cx.suggestionlist.js 
b/modules/dashboard/ext.cx.suggestionlist.js
index 0e56f88..273c565 100644
--- a/modules/dashboard/ext.cx.suggestionlist.js
+++ b/modules/dashboard/ext.cx.suggestionlist.js
@@ -10,13 +10,21 @@
        'use strict';
 
        // Name of the empty list, used to show when there is no suggestions
-       var emptyListName = 'cx-suggestionlist-empty';
+       var
+               emptyListName = 'cx-suggestionlist-empty',
+               listTypes = {
+                       TYPE_DEFAULT: 0,
+                       TYPE_FEATURED: 1,
+                       TYPE_DISCARDED: 2,
+                       TYPE_FAVORITE: 3,
+                       TYPE_CATEGORY: 4
+               };
 
        /**
         * CXSuggestionList
         *
         * @param {jQuery} $container The container for this suggestion list
-        * @param {Object} The stitempper instance
+        * @param {Object} siteMapper
         * @class
         */
        function CXSuggestionList( $container, siteMapper ) {
@@ -25,9 +33,6 @@
                this.suggestions = [];
                this.sourceLanguage = null;
                this.targetLanguage = null;
-               this.promise = null;
-               this.queryContinue = null;
-               this.hasMore = true;
                this.lists = {};
                this.active = false;
                this.filters = {
@@ -68,17 +73,28 @@
                };
        };
 
-       CXSuggestionList.prototype.loadItems = function () {
-               var lists, list, listId, promise,
+       /**
+        * @param {Object} [list]
+        */
+       CXSuggestionList.prototype.loadItems = function ( list ) {
+               var lists, promise,
                        isEmpty = true,
                        self = this;
 
-               if ( this.promise ) {
-                       return this.promise;
+               if ( !list ) {
+                       // Initial load, load available lists and couple of 
suggestions for them
+                       promise = this.getSuggestionLists();
+               } else {
+                       // Afterwards, suggestions are loaded per list
+                       if ( list.promise ) {
+                               return;
+                       }
+
+                       promise = this.loadSuggestionsForList( list );
                }
-               promise = this.getSuggestionLists();
+
                promise.done( function ( suggestions ) {
-                       var i, listIds;
+                       var i, list, listId, listIds;
 
                        lists = suggestions.lists;
                        // Hide empty list, if any
@@ -86,21 +102,18 @@
                                self.lists[ emptyListName ].$list.hide();
                        }
 
-                       listIds = Object.keys( lists );
-                       // Sort the items based on type. Descending order.
-                       // When we have many lists, we will require more 
intellignet list sorting
-                       listIds.sort( function ( a, b ) {
-                               return lists[ a ].type < lists[ b ].type;
-                       } );
-
+                       listIds = self.sortLists( lists );
                        for ( i = 0; i < listIds.length; i++ ) {
                                listId = listIds[ i ];
                                list = lists[ listId ];
                                if ( self.lists[ listId ] ) {
                                        // Add new set of suggestions to 
existing list
-                                       self.lists[ listId 
].suggestions.concat( list.suggestions );
+                                       self.lists[ listId ].suggestions =
+                                               self.lists[ listId 
].suggestions.concat( list.suggestions );
                                } else {
                                        // Add as new list
+                                       list.id = listId;
+                                       list.seed = self.seed;
                                        self.lists[ listId ] = list;
                                }
                                if ( self.lists[ listId ].suggestions.length ) {
@@ -114,7 +127,6 @@
                                self.showEmptySuggestionList();
                        }
                } ).fail( function () {
-                       self.promise = null;
                        // Show empty list
                        self.showEmptySuggestionList();
                } );
@@ -128,36 +140,57 @@
         * @return {jQuery.Promise}
         */
        CXSuggestionList.prototype.getSuggestionLists = function () {
-               var self = this,
-                       params,
+               var params,
                        api = new mw.Api();
 
-               if ( this.promise ) {
-                       // Avoid duplicate API requests.
-                       return this.promise;
-               }
-               if ( this.hasMore === false ) {
-                       // This method is supposed to call only if we there are 
items to fetch
+               params = {
+                       action: 'query',
+                       list: 'contenttranslationsuggestions',
+                       from: this.filters.sourceLanguage,
+                       to: this.filters.targetLanguage,
+                       limit: 4,
+                       seed: this.seed
+               };
+
+               return api.get( params ).then( function ( response ) {
+                       return response.query.contenttranslationsuggestions;
+               } );
+       };
+
+       /**
+        * Get all the translation suggestion lists of given user.
+        *
+        * @param {Object} list
+        * @return {jQuery.Promise}
+        */
+       CXSuggestionList.prototype.loadSuggestionsForList = function ( list ) {
+               var params,
+                       api = new mw.Api();
+
+
+               if ( list.hasMore === false ) {
+                       // This method is supposed to be called only if we 
there are items to fetch
                        return $.Deferred().reject();
                }
 
                params = $.extend( {
                        action: 'query',
                        list: 'contenttranslationsuggestions',
+                       listid: list.id,
                        from: this.filters.sourceLanguage,
                        to: this.filters.targetLanguage,
                        limit: 10,
-                       seed: this.seed
-               }, this.queryContinue );
+                       seed: list.seed
+               }, list.queryContinue );
 
-               this.promise = api.get( params ).then( function ( response ) {
-                       self.promise = null;
-                       self.queryContinue = response.continue;
-                       self.hasMore = !!response.continue;
+               list.promise = api.get( params ).then( function ( response ) {
+                       list.promise = undefined;
+                       list.queryContinue = response.continue;
+                       list.hasMore = !!response.continue;
                        return response.query.contenttranslationsuggestions;
                } );
 
-               return this.promise;
+               return list.promise;
        };
 
        CXSuggestionList.prototype.show = function () {
@@ -197,10 +230,6 @@
                        }
                } );
                this.lists = {};
-               // Reset the ongoing request trackers
-               this.hasMore = true;
-               this.queryContinue = null;
-               this.promise = null;
                this.loadItems();
        };
 
@@ -230,7 +259,7 @@
        /**
         * Show a title image of the translation based on source title.
         *
-        * @param {Object} suggestion
+        * @param {Object} suggestions
         */
        CXSuggestionList.prototype.showTitleImageAndDesc = function ( 
suggestions ) {
                var apply,
@@ -254,6 +283,9 @@
                } );
 
                apply = function ( page ) {
+                       if ( !map[ page.title ] ) {
+                               return;
+                       }
                        $.each( map[ page.title ], function ( i, item ) {
                                if ( page.thumbnail ) {
                                        item.$image.css( {
@@ -292,8 +324,9 @@
                // Create the list container if not present already.
                if ( !list.$list ) {
                        list.$list = $( '<div>' )
+                               .attr( 'data-listid', listId )
                                .addClass( 'cx-suggestionlist ' + list.name );
-                       $listHeading = $( '<h2>' ).text( mw.msg( list.name ) );
+                       $listHeading = $( '<h2>' ).text( list.displayName );
                        list.$list.append( $listHeading );
 
                        if ( addtoTop && this.$container.find( 
'.cx-suggestionlist' ).length ) {
@@ -312,7 +345,19 @@
                        $suggestions.push( $suggestion );
                }
                this.showTitleImageAndDesc( suggestions );
-               list.$list.append( $suggestions );
+
+               // Insert after last suggestion, but before any buttons etc.
+               if ( list.$list.find( '.cx-slitem:last' ).length ) {
+                       list.$list.find( '.cx-slitem:last' ).after( 
$suggestions );
+               } else {
+                       list.$list.append( $suggestions );
+               }
+
+               if ( list.type === listTypes.TYPE_CATEGORY && 
list.suggestions.length > 2 ) {
+                       this.makeExpandableList( listId );
+               } else if ( list.type === listTypes.TYPE_FEATURED ) {
+                       this.addRefreshTrigger( listId );
+               }
        };
 
        /**
@@ -390,13 +435,13 @@
                        .one( 'click', $.proxy( this.discardSuggestion, this, 
suggestion ) );
 
                $featured = $( [] );
-               if ( this.lists[ suggestion.listId ].name === 
'cx-suggestionlist-featured' ) {
+               if ( this.lists[ suggestion.listId ].type === 
listTypes.TYPE_FEATURED ) {
                        $featured = $( '<div>' )
                                .addClass( 'cx-sltag cx-sltag--featured' )
-                               .text( mw.msg( this.lists[ suggestion.listId 
].displayName ) );
+                               .text( this.lists[ suggestion.listId 
].displayName );
                }
 
-               if ( this.lists[ suggestion.listId ].name === 
'cx-suggestionlist-favorite' ) {
+               if ( this.lists[ suggestion.listId ].type === 
listTypes.TYPE_FAVORITE ) {
                        $discardAction.hide();
                        $favoriteAction = $( '<div>' )
                                .addClass( 'cx-slitem__action 
cx-slitem__action--nonfavorite' )
@@ -619,7 +664,8 @@
         * Scroll handler for the suggestions
         */
        CXSuggestionList.prototype.scroll = function () {
-               var $window, scrollTop, windowHeight, offsetTop;
+               var expandedListId, $expandedList, $window, triggerPos,
+                       scrollTop, windowHeight, offsetTop, visibleArea, 
$loadTrigger;
 
                if ( !this.active ) {
                        return;
@@ -628,17 +674,125 @@
                scrollTop = $window.scrollTop();
                windowHeight = $window.height();
                offsetTop = this.$container.offset().top;
-
+               visibleArea = windowHeight + scrollTop;
                if ( scrollTop > offsetTop ) {
                        this.$container.addClass( 'sticky' );
                } else if ( scrollTop <= offsetTop ) {
                        this.$container.removeClass( 'sticky' );
                }
 
-               // Load next batch of items on scroll.
-               if ( this.hasMore && scrollTop + windowHeight + 100 > $( 
document ).height() ) {
-                       this.loadItems();
+               // Load next batch of items when loadTrigger is in viewpot
+               $expandedList = this.$container.find( 
'.cx-suggestionlist--expanded' );
+               $loadTrigger = $expandedList.find( 
'.cx-suggestionlist__collapse' );
+
+               if ( !$loadTrigger.length ) {
+                       return;
                }
+
+               triggerPos = $loadTrigger.offset().top + 
$loadTrigger.outerHeight();
+               expandedListId = $expandedList.data( 'listid' );
+               if (
+                       expandedListId &&
+                       this.lists[ expandedListId ].hasMore !== false &&
+                       visibleArea >= triggerPos && scrollTop <= triggerPos
+               ) {
+                       this.loadItems( this.lists[ expandedListId ] );
+               }
+       };
+
+       /**
+        * Sort the lists in their logical order to display.
+        *
+        * @param  {Object[]} lists
+        * @return {number[]} Orderded list ids.
+        */
+       CXSuggestionList.prototype.sortLists = function ( lists ) {
+               var order, ids;
+
+               order = {};
+               order[ listTypes.TYPE_FAVORITE ] = 0;
+               order[ listTypes.TYPE_DISCARDED ] = 1;
+               order[ listTypes.TYPE_CATEGORY ] = 2;
+               order[ listTypes.TYPE_FEATURED ] = 3;
+               order[ listTypes.TYPE_DEFAULT ] = 4;
+
+               ids = Object.keys( lists ).sort( function ( a, b ) {
+                       return order[ lists[ a ].type ] - order[ lists[ b 
].type ];
+               } );
+
+               return ids;
+       };
+
+       /**
+        * Make the list expandable and collapsable.
+        *
+        * @param {string} listId
+        */
+       CXSuggestionList.prototype.makeExpandableList = function ( listId ) {
+               var self = this,
+                       list = this.lists[ listId ];
+               if ( list.$list.is( '.cx-suggestionlist--collapsed' ) ||
+                       list.$list.is( '.cx-suggestionlist--expanded' )
+               ) {
+                       return;
+               }
+
+               list.$list.append( $( '<div>' )
+                       .addClass( 'cx-suggestionlist__expand' )
+                       .text( mw.msg( 'cx-suggestionlist-expand' ) )
+                       .on( 'click', function () {
+                               if ( $( this ).is( '.cx-suggestionlist__expand' 
) ) {
+                                       $( this ).text( mw.msg( 
'cx-suggestionlist-collapse' ) );
+                                       // Collapse all expended lists.
+                                       self.$container.find( 
'.cx-suggestionlist__collapse' ).trigger( 'click' );
+                               } else {
+                                       $( this ).text( mw.msg( 
'cx-suggestionlist-expand' ) );
+                               }
+                               $( this )
+                                       .toggleClass( 
'cx-suggestionlist__collapse cx-suggestionlist__expand' )
+                                       .parent()
+                                       .toggleClass( 
'cx-suggestionlist--collapsed cx-suggestionlist--expanded' );
+
+                       } )
+               );
+               list.$list.addClass( 'cx-suggestionlist--collapsed' );
+       };
+
+       /**
+        * Make the list refreshable
+        *
+        * @param {string} listId
+        */
+       CXSuggestionList.prototype.addRefreshTrigger = function ( listId ) {
+               var i, suggestion,
+                       self = this,
+                       list = this.lists[ listId ];
+
+               if ( list.$list.find( '.cx-suggestionlist__refresh' ).length ) {
+                       return;
+               }
+
+               list.$list.append( $( '<div>' )
+                       .addClass( 'cx-suggestionlist__refresh' )
+                       .text( mw.msg( 'cx-suggestionlist-refresh' ) )
+                       .on( 'click', function () {
+                               if ( list.suggestions ) {
+                                       for ( i = 0; i < 
list.suggestions.length; i++ ) {
+                                               suggestion = list.suggestions[ 
i ];
+                                               suggestion.$element.hide();
+                                       }
+                               }
+                               list.suggestions = [];
+                               // Do not run out of suggestions
+                               list.seed = parseInt( Math.random() * 10000, 10 
);
+                               list.queryContinue = undefined;
+                               list.hasMore = true;
+                               // FIXME: Till the new items arrive, this list 
will become empty.
+                               // May be we need to keep the height of 
container and show a loading
+                               // indicator?
+                               self.loadItems( list );
+                       } )
+               );
        };
 
        mw.cx.CXSuggestionList = CXSuggestionList;
diff --git a/modules/dashboard/styles/ext.cx.suggestionlist.less 
b/modules/dashboard/styles/ext.cx.suggestionlist.less
index abbf489..a1afbc1 100644
--- a/modules/dashboard/styles/ext.cx.suggestionlist.less
+++ b/modules/dashboard/styles/ext.cx.suggestionlist.less
@@ -7,6 +7,29 @@
        border-bottom: none;
 }
 
+.cx-suggestionlist--collapsed {
+       // First item is heading and then next 2 items.
+       // So hide all items starting at 4th child.
+       .cx-slitem:nth-child(n+4) {
+               display: none;
+       }
+}
+
+.cx-suggestionlist__collapse,
+.cx-suggestionlist__expand,
+.cx-suggestionlist__refresh {
+       .mw-ui-one-whole;
+       padding: 10px;
+       background-color: #fff;
+       border: 1px solid #ddd;
+       cursor: pointer;
+       font-size: medium;
+       text-align: center;
+       &:hover {
+               background-color: #eff3fb;
+       }
+}
+
 .cx-slitem__desc {
        color: #555;
        font-size: 1.2em;
diff --git a/scripts/manage-lists.php b/scripts/manage-lists.php
index 818b5a8..c979d1c 100644
--- a/scripts/manage-lists.php
+++ b/scripts/manage-lists.php
@@ -50,6 +50,12 @@
                        true,
                        true
                );
+               $this->addOption(
+                       'type',
+                       'Type of the list. Allowed values: (a) featured (b) 
category',
+                       true,
+                       true
+               );
        }
 
        public function execute() {
@@ -57,6 +63,7 @@
                $sourceDomain = SiteMapper::getDomainCode( $this->getOption( 
'source' ) );
                $targetDomain = SiteMapper::getDomainCode( $this->getOption( 
'target' ) );
                $category = $this->getOption( 'category' );
+               $type = $this->getOption( 'type' );
 
                if ( $this->dryrun ) {
                        $this->output( "DRY-RUN mode: actions are NOT 
executed\n" );
@@ -70,7 +77,11 @@
                $count = count( $pages );
 
                if ( !$this->dryrun ) {
-                       $this->createFeaturedSuggestions( $pages );
+                       if ( $type === 'featured' ) {
+                               $this->createFeaturedSuggestions( $pages );
+                       } elseif ( $type === 'category' ) {
+                               $this->createCategoryList( $category, $pages );
+                       }
                        $this->output( "$count pages added to the list 
successfully.\n" );
                } else {
                        $this->output( "Found $count pages:\n" );
@@ -136,16 +147,31 @@
 
        protected function createFeaturedSuggestions( $pages ) {
                $featureListName = 'cx-suggestionlist-featured';
+               $type = SuggestionList::TYPE_FEATURED;
+               $this->createPublicList( $featureListName, $type, $pages );
+       }
+
+       protected function createCategoryList( $category, $pages ) {
+               $name = $category;
+               $type = SuggestionList::TYPE_CATEGORY;
+               $this->createPublicList( $name, $type, $pages );
+       }
+
+       protected function createPublicList( $name, $type, $pages ) {
+               if ( !count( $pages ) ) {
+                       return;
+               }
+
                $sourceLanguage = $this->getOption( 'source' );
                $targetLanguage = $this->getOption( 'target' );
 
                $manager = new SuggestionListManager();
-               $list = $manager->getListByName( $featureListName );
+               $list = $manager->getListByName( $name );
 
                if ( $list === null ) {
                        $list = new SuggestionList( array(
-                               'type' => SuggestionList::TYPE_FEATURED,
-                               'name' => $featureListName,
+                               'type' => $type,
+                               'name' => $name,
                                'public' => true,
                        ) );
                        $listId = $manager->insertList( $list );

-- 
To view, visit https://gerrit.wikimedia.org/r/247261
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I3eb29a2337f2ddbb2bc9bcf86f8efcfa532b0984
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <santhosh.thottin...@gmail.com>
Gerrit-Reviewer: Amire80 <amir.ahar...@mail.huji.ac.il>
Gerrit-Reviewer: Nikerabbit <niklas.laxst...@gmail.com>
Gerrit-Reviewer: Santhosh <santhosh.thottin...@gmail.com>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to