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

Change subject: Add categories to the sidebar
......................................................................


Add categories to the sidebar

Add categories to the board data model and have a categories widget
that synchronizes with the model to present the current categories
in the board.

Bug: T92874
Bug: T111110
Change-Id: Ib9b766f9125e1e0c95798db46bf29ca7e0ac0408
---
M Resources.php
M handlebars/compiled/flow_block_header.handlebars.php
M handlebars/compiled/flow_block_topiclist.handlebars.php
M handlebars/flow_block_header.handlebars
A handlebars/flow_header_categories.partial.handlebars
M includes/View.php
M modules/flow-initialize.js
M modules/flow/dm/api/mw.flow.dm.APIHandler.js
M modules/flow/dm/mw.flow.dm.Board.js
A modules/flow/dm/mw.flow.dm.Categories.js
A modules/flow/dm/mw.flow.dm.CategoryItem.js
M modules/flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js
A modules/flow/ui/widgets/mw.flow.ui.CategoriesWidget.js
A modules/flow/ui/widgets/mw.flow.ui.CategoryItemWidget.js
M modules/styles/board/header.less
A modules/styles/flow/widgets/mw.flow.ui.CategoriesWidget.less
A modules/styles/flow/widgets/mw.flow.ui.CategoryItemWidget.less
17 files changed, 441 insertions(+), 8 deletions(-)

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



diff --git a/Resources.php b/Resources.php
index 19e2fc7..bee4a05 100644
--- a/Resources.php
+++ b/Resources.php
@@ -58,6 +58,7 @@
                        'handlebars/flow_form_cancel_button.partial.handlebars',
                        'handlebars/flow_header_title.partial.handlebars',
                        'handlebars/flow_header_detail.partial.handlebars',
+                       'handlebars/flow_header_categories.partial.handlebars',
                        'handlebars/flow_header_footer.partial.handlebars',
                        // HACK: Get rid of this when the description uses js 
ooui widgets
                        
'handlebars/flow_header_detail_oldsystem.partial.handlebars',
@@ -373,6 +374,8 @@
                        'flow/dm/mw.flow.dm.Post.js',
                        'flow/dm/mw.flow.dm.Topic.js',
                        'flow/dm/mw.flow.dm.Board.js',
+                       'flow/dm/mw.flow.dm.CategoryItem.js',
+                       'flow/dm/mw.flow.dm.Categories.js',
                ),
                'dependencies' => array(
                        'oojs'
@@ -402,6 +405,8 @@
                        'flow/ui/widgets/editor/mw.flow.ui.EditorWidget.js',
                        'flow/ui/widgets/editor/mw.flow.ui.SwitchToVeTool.js',
                        'flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js',
+                       'flow/ui/widgets/mw.flow.ui.CategoryItemWidget.js',
+                       'flow/ui/widgets/mw.flow.ui.CategoriesWidget.js',
                ),
                'styles' => array(
                        'styles/flow/mw.flow.ui.Overlay.less',
@@ -416,7 +421,9 @@
                        
'styles/flow/widgets/editor/mw.flow.ui.EditorControlsWidget.less',
                        
'styles/flow/widgets/editor/mw.flow.ui.EditorSwitcherWidget.less',
                        
'styles/flow/widgets/editor/mw.flow.ui.EditorWidget.less',
-                       
'styles/flow/widgets/editor/editors/mw.flow.ui.WikitextEditorWidget.less'
+                       
'styles/flow/widgets/editor/editors/mw.flow.ui.WikitextEditorWidget.less',
+                       
'styles/flow/widgets/mw.flow.ui.CategoryItemWidget.less',
+                       'styles/flow/widgets/mw.flow.ui.CategoriesWidget.less',
                ),
                'messages' => array(
                        'flow-error-parsoid-failure',
@@ -426,6 +433,9 @@
                        'flow-dialog-cancelconfirm-keep',
                        'flow-dialog-cancelconfirm-discard',
                        'flow-wikitext-switch-editor-tooltip',
+                       'red-link-title',
+                       'pagecategories',
+                       'colon-separator'
                ),
                'dependencies' => array (
                        'oojs-ui',
diff --git a/handlebars/compiled/flow_block_header.handlebars.php 
b/handlebars/compiled/flow_block_header.handlebars.php
index 7995af2..9291a9a 100644
--- a/handlebars/compiled/flow_block_header.handlebars.php
+++ b/handlebars/compiled/flow_block_header.handlebars.php
@@ -62,7 +62,12 @@
 '.$sp.'        <span class="mw-ui-icon mw-ui-icon-before 
mw-ui-icon-speechBubbles pull-right expand-button"
 '.$sp.'                  title="'.LCRun3::ch($cx, 'l10n', 
array(array('flow-board-expand-description'),array()), 'encq').'"></span>
 '.$sp.'</a>
-';},'flow_header_footer' => function ($cx, $in, $sp) {return ''.$sp.'<div 
class="flow-board-header-footer">
+';},'flow_header_categories' => function ($cx, $in, $sp) {return 
''.$sp.''.((LCRun3::ifvar($cx, ((isset($cx['sp_vars']['root']['categories']) && 
is_array($cx['sp_vars']['root'])) ? $cx['sp_vars']['root']['categories'] : 
null))) ? '<div class="flow-board-header-category-view-nojs">
+'.$sp.'        <ul class="flow-board-header-category-list">
+'.$sp.''.LCRun3::sec($cx, ((isset($cx['sp_vars']['root']['categories']) && 
is_array($cx['sp_vars']['root'])) ? $cx['sp_vars']['root']['categories'] : 
null), $in, true, function($cx, $in)use($sp){return '            <li 
class="flow-board-header-category-item">'.LCRun3::ch($cx, 'html', 
array(array($in),array()), 'encq').'</li>
+'.$sp.'';}).'  </ul>
+'.$sp.'</div>
+'.$sp.'' : '').'';},'flow_header_footer' => function ($cx, $in, $sp) {return 
''.$sp.'<div class="flow-board-header-footer">
 '.$sp.'  <hr />
 '.$sp.'  <p>
 '.$sp.'    '.LCRun3::ch($cx, 'html', 
array(array(((isset($in['copyrightMessage']) && is_array($in)) ? 
$in['copyrightMessage'] : null)),array()), 'encq').'
@@ -76,7 +81,7 @@
     );
     
     return '<div class="flow-board-header flow-load-interactive" 
data-flow-load-handler="loadSideRail">
-'.LCRun3::p($cx, 'flow_header_title', array(array($in),array()), '     
').''.LCRun3::p($cx, 'flow_errors', array(array($in),array()), '        
').''.((LCRun3::ifvar($cx, ((isset($in['oldSystem']) && is_array($in)) ? 
$in['oldSystem'] : null))) ? ''.LCRun3::p($cx, 'flow_header_detail_oldsystem', 
array(array($in),array()), '            ').'' : ''.LCRun3::p($cx, 
'flow_header_detail', array(array($in),array()), '            
').'').''.LCRun3::p($cx, 'flow_header_footer', array(array($in),array()), '     
').'</div>
+'.LCRun3::p($cx, 'flow_header_title', array(array($in),array()), '     
').''.LCRun3::p($cx, 'flow_errors', array(array($in),array()), '        
').''.((LCRun3::ifvar($cx, ((isset($in['oldSystem']) && is_array($in)) ? 
$in['oldSystem'] : null))) ? ''.LCRun3::p($cx, 'flow_header_detail_oldsystem', 
array(array($in),array()), '            ').'' : ''.LCRun3::p($cx, 
'flow_header_detail', array(array($in),array()), '            
').'').''.LCRun3::p($cx, 'flow_header_categories', array(array($in),array()), ' 
').''.LCRun3::p($cx, 'flow_header_footer', array(array($in),array()), ' 
').'</div>
 ';
 }
 ?>
\ No newline at end of file
diff --git a/handlebars/compiled/flow_block_topiclist.handlebars.php 
b/handlebars/compiled/flow_block_topiclist.handlebars.php
index 8f16d4f..a6ce538 100644
--- a/handlebars/compiled/flow_block_topiclist.handlebars.php
+++ b/handlebars/compiled/flow_block_topiclist.handlebars.php
@@ -302,4 +302,4 @@
 </div>
 ';
 }
-?>
+?>
\ No newline at end of file
diff --git a/handlebars/flow_block_header.handlebars 
b/handlebars/flow_block_header.handlebars
index 5dd11cb..b226890 100644
--- a/handlebars/flow_block_header.handlebars
+++ b/handlebars/flow_block_header.handlebars
@@ -6,5 +6,6 @@
        {{else}}
                {{> flow_header_detail}}
        {{/if}}
+       {{> flow_header_categories}}
        {{> flow_header_footer}}
 </div>
diff --git a/handlebars/flow_header_categories.partial.handlebars 
b/handlebars/flow_header_categories.partial.handlebars
new file mode 100644
index 0000000..0e3798f
--- /dev/null
+++ b/handlebars/flow_header_categories.partial.handlebars
@@ -0,0 +1,9 @@
+{{#if @root.categories}}
+<div class="flow-board-header-category-view-nojs">
+       <ul class="flow-board-header-category-list">
+       {{#each @root.categories}}
+               <li class="flow-board-header-category-item">{{html this}}</li>
+       {{/each}}
+       </ul>
+</div>
+{{/if}}
diff --git a/includes/View.php b/includes/View.php
index 1571904..80e7a44 100644
--- a/includes/View.php
+++ b/includes/View.php
@@ -151,10 +151,24 @@
                $workflow = $loader->getWorkflow();
                $title = $workflow->getArticleTitle();
                $user = $this->getUser();
+               $categories = array_keys( $title->getParentCategories() );
+               $categoryObject = array();
+               $linkedCategories = array();
+
+               // Transform the raw category names into links
+               foreach ( $categories as $value ) {
+                       $categoryTitle = Title::newFromText( $value );
+                       $categoryObject[ $value ] = array(
+                               'name' => $value,
+                               'exists' => $categoryTitle->exists()
+                       );
+                       $linkedCategories[] = \Linker::link( $categoryTitle, 
htmlspecialchars( $categoryTitle->getText() ) );
+               }
 
                // @todo This and API should use same code
                $apiResponse = array(
                        'title' => $title->getPrefixedText(),
+                       'categories' => $categoryObject,
                        'workflow' => $workflow->isNew() ? '' : 
$workflow->getId()->getAlphadecimal(),
                        'blocks' => array(),
                        'isWatched' => $user->isWatched( $title ),
@@ -179,6 +193,7 @@
                                                                        'title' 
=> $apiResponse['title'],
                                                                        
'block-action-template' => $block->getTemplate( $action ),
                                                                        
'editToken' => $editToken,
+                                                                       
'categories' => $linkedCategories
                                                                );
                                if ( $block->getName() == 'topiclist' ) {
                                        $topicListBlock = $block;
diff --git a/modules/flow-initialize.js b/modules/flow-initialize.js
index 4cda684..f790d16 100644
--- a/modules/flow-initialize.js
+++ b/modules/flow-initialize.js
@@ -127,9 +127,15 @@
 
                // Board description widget
                descriptionWidget = new mw.flow.ui.BoardDescriptionWidget( 
dmBoard, {
-                       $existing: $( '.flow-ui-boardDescriptionWidget-content' 
).contents()
+                       $existing: $( '.flow-ui-boardDescriptionWidget-content' 
).contents(),
+                       $categories: $( '.flow-board-header-category-view-nojs' 
).contents()
                } );
                $( '.flow-ui-boardDescriptionWidget' ).replaceWith( 
descriptionWidget.$element );
+               // The category widget is inside the board description widget.
+               // Remove it here
+               $( '.flow-board-header-category-view-nojs' ).detach();
+               // HACK: Remove the MW page categories
+               $( '.catlinks' ).detach();
 
                // HACK: These event handlers should be in the prospective 
widgets
                // they will move once we have Board UI and Topic UI widgets
diff --git a/modules/flow/dm/api/mw.flow.dm.APIHandler.js 
b/modules/flow/dm/api/mw.flow.dm.APIHandler.js
index 3ac598e..13dc453 100644
--- a/modules/flow/dm/api/mw.flow.dm.APIHandler.js
+++ b/modules/flow/dm/api/mw.flow.dm.APIHandler.js
@@ -77,6 +77,27 @@
        };
 
        /**
+        * Get page categories. This will recursively continue to fetch results
+        * until all page categories are fetched.
+        *
+        * @return {jQuery.Promise} Promise that is resolved when the API 
request
+        *  is done, with the API result.
+        */
+       mw.flow.dm.APIHandler.prototype.getCategories = function () {
+               var params = {
+                               action: 'query',
+                               titles: this.page,
+                               generator: 'categories',
+                               gcllimit: 'max'
+                       };
+
+               return ( new mw.Api() ).get( params )
+                       .then( function ( response ) {
+                               return OO.getProp( response, 'query', 'pages' );
+                       } );
+       };
+
+       /**
         * Send a request to get topic list
         *
         * @param {string} orderType Sort order type, 'newest' or 'updated'
diff --git a/modules/flow/dm/mw.flow.dm.Board.js 
b/modules/flow/dm/mw.flow.dm.Board.js
index 7784861..fec6d2c 100644
--- a/modules/flow/dm/mw.flow.dm.Board.js
+++ b/modules/flow/dm/mw.flow.dm.Board.js
@@ -23,6 +23,8 @@
                // Mixin constructor
                mw.flow.dm.List.call( this, config );
 
+               this.categories = new mw.flow.dm.Categories();
+
                // TODO: Fill this stuff in properly
                this.setId( data.id );
                this.pageTitle = data.pageTitle;
@@ -30,7 +32,13 @@
                this.sort = data.defaultSort || 'newest';
                this.description = new mw.flow.dm.BoardDescription();
 
+               // Events
                this.aggregate( { contentChange: 'topicContentChange' } );
+               this.categories.connect( this, {
+                       add: [ 'emit', 'addCategories' ],
+                       remove: [ 'emit', 'removeCategories' ],
+                       clear: [ 'emit', 'clearCategories' ]
+               } );
        };
 
        /* Initialization */
@@ -81,6 +89,65 @@
        };
 
        /**
+        * Add raw categories from the initial board api response
+        * @param {Object} categories Categories object
+        */
+       mw.flow.dm.Board.prototype.setCategoriesFromObject = function ( 
categories ) {
+               var cat,
+                       categoryDMs = [];
+
+               // Add
+               for ( cat in categories ) {
+                       categoryDMs.push( new mw.flow.dm.CategoryItem( cat, {
+                               exists: !!categories[ cat ].exists
+                       } ) );
+               }
+               this.addCategories( categoryDMs );
+       };
+
+       /**
+        * Add categories to the board
+        *
+        * @param {mw.flow.dm.CategoryItem[]} categories An array of category 
items
+        */
+       mw.flow.dm.Board.prototype.addCategories = function ( categories ) {
+               this.categories.addItems( categories );
+       };
+
+       /**
+        * Get board categories
+        *
+        * @return {mw.flow.dm.Categories} An array of category items
+        */
+       mw.flow.dm.Board.prototype.getCategories = function () {
+               return this.categories;
+       };
+
+       /**
+        * Remove board categories
+        *
+        * @param {mw.flow.dm.CategoryItem[]} categories An array of category 
items
+        */
+       mw.flow.dm.Board.prototype.removeCategories = function ( categories ) {
+               this.categories.removeItems( categories );
+       };
+
+       /**
+        * Clear the categories of this board
+        */
+       mw.flow.dm.Board.prototype.clearCategories = function () {
+               this.categories.clearItems();
+       };
+
+       /**
+        * Check whether the board has any categories
+        * @return {boolean} Board has categories
+        */
+       mw.flow.dm.Board.prototype.hasCategories = function () {
+               return !!this.categories.getItemCount();
+       };
+
+       /**
         * Check if the board is in a deleted page
         *
         * @return {boolean} Board is in a deleted page
diff --git a/modules/flow/dm/mw.flow.dm.Categories.js 
b/modules/flow/dm/mw.flow.dm.Categories.js
new file mode 100644
index 0000000..b002788
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.Categories.js
@@ -0,0 +1,26 @@
+( function ( $ ) {
+       /**
+        * Flow Board
+        *
+        * @class
+        * @mixins mw.flow.dm.List
+        * @mixins OO.EventEmitter
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        */
+       mw.flow.dm.Categories = function mwFlowDmCategories( config ) {
+               config = config || {};
+
+               // Mixin constructor
+               OO.EventEmitter.call( this, config );
+
+               // Mixin constructor
+               mw.flow.dm.List.call( this, config );
+       };
+
+       /* Initialization */
+
+       OO.mixinClass( mw.flow.dm.Categories, OO.EventEmitter );
+       OO.mixinClass( mw.flow.dm.Categories, mw.flow.dm.List );
+}( jQuery ) );
diff --git a/modules/flow/dm/mw.flow.dm.CategoryItem.js 
b/modules/flow/dm/mw.flow.dm.CategoryItem.js
new file mode 100644
index 0000000..fa4372a
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.CategoryItem.js
@@ -0,0 +1,43 @@
+( function ( $ ) {
+       /**
+        * Flow Board
+        *
+        * @class
+        * @extends mw.flow.dm.Item
+        *
+        * @constructor
+        * @param {string} name Category name
+        * @param {Object} [config] Configuration options
+        */
+       mw.flow.dm.CategoryItem = function mwFlowDmCategoryItem( name, config ) 
{
+               config = config || {};
+
+               // Parent constructor
+               mw.flow.dm.CategoryItem.parent.call( this, config );
+
+               this.setId( name );
+               this.setExists( !!config.exists );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.flow.dm.CategoryItem, mw.flow.dm.Item );
+
+       /**
+        * Set exist status for this category in this wiki.
+        *
+        * @param {boolean} exists Category page exists in this wiki
+        */
+       mw.flow.dm.CategoryItem.prototype.setExists = function ( exists ) {
+               this.categoryExists = exists;
+       };
+
+       /**
+        * Set exist status for this category in this wiki.
+        *
+        * @return {boolean} Category page exists in this wiki
+        */
+       mw.flow.dm.CategoryItem.prototype.exists = function () {
+               return this.categoryExists;
+       };
+}( jQuery ) );
diff --git a/modules/flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js 
b/modules/flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js
index 87a14aa..3b10728 100644
--- a/modules/flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js
+++ b/modules/flow/ui/widgets/mw.flow.ui.BoardDescriptionWidget.js
@@ -18,7 +18,8 @@
                // Parent constructor
                mw.flow.ui.BoardDescriptionWidget.parent.call( this, config );
 
-               this.attachModel( boardModel.getDescription() );
+               this.board = boardModel;
+               this.attachModel( this.board.getDescription() );
 
                // Since the content is already displayed, we will "steal" the 
already created
                // node to avoid having to render it twice.
@@ -34,7 +35,7 @@
                        .append( $content );
 
                this.api = new mw.flow.dm.APIHandler(
-                       boardModel.getPageTitle().getPrefixedDb(),
+                       this.board.getPageTitle().getPrefixedDb(),
                        {
                                currentRevision: this.model.getRevisionId()
                        }
@@ -66,12 +67,18 @@
                        this.button.toggle( false );
                }
 
+               this.categoriesWidget = new mw.flow.ui.CategoriesWidget( 
this.board );
+               if ( config.$categories ) {
+                       this.addCategoriesFromDom( config.$categories );
+               }
+
                // Events
                this.button.connect( this, { click: 'onEditButtonClick' } );
                this.editor.connect( this, {
                        saveContent: 'onEditorSaveContent',
                        cancel: 'onEditorCancel'
                } );
+
                // Initialize
                this.$element
                        .append(
@@ -79,7 +86,8 @@
                                this.anonWarning.$element,
                                this.button.$element,
                                this.$content,
-                               this.editor.$element
+                               this.editor.$element,
+                               this.categoriesWidget.$element
                        )
                        .addClass( 'flow-ui-boardDescriptionWidget' );
        };
@@ -112,6 +120,7 @@
                // Hide the edit button, any errors, and the content
                this.button.toggle( false );
                this.error.toggle( false );
+               this.categoriesWidget.toggle( false );
                this.$content.addClass( 'oo-ui-element-hidden' );
 
                this.editor.toggle( true );
@@ -219,9 +228,48 @@
 
                                widget.error.toggle( true );
                        } )
+                       // Get the new categories
+                       .then( this.api.getCategories.bind( this.api ) )
+                       .then( function ( catObject ) {
+                               var cat, title,
+                                       categories = {};
+
+                               for ( cat in catObject ) {
+                                       title = mw.Title.newFromText( 
catObject[ cat ].title );
+                                       categories[ title.getName() ] = { 
exists: catObject[ cat ].missing === undefined };
+                               }
+                               // Update the board data model
+                               widget.board.clearCategories();
+                               widget.board.setCategoriesFromObject( 
categories );
+                       } )
+                       // Remove the editor and show content
+                       .then( function () {
+                               widget.showContent( true );
+                       } )
+                       // Always pop pending for the editor
                        .always( function () {
                                widget.editor.popPending();
                        } );
+       };
+
+       /**
+        * Add categories from a jQuery object. This is so that we can feed 
categories from the
+        * nojs rendering of the page without having the widget to ask the API 
for the categories
+        * when it just loads.
+        *
+        * @param {jQuery} $categoriesWrapper Categories div wrapper
+        */
+       mw.flow.ui.BoardDescriptionWidget.prototype.addCategoriesFromDom = 
function ( $categoriesWrapper ) {
+               var categories = {};
+
+               $categoriesWrapper.find( '.flow-board-header-category-item a' 
).each( function () {
+                       categories[ $( this ).text() ] = {
+                               exists: !$( this ).hasClass( 'new' )
+                       };
+               } );
+
+               this.board.setCategoriesFromObject( categories );
+               this.categoriesWidget.toggle( this.board.hasCategories() );
        };
 
        /**
@@ -242,6 +290,7 @@
                // Display the edit button and the content
                this.button.toggle( true );
                this.$content.removeClass( 'oo-ui-element-hidden' );
+               this.categoriesWidget.toggle( this.board.hasCategories() );
        };
 
        /**
diff --git a/modules/flow/ui/widgets/mw.flow.ui.CategoriesWidget.js 
b/modules/flow/ui/widgets/mw.flow.ui.CategoriesWidget.js
new file mode 100644
index 0000000..ab2ea21
--- /dev/null
+++ b/modules/flow/ui/widgets/mw.flow.ui.CategoriesWidget.js
@@ -0,0 +1,100 @@
+( function ( $ ) {
+       /**
+        * Flow board categories widget
+        *
+        * @class
+        * @extends OO.ui.Widget
+        * @mixins OO.ui.mixin.GroupElement
+        *
+        * @constructor
+        * @param {mw.flow.dm.Board} model Board model
+        * @param {Object} [config]
+        *
+        */
+       mw.flow.ui.CategoriesWidget = function mwFlowUiCategoriesWidget( model, 
config ) {
+               config = config || {};
+
+               // Parent constructor
+               mw.flow.ui.CategoriesWidget.parent.call( this, config );
+
+               // Mixin constructor
+               OO.ui.mixin.GroupElement.call( this, config );
+
+               this.model = model;
+               this.model.connect( this, {
+                       addCategories: 'onModelAddCategories',
+                       removeCategories: 'onModelRemoveCategories',
+                       clearCategories: 'onModelClearCategories'
+               } );
+
+               this.categoriesLabel = new OO.ui.LabelWidget();
+               this.updateCategoriesLabel();
+
+               // Initialize
+               this.$element
+                       .append(
+                               $( '<div>' )
+                                       .addClass( 
'flow-board-header-category-title' )
+                                       .append( this.categoriesLabel.$element 
),
+                               this.$group
+                                       .addClass( 
'flow-board-header-category-list' )
+                       )
+                       .addClass( 'flow-ui-categoriesWidget 
flow-board-header-category-view' );
+       };
+
+       /* Initialization */
+
+       OO.inheritClass( mw.flow.ui.CategoriesWidget, OO.ui.Widget );
+       OO.mixinClass( mw.flow.ui.CategoriesWidget, OO.ui.mixin.GroupElement );
+
+       /**
+        * Respond to a change of categories in the board model
+        *
+        * @param {mw.flow.dm.CategoryItem[]} categories Added categories
+        */
+       mw.flow.ui.CategoriesWidget.prototype.onModelAddCategories = function ( 
categories ) {
+               var i, len,
+                       widgets = [];
+
+               for ( i = 0, len = categories.length; i < len; i++ ) {
+                       widgets.push( new mw.flow.ui.CategoryItemWidget( 
categories[ i ] ) );
+               }
+
+               this.addItems( widgets );
+               this.updateCategoriesLabel();
+       };
+
+       /**
+        * Respond to removing categories from the model
+        *
+        * @param {mw.flow.dm.CategoryItem[]} categories Removed categories
+        */
+       mw.flow.ui.CategoriesWidget.prototype.onModelRemoveCategories = 
function ( categories ) {
+               var i, len,
+                       widgets = [];
+
+               for ( i = 0, len = categories.length; i < len; i++ ) {
+                       widgets.push( this.getItemFromData( categories[ i 
].getId() ) );
+               }
+
+               this.removeItems( widgets );
+               this.updateCategoriesLabel();
+       };
+
+       /**
+        * Respond to clearing all categories from the model
+        */
+       mw.flow.ui.CategoriesWidget.prototype.onModelClearCategories = function 
() {
+               this.clearItems();
+       };
+
+       /**
+        * Update the category label according to the number of available items
+        */
+       mw.flow.ui.CategoriesWidget.prototype.updateCategoriesLabel = function 
() {
+               this.categoriesLabel.setLabel(
+                       mw.msg( 'pagecategories', this.model.getItemCount() ) +
+                       mw.msg( 'colon-separator' )
+               );
+       };
+}( jQuery ) );
diff --git a/modules/flow/ui/widgets/mw.flow.ui.CategoryItemWidget.js 
b/modules/flow/ui/widgets/mw.flow.ui.CategoryItemWidget.js
new file mode 100644
index 0000000..2ad0c2e
--- /dev/null
+++ b/modules/flow/ui/widgets/mw.flow.ui.CategoryItemWidget.js
@@ -0,0 +1,49 @@
+( function ( $ ) {
+       /**
+        * Flow board categories widget
+        *
+        * @class
+        * @extends OO.ui.Widget
+        * @mixin OO.ui.mixin.GroupElement
+        *
+        * @constructor
+        * @param {mw.flow.dm.CategoryItem} categoryModel Category item model
+        * @param {Object} [config]
+        * @cfg {boolean} [exists] Category page exists on this wiki
+        */
+       mw.flow.ui.CategoryItemWidget = function mwFlowUiCategoryItemWidget( 
categoryModel, config ) {
+               var prefixedCleanName, $link;
+
+               config = config || {};
+
+               // Parent constructor
+               mw.flow.ui.CategoryItemWidget.parent.call( this, config );
+
+               this.model = categoryModel;
+               this.name = this.model.getId();
+               this.title = mw.Title.newFromText( this.name, mw.config.get( 
'wgNamespaceIds' ).category );
+               this.exists = this.model.exists();
+               prefixedCleanName = this.title && this.title.getPrefixedText() 
|| this.name;
+
+               $link = $( '<a>' )
+                       .attr( 'href', mw.util.getUrl( this.title && 
this.title.getPrefixedDb() || this.name ) )
+                       .attr( 'title', this.exists ? prefixedCleanName : 
mw.msg( 'red-link-title', prefixedCleanName ) )
+                       .text( this.name )
+                       .toggleClass( 'new', !this.exists );
+
+               this.$element
+                       .addClass( 'flow-ui-categoryItemWidget 
flow-board-header-category-item' )
+                       .append( $link );
+       };
+
+       OO.inheritClass( mw.flow.ui.CategoryItemWidget, OO.ui.Widget );
+
+       /**
+        * Get the category data
+        *
+        * @return {string} Category name
+        */
+       mw.flow.ui.CategoryItemWidget.prototype.getData = function () {
+               return this.name;
+       };
+}( jQuery ) );
diff --git a/modules/styles/board/header.less b/modules/styles/board/header.less
index 586df78..c621d1b 100644
--- a/modules/styles/board/header.less
+++ b/modules/styles/board/header.less
@@ -48,6 +48,25 @@
        display: none;
 }
 
+.flow-board-header-category-view-nojs,
+.flow-board-header-category-view {
+       background: white;
+       padding: 0.5em;
+
+       .flow-board-header-category-list { //ul
+               list-style-type: none;
+               list-style-image: none;
+
+               .flow-board-header-category-item {
+                       display: inline-block;
+               }
+
+               .flow-board-header-category-item + 
.flow-board-header-category-item:before {
+                       content: " | ";
+               }
+       }
+}
+
 .side-rail-toggle-button {
        visibility: hidden;
        position: absolute;
diff --git a/modules/styles/flow/widgets/mw.flow.ui.CategoriesWidget.less 
b/modules/styles/flow/widgets/mw.flow.ui.CategoriesWidget.less
new file mode 100644
index 0000000..9dab103
--- /dev/null
+++ b/modules/styles/flow/widgets/mw.flow.ui.CategoriesWidget.less
@@ -0,0 +1,10 @@
+.flow-ui-categoriesWidget {
+       .flow-board-header-category-list {
+               margin: 0.3em 0 0 0.3em;
+               display: inline;
+       }
+
+       .flow-board-header-category-title {
+               display: inline;
+       }
+}
diff --git a/modules/styles/flow/widgets/mw.flow.ui.CategoryItemWidget.less 
b/modules/styles/flow/widgets/mw.flow.ui.CategoryItemWidget.less
new file mode 100644
index 0000000..00d164f
--- /dev/null
+++ b/modules/styles/flow/widgets/mw.flow.ui.CategoryItemWidget.less
@@ -0,0 +1,3 @@
+.flow-ui-categoryItemWidget {
+       padding-right: 0.2em;
+}

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ib9b766f9125e1e0c95798db46bf29ca7e0ac0408
Gerrit-PatchSet: 25
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Matthias Mullie <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
Gerrit-Reviewer: Sbisson <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to