Mooeypoo has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/209166

Change subject: [wip^n] Create a basic data model layer for Flow JS
......................................................................

[wip^n] Create a basic data model layer for Flow JS

Create a dm for Flow; start with a flow Board and Topic models with
basic fetching from the API.

Change-Id: Iba245037078f548edd2dc10519e8f2c0c650cb00
---
M .jshintrc
M Hooks.php
M Resources.php
M modules/flow-initialize.js
A modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js
A modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js
A modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js
A modules/flow/dm/mixins/mw.flow.dm.List.js
A modules/flow/dm/mw.flow.dm.Board.js
A modules/flow/dm/mw.flow.dm.Item.js
A modules/flow/dm/mw.flow.dm.Topic.js
A modules/flow/dm/mw.flow.dm.js
A modules/flow/mw.flow.js
13 files changed, 1,164 insertions(+), 2 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow 
refs/changes/66/209166/1

diff --git a/.jshintrc b/.jshintrc
index 85b889e..dc3cc52 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -27,6 +27,7 @@
        "supernew": true,  // suppress warnings about "weird" object 
constructions
        "trailing": true,  // disallow trailing whitespace
        "undef" :   true,   // prohibits the use of undefined variables
-       "unused":   "vars"  // complain about unused variables but not arguments
+       "unused":   "vars",  // complain about unused variables but not 
arguments
+       "jquery":       true
        // "white":    true  // enforce Crockford rules
 }
diff --git a/Hooks.php b/Hooks.php
index 695c5e5..c2ac9f3 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -999,7 +999,6 @@
                                }
                        }
                }
-
                return true;
        }
 
diff --git a/Resources.php b/Resources.php
index 6b8aa95..b49423d 100644
--- a/Resources.php
+++ b/Resources.php
@@ -349,6 +349,22 @@
                        'mediawiki.Uri',
                ),
        ) + $mobile,
+       'ext.flow.dm' => $flowResourceTemplate + array(
+               'scripts' => array( // Component order is important
+                       'flow/mw.flow.js',
+                       'flow/dm/mw.flow.dm.js',
+                       'flow/dm/mw.flow.dm.Item.js',
+                       'flow/dm/mixins/mw.flow.dm.List.js',
+                       'flow/dm/api/mw.flow.dm.APIResultsProvider.js',
+                       'flow/dm/api/mw.flow.dm.APIResultsQueue.js',
+                       'flow/dm/api/mw.flow.dm.APITopicsProvider.js',
+                       'flow/dm/mw.flow.dm.Topic.js',
+                       'flow/dm/mw.flow.dm.Board.js',
+               ),
+               'dependencies' => array(
+                       'oojs'
+               )
+       ) + $mobile,
        'ext.flow' => $flowResourceTemplate + array(
                'scripts' => array( // Component order is important
                        // MW UI
@@ -385,6 +401,7 @@
                        'jquery.throttle-debounce',
                        'mediawiki.jqueryMsg',
                        'ext.flow.jquery.conditionalScroll',
+                       'ext.flow.dm',
                        'mediawiki.api',
                        'mediawiki.util',
                        'mediawiki.api.options', // required by switch-editor 
feature
diff --git a/modules/flow-initialize.js b/modules/flow-initialize.js
index 279a1e3..dafca71 100644
--- a/modules/flow-initialize.js
+++ b/modules/flow-initialize.js
@@ -10,5 +10,8 @@
         */
        $( document ).ready( function () {
                mw.flow.initComponent( $( '.flow-component' ) );
+
+               // Load data model
+               mw.flow.Initialize( $( '.flow-component' ) );
        } );
 }( jQuery ) );
diff --git a/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js 
b/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js
new file mode 100644
index 0000000..b6729e2
--- /dev/null
+++ b/modules/flow/dm/api/mw.flow.dm.APIResultsProvider.js
@@ -0,0 +1,234 @@
+/**
+ * Resource Provider object.
+ *
+ * @class
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @abstract
+ * @param {string} apiurl The URL to the api
+ * @param {Object} [config] Configuration options
+ * @cfg {number} fetchLimit The default number of results to fetch
+ * @cfg {string} lang The language of the API
+ * @cfg {number} offset Initial offset, if relevant, to call results from
+ * @cfg {Object} ajaxSettings The settings for the ajax call
+ * @cfg {Object} staticParams The data parameters that are static and should
+ *  always be sent to the API request, as opposed to user parameters.
+ * @cfg {Object} userParams Initial user parameters to be sent as data to
+ *  the API request. These can change per request, like the search query term
+ *  or sizing parameters for images, etc.
+ */
+mw.flow.dm.APIResultsProvider = function VeDmResourceProvider( apiurl, config 
) {
+       config = config || {};
+
+       this.setAPIurl( apiurl );
+       this.fetchLimit = config.fetchLimit || 30;
+       this.lang = config.lang;
+       this.offset = config.offset || 0;
+       this.ajaxSettings = config.ajaxSettings || {};
+
+       this.staticParams = config.staticParams || {};
+       this.userParams = config.userParams || {};
+
+       this.toggleDepleted( false );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+};
+
+/* Setup */
+OO.mixinClass( mw.flow.dm.APIResultsProvider, OO.EventEmitter );
+
+/* Methods */
+
+
+/**
+ * Get results from the source
+ *
+ * @param {number} howMany Number of results to ask for
+ * @return {jQuery.Promise} Promise that is resolved into an array
+ * of available results, or is rejected if no results are available.
+ */
+mw.flow.dm.APIResultsProvider.prototype.getResults = function ( howMany ) {
+       var xhr,
+               aborted = false,
+               provider = this;
+
+       return this.fetchAPIresults( howMany )
+               .then(
+                       function ( results ) {
+                               if ( !results || results.length === 0 ) {
+                                       provider.toggleDepleted( true );
+                                       return [];
+                               }
+                               return results;
+                       },
+                       // Process failed, return an empty promise
+                       function () {
+                               provider.toggleDepleted( true );
+                               return $.Deferred().resolve( [] );
+                       }
+               )
+               .promise( { abort: function () {
+                       aborted = true;
+                       if ( xhr ) {
+                               xhr.abort();
+                       }
+               } } );
+};
+
+/**
+ * Call the API for search results.
+ *
+ * @abstract
+ * @param {number} [howMany] The number of results to retrieve
+ * @return {jQuery.Promise} Promise that resolves with an array of objects 
that contain
+ *  the fetched data.
+ */
+mw.flow.dm.APIResultsProvider.prototype.fetchAPIresults = function ( howMany ) 
{
+       return $.Deferred().resolve().promise( { abort: $.noop } );
+};
+
+/**
+ * Set API url
+ *
+ * @param {string} apiurl API url
+ */
+mw.flow.dm.APIResultsProvider.prototype.setAPIurl = function ( apiurl ) {
+       this.apiurl = apiurl;
+};
+
+/**
+ * Set api url
+ *
+ * @returns {string} API url
+ */
+mw.flow.dm.APIResultsProvider.prototype.getAPIurl = function () {
+       return this.apiurl;
+};
+
+/**
+ * Get the static, non-changing data parameters sent to the API
+ *
+ * @returns {Object} Data parameters
+ */
+mw.flow.dm.APIResultsProvider.prototype.getStaticParams = function () {
+       return this.staticParams;
+};
+
+/**
+ * Get the user-inputted dybamic data parameters sent to the API
+ *
+ * @returns {Object} Data parameters
+ */
+mw.flow.dm.APIResultsProvider.prototype.getUserParams = function () {
+       return this.userParams;
+};
+
+/**
+ * Set the data parameters sent to the API
+ *
+ * @param {Object} params User defined data parameters
+ */
+mw.flow.dm.APIResultsProvider.prototype.setUserParams = function ( params ) {
+       // Assymetrically compare (params is subset of this.userParams)
+       if ( !ve.compare( params, this.userParams, true ) ) {
+               this.userParams = $.extend( {}, this.userParams, params );
+               // Reset offset
+               this.setOffset( 0 );
+               // Reset depleted status
+               this.toggleDepleted( false );
+       }
+};
+
+/**
+ * Get fetch limit or 'page' size. This is the number
+ * of results per request.
+ *
+ * @returns {number} limit
+ */
+mw.flow.dm.APIResultsProvider.prototype.getDefaultFetchLimit = function () {
+       return this.limit;
+};
+
+/**
+ * Set limit
+ *
+ * @param {number} limit Default number of results to fetch from the API
+ */
+mw.flow.dm.APIResultsProvider.prototype.setDefaultFetchLimit = function ( 
limit ) {
+       this.limit = limit;
+};
+
+/**
+ * Get provider API language
+ *
+ * @returns {string} Provider API language
+ */
+mw.flow.dm.APIResultsProvider.prototype.getLang = function () {
+       return this.lang;
+};
+
+/**
+ * Set provider API language
+ *
+ * @param {string} lang Provider API language
+ */
+mw.flow.dm.APIResultsProvider.prototype.setLang = function ( lang ) {
+       this.lang = lang;
+};
+
+/**
+ * Get result offset
+ *
+ * @returns {number} Offset Results offset for the upcoming request
+ */
+mw.flow.dm.APIResultsProvider.prototype.getOffset = function () {
+       return this.offset;
+};
+
+/**
+ * Set result offset
+ *
+ * @param {number} Results offset for the upcoming request
+ */
+mw.flow.dm.APIResultsProvider.prototype.setOffset = function ( offset ) {
+       this.offset = offset;
+};
+
+/**
+ * Check whether the provider is depleted and has no more results
+ * to hand off.
+ *
+ * @returns {boolean} The provider is depleted
+ */
+mw.flow.dm.APIResultsProvider.prototype.isDepleted = function () {
+       return this.depleted;
+};
+
+/**
+ * Toggle depleted state
+ *
+ * @param {boolean} isDepleted The provider is depleted
+ */
+mw.flow.dm.APIResultsProvider.prototype.toggleDepleted = function ( isDepleted 
) {
+       this.depleted = isDepleted !== undefined ? isDepleted : !this.depleted;
+};
+
+/**
+ * Get the default ajax settings
+ *
+ * @returns {Object} Ajax settings
+ */
+mw.flow.dm.APIResultsProvider.prototype.getAjaxSettings = function () {
+       return this.ajaxSettings;
+};
+
+/**
+ * Get the default ajax settings
+ *
+ * @param {Object} settings Ajax settings
+ */
+mw.flow.dm.APIResultsProvider.prototype.setAjaxSettings = function ( settings 
) {
+       this.ajaxSettings = settings;
+};
diff --git a/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js 
b/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js
new file mode 100644
index 0000000..a21c2b8
--- /dev/null
+++ b/modules/flow/dm/api/mw.flow.dm.APIResultsQueue.js
@@ -0,0 +1,198 @@
+/**
+ * Resource Queue object.
+ *
+ * @class
+ * @mixins OO.EventEmitter
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} limit The default number of results to fetch
+ * @cfg {number} threshold The default number of extra results
+ *  that the queue should always strive to have on top of the
+ *  individual requests for items.
+ */
+mw.flow.dm.APIResultsQueue = function VeDmResourceQueue( config ) {
+       config = config || {};
+
+       this.fileRepoPromise = null;
+       this.providers = [];
+       this.providerPromises = [];
+       this.queue = [];
+
+       this.params = {};
+
+       this.limit = config.limit || 20;
+       this.setThreshold( config.threshold || 10 );
+
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+};
+
+/* Setup */
+OO.mixinClass( mw.flow.dm.APIResultsQueue, OO.EventEmitter );
+
+/* Methods */
+
+/**
+ * Set up the queue and its resources.
+ * This should be overrided if there are any setup steps to perform.
+ *
+ * @return {jQuery.Promise} Promise that resolves when the resources
+ *  are set up. Note: The promise must have an .abort() functionality.
+ */
+mw.flow.dm.APIResultsQueue.prototype.setup = function () {
+       return $.Deferred().resolve().promise( { abort: $.noop } );
+};
+
+/**
+ * Get items from the queue
+ *
+ * @param {number} [howMany] How many items to retrieve. Defaults to the
+ *  default limit supplied on initialization.
+ * @return {jQuery.Promise} Promise that resolves into an array of items.
+ */
+mw.flow.dm.APIResultsQueue.prototype.get = function ( howMany ) {
+       var fetchingPromise = null,
+               me = this;
+
+       howMany = howMany || this.limit;
+
+       // Check if the queue has enough items
+       if ( this.queue.length < howMany + this.threshold ) {
+               // Call for more results
+               fetchingPromise = this.queryProviders( howMany + this.threshold 
)
+                       .then( function ( items ) {
+                               // Add to the queue
+                               me.queue = me.queue.concat.apply( me.queue, 
items );
+                       } );
+       }
+
+       return $.when( fetchingPromise )
+               .then( function () {
+                       return me.queue.splice( 0, howMany );
+               } );
+
+};
+
+/**
+ * Get results from all providers
+ *
+ * @param {number} [howMany] How many items to retrieve. Defaults to the
+ *  default limit supplied on initialization.
+ * @return {jQuery.Promise} Promise that is resolved into an array
+ *  of fetched items. Note: The promise must have an .abort() functionality.
+ */
+mw.flow.dm.APIResultsQueue.prototype.queryProviders = function ( howMany ) {
+       var i, len,
+               queue = this;
+
+       // Make sure there are resources set up
+       return this.setup()
+               .then( function () {
+                       // Abort previous requests
+                       for ( i = 0, len = queue.providerPromises.length; i < 
len; i++ ) {
+                               queue.providerPromises[i].abort();
+                       }
+                       queue.providerPromises = [];
+                       // Set up the query to all providers
+                       for ( i = 0, len = queue.providers.length; i < len; i++ 
) {
+                               if ( !queue.providers[i].isDepleted() ) {
+                                       queue.providerPromises.push(
+                                               queue.providers[i].getResults( 
howMany )
+                                       );
+                               }
+                       }
+
+                       return $.when.apply( $, queue.providerPromises )
+                               .then( Array.prototype.concat.bind( [] ) );
+               } );
+};
+
+/**
+ * Set the search query for all the providers.
+ *
+ * This also makes sure to abort any previous promises.
+ *
+ * @param {Object} params API search parameters
+ */
+mw.flow.dm.APIResultsQueue.prototype.setParams = function ( params ) {
+       var i, len;
+       if ( !ve.compare( params, this.params, true ) ) {
+               this.params = ve.extendObject( this.params, params );
+               // Reset queue
+               this.queue = [];
+               // Reset promises
+               for ( i = 0, len = this.providerPromises.length; i < len; i++ ) 
{
+                       this.providerPromises[i].abort();
+               }
+               // Change queries
+               for ( i = 0, len = this.providers.length; i < len; i++ ) {
+                       this.providers[i].setUserParams( this.params );
+               }
+       }
+};
+
+/**
+ * Get the data parameters sent to the API
+ *
+ * @returns {Object} params API search parameters
+ */
+mw.flow.dm.APIResultsQueue.prototype.getParams = function () {
+       return this.params;
+};
+
+/**
+ * Set the providers
+ *
+ * @param {ve.dm.APIResultsProvider[]} providers An array of providers
+ */
+mw.flow.dm.APIResultsQueue.prototype.setProviders = function ( providers ) {
+       this.providers = providers;
+};
+
+/**
+ * Add a provbider to the group
+ *
+ * @param {ve.dm.APIResultsProvider} provider A provider object
+ */
+mw.flow.dm.APIResultsQueue.prototype.addProvider = function ( provider ) {
+       this.providers.push( provider );
+};
+
+/**
+ * Set the providers
+ *
+ * @returns {ve.dm.APIResultsProvider[]} providers An array of providers
+ */
+mw.flow.dm.APIResultsQueue.prototype.getProviders = function () {
+       return this.providers;
+};
+
+/**
+ * Get the queue size
+ *
+ * @return {number} Queue size
+ */
+mw.flow.dm.APIResultsQueue.prototype.getQueueSize = function () {
+       return this.queue.length;
+};
+
+/**
+ * Set queue threshold
+ *
+ * @param {number} threshold Queue threshold, below which we will
+ *  request more items
+ */
+mw.flow.dm.APIResultsQueue.prototype.setThreshold = function ( threshold ) {
+       this.threshold = threshold;
+};
+
+/**
+ * Get queue threshold
+ *
+ * @returns {number} threshold Queue threshold, below which we will
+ *  request more items
+ */
+mw.flow.dm.APIResultsQueue.prototype.getThreshold = function () {
+       return this.threshold;
+};
diff --git a/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js 
b/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js
new file mode 100644
index 0000000..743e793
--- /dev/null
+++ b/modules/flow/dm/api/mw.flow.dm.APITopicsProvider.js
@@ -0,0 +1,85 @@
+/**
+ * Flow posts resource provider.
+ *
+ * @class
+ * @extends mw.flow.dm.APIResultsProvider
+ *
+ * @constructor
+ * @param {string} page The page associated with the topics
+ * @param {Object} [config] Configuration options
+ * @cfg {string} [scriptDirUrl] The url of the API script
+ */
+mw.flow.dm.APITopicsProvider = function MwFlowDmAPITopicsProvider( page, 
config ) {
+       config = config || {};
+
+       this.page = page;
+
+       // Parent constructor
+       mw.flow.dm.APITopicsProvider.super.call(
+               this,
+               mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + 
'/api.php',
+               $.extend( {
+                       staticParams: {
+                               action: 'flow',
+                               submodule: 'view-topiclist',
+                               page: page
+                       }
+               }, config )
+       );
+};
+
+/* Inheritance */
+OO.inheritClass( mw.flow.dm.APITopicsProvider, mw.flow.dm.APIResultsProvider );
+
+/* Methods */
+
+/**
+ * Call the API for search results.
+ *
+ * @param {number} [howMany] The number of results to retrieve
+ * @return {jQuery.Promise} Promise that resolves with an array of objects 
that contain
+ *  the fetched data.
+ */
+mw.flow.dm.APITopicsProvider.prototype.fetchAPIresults = function ( howMany ) {
+       var xhr, apiCallConfig,
+               provider = this;
+
+       howMany = howMany || this.getDefaultFetchLimit();
+
+       apiCallConfig = $.extend(
+               {},
+               this.getUserParams(),
+               {
+                       vtloffset: this.getOffset(),
+                       vtllimit: howMany
+               } );
+
+       xhr = new mw.Api().get( $.extend( this.getStaticParams(), apiCallConfig 
), this.getAjaxSettings() );
+       return xhr
+               .then( function ( data ) {
+                       var i, len, post,
+                               result = {},
+                               topiclist = OO.getProp( data.flow, 
'view-topiclist', 'result', 'topiclist' );
+
+                       if ( data.error ) {
+                               provider.toggleDepleted( true );
+                               return [];
+                       }
+
+                       // If there are no more posts, mark the board to 
prevent redundant api calls
+                       if ( Object.keys( topiclist.roots ).length < howMany ) {
+                               provider.toggleDepleted( true );
+                       } else {
+                               provider.setOffset( provider.getOffset() + 
howMany );
+                       }
+
+                       // Connect the information to the topic
+                       for ( i = 0, len = topiclist.roots.length; i < len; i++ 
) {
+                               post = topiclist.posts[ topiclist.roots[i] ];
+                               result[ topiclist.roots[i] ] = 
topiclist.revisions[ post ];
+                       }
+
+                       return result;
+               } )
+               .promise( { abort: xhr.abort } );
+};
diff --git a/modules/flow/dm/mixins/mw.flow.dm.List.js 
b/modules/flow/dm/mixins/mw.flow.dm.List.js
new file mode 100644
index 0000000..06af3f2
--- /dev/null
+++ b/modules/flow/dm/mixins/mw.flow.dm.List.js
@@ -0,0 +1,110 @@
+/**
+ * Flow List mixin
+ * Must be mixed into an mw.flow.dm.Item element
+ *
+ * @mixin
+ * @abstract
+ * @constructor
+ * @param {Object} config Configuration options
+ */
+mw.flow.dm.List = function mwFlowDmList( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       this.items = [];
+};
+
+/* Events */
+
+/**
+ * @event add Items have been added
+ * @param {flow.dm.Item[]} items Added items
+ * @param {number} index Index items were added at
+ */
+
+/**
+ * @event remove Items have been removed
+ * @param {flow.dm.Item[]} items Removed items
+ */
+
+/* Methods */
+
+/**
+ * Get all items
+ *
+ * @return {mw.flow.dm.Item[]} Items in the list
+ */
+mw.flow.dm.List.prototype.getItems = function () {
+       return this.items.slice( 0 );
+};
+
+/**
+ * Get number of items
+ *
+ * @return {number} Number of items in the list
+ */
+mw.flow.dm.List.prototype.getItemCount = function () {
+       return this.items.length;
+};
+
+/**
+ * Add items
+ *
+ * @param {mw.flow.dm.Item[]} items Items to add
+ * @param {number} index Index to add items at
+ * @chainable
+ */
+mw.flow.dm.List.prototype.addItems = function ( items, index ) {
+       var i, at, len, item, currentIndex;
+
+       // Support adding existing items at new locations
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               // Check if item exists then remove it first, effectively 
"moving" it
+               currentIndex = this.items.indexOf( item );
+               if ( currentIndex >= 0 ) {
+                       this.removeItems( [ item ] );
+                       // Adjust index to compensate for removal
+                       if ( currentIndex < index ) {
+                               index--;
+                       }
+               }
+       }
+
+       if ( index === undefined || index < 0 || index >= this.items.length ) {
+               at = this.items.length;
+               this.items.push.apply( this.items, items );
+       } else if ( index === 0 ) {
+               at = 0;
+               this.items.unshift.apply( this.items, items );
+       } else {
+               at = index;
+               this.items.splice.apply( this.items, [ index, 0 ].concat( items 
) );
+       }
+       this.emit( 'add', items, at );
+
+       return this;
+};
+
+/**
+ * Remove items
+ *
+ * @param {mw.flow.dm.Item[]} items Items to remove
+ * @chainable
+ */
+mw.flow.dm.List.prototype.removeItems = function ( items ) {
+       var i, len, item, index,
+               removed = [];
+
+       // Remove specific items        101
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               item = items[i];
+               index = this.items.indexOf( item );
+               if ( index !== -1 ) {
+                       removed = removed.concat( this.items.splice( index, 1 ) 
);
+               }
+       }
+       this.emit( 'remove', removed );
+
+       return this;
+};
diff --git a/modules/flow/dm/mw.flow.dm.Board.js 
b/modules/flow/dm/mw.flow.dm.Board.js
new file mode 100644
index 0000000..a3c5d69
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.Board.js
@@ -0,0 +1,141 @@
+/**
+ * Flow Board
+ *
+ * @constructor
+ *
+ * @extends mw.flow.dm.Item
+ * @mixins mw.flow.dm.List
+ *
+ * @param {Object} data API data to build board with
+ * @param {string} data.id Board Id
+ * @param {mw.Title} data.pageTitle Current page title
+ * @param {string} [data.highlight] An Id of a highlighted post, if exists
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [tocPostsLimit] Post count for the table of contents. This is 
also
+ *  used as the number of posts to fetch when more posts are requested for the 
table
+ *  of contents. Defaults to 10
+ * @cfg {number} [visibleItems] Number of visible posts on the page. This is 
also
+ *  used as the number of posts to fetch when more posts are requested on 
scrolling
+ *  the page. Defaults to the tocPostsLimit
+ */
+mw.flow.dm.Board = function mwFlowDmBoard( data, config ) {
+       config = config || {};
+
+       // Parent constructor
+       mw.flow.dm.Board.super.call( this, config );
+
+       // Mixing constructor
+       mw.flow.dm.List.call( this, config );
+
+       // TODO: Fill this stuff in properly
+       this.id = data.id;
+       this.pageTitle = data.pageTitle;
+       this.highlight = data.highlight || null;
+
+       // Configuration options
+       this.tocPostsLimit = config.tocPostsLimit || 10;
+       this.visibleItems = config.visibleItems || this.tocPostsLimit;
+
+       // Set up the API queue
+       this.queue = new mw.flow.dm.APIResultsQueue( {
+               limit: this.tocPostsLimit,
+               threshhold: 5
+       } );
+       this.queue.addProvider(
+               new mw.flow.dm.APITopicsProvider(
+                       this.getPageTitle().getPrefixedDb(),
+                       {
+                               fetchLimit: this.tocPostsLimit
+                       }
+               )
+       );
+};
+
+/* Initialization */
+
+OO.inheritClass( mw.flow.dm.Board, mw.flow.dm.Item );
+OO.mixinClass( mw.flow.dm.Board, mw.flow.dm.List );
+
+/**
+ * Initialize the board and fetch topics
+ *
+ * TODO: We should at some point replace this to first go over the
+ * available DOM and get the initial topics from there, saving an
+ * API call.
+ *
+ * @return {jQuery.Promise} Promise that resolves when the board finished 
initializing
+ */
+mw.flow.dm.Board.prototype.initialize = function () {
+       return this.fetchTopics();
+};
+/**
+ * Fetch topics from the API queue
+ *
+ * @return {jQuery.Promise} Promise that resolves when the topics finished
+ *  populating
+ */
+mw.flow.dm.Board.prototype.fetchTopics = function ( numVisibleTopics ) {
+       numVisibleTopics = numVisibleTopics === undefined ? this.tocPostsLimit 
: numVisibleTopics;
+
+       return this.queue.get()
+               .then( this.addTopicsFromApi.bind( this, numVisibleTopics ) );
+};
+
+/**
+ * Add topics from API response
+ *
+ * @param {number} numVisibleTopics Number of visible topics; those topics 
will be
+ *  fetched so we have all the information to display. The rest will only 
receive
+ *  basic information that we need to display in the ToC, and they will be 
considered
+ *  'stubs' until their information is fetched as well.
+ * @param {Object} result API result
+ * @return {jQuery.Promise} Promise that resolves when all visible topics have 
finished
+ * fetching the data they require to be displayed.
+ */
+mw.flow.dm.Board.prototype.addTopicsFromApi = function ( numVisibleTopics, 
result ) {
+       var topicId, topic,
+               count = 0,
+               promises = [],
+               topics = [],
+               topicList = result[ 0 ];
+
+       for ( topicId in topicList ) {
+               topic = new mw.flow.dm.Topic( topicId, topicList[topicId] );
+               if ( count < numVisibleTopics ) {
+                       // Get full information for the topic, since
+                       // it is visible
+                       promises.push( topic.fetchInformation() );
+               }
+               count++;
+               topics.push( topic );
+       }
+       // Add topics
+       this.addItems( topics );
+
+       return $.when.apply( this, promises );
+};
+
+/**
+ * Get board UUID
+ * @return {string} UUID
+ */
+mw.flow.dm.Board.prototype.getId = function () {
+       return this.id;
+};
+
+/**
+ * Get page title
+ * @return {mw.Title} Page title
+ */
+mw.flow.dm.Board.prototype.getPageTitle = function () {
+       return this.pageTitle;
+};
+
+/**
+ * Get the maximum post limit to fetch for the ToC
+ *
+ * @return {number} Fetching limit
+ */
+mw.flow.dm.Board.prototype.getToCPostLimit = function () {
+       return this.tocPostsLimit;
+};
diff --git a/modules/flow/dm/mw.flow.dm.Item.js 
b/modules/flow/dm/mw.flow.dm.Item.js
new file mode 100644
index 0000000..c6f5662
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.Item.js
@@ -0,0 +1,14 @@
+/**
+ * Flow Item
+ *
+ * @abstract
+ * @mixins OO.EventEmitter
+ */
+mw.flow.dm.Item = function mwFlowDmItem() {
+       // Mixin constructor
+       OO.EventEmitter.call( this );
+};
+
+/* Inheritance */
+
+OO.mixinClass( mw.flow.dm.Item, OO.EventEmitter );
diff --git a/modules/flow/dm/mw.flow.dm.Topic.js 
b/modules/flow/dm/mw.flow.dm.Topic.js
new file mode 100644
index 0000000..8905300
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.Topic.js
@@ -0,0 +1,308 @@
+/**
+ * Flow Topic
+ *
+ * @constructor
+ *
+ * @extends mw.flow.dm.Item
+ * @mixins mw.flow.dm.List
+ *
+ * @param {string} id Topic Id
+ * @param {Object} data API data to build topic with
+ * @param {Object} [config] Configuration options
+ */
+mw.flow.dm.Topic = function mwFlowDmTopic( id, data, config ) {
+       config = config || {};
+
+       // Parent constructor
+       mw.flow.dm.Topic.super.call( this, config );
+
+       // Mixing constructor
+       mw.flow.dm.List.call( this, config );
+
+       this.id = id;
+       this.content = data.content;
+       this.timestamp = data.timestamp;
+       this.articleTitle = data.articleTitle;
+       this.author = data.author;
+       this.creator = data.creator;
+
+       this.summary = data.summary && data.summary.revision && 
data.summary.revision.content;
+
+       this.actions = data.actions;
+
+       // I assume this is per post, and not so relevant for a topic
+       // as a whole?
+       // this.maxThreadingDepth = !!data.isMaxThreadingDepth;
+
+       this.moderated = !!data.isModerated;
+       this.moderationReason = data.moderateReason;
+       this.moderationState = data.moderateState;
+       this.moderator = data.moderator;
+
+       // What is this?
+//     this.isOriginalContent = !!data.isOriginalContent;
+
+       this.watched = !!data.isWatched;
+       this.lastUpdate = data.last_updated;
+       this.watchable = data.watchable;
+
+       // TODO: These should be added as Reply objects
+       this.replies = data.replies;
+
+
+       // Configuration
+       this.highlighted = !!config.highlighted;
+       this.stub = true;
+};
+
+/* Initialization */
+
+OO.inheritClass( mw.flow.dm.Topic, mw.flow.dm.Item );
+OO.mixinClass( mw.flow.dm.Topic, mw.flow.dm.List );
+
+/**
+ * Fetch information about this topic from the API
+ * and populate it.
+ * @return {jQuery.Promise} Promise that resolves when all the
+ *  information is ready and available
+ */
+mw.flow.dm.Topic.prototype.fetchInformation = function () {
+       this.unStub();
+       return $.Deferred().resolve();
+};
+
+/**
+ * Get topic id
+ */
+mw.flow.dm.Topic.prototype.getId = function () {
+       return this.id;
+};
+
+/**
+ * Check if a topic is a stub
+ * @return {Boolean} Topic is a stub
+ */
+mw.flow.dm.Topic.prototype.isStub = function () {
+       return this.stub;
+};
+
+/**
+ * Unstub a topic when all available information exists on it
+ */
+mw.flow.dm.Topic.prototype.unStub = function () {
+       this.stub = false;
+};
+
+/**
+ * Get topic content
+ *
+ * @return {string} Topic content
+ */
+mw.flow.dm.Topic.prototype.getContent = function () {
+       return this.content;
+};
+
+/**
+ * Get topic raw content
+ *
+ * @return {string} Topic raw content
+ */
+mw.flow.dm.Topic.prototype.getRawContent = function () {
+       return this.content.content;
+};
+
+/**
+ * Get topic timestamp
+ * @return {number} Topic timestamp
+ */
+mw.flow.dm.Topic.prototype.getTimestamp = function () {
+       return this.timestamp;
+};
+
+/**
+ * Get topic article title
+ * @return {string} Article title
+ */
+mw.flow.dm.Topic.prototype.getArticleTitle = function () {
+       return this.articleTitle;
+};
+
+/**
+ * Get topic author
+ *
+ * @return {string} Topic author
+ */
+mw.flow.dm.Topic.prototype.getAuthor = function () {
+       return this.author;
+};
+
+/**
+ * Get topic creator
+ *
+ * @return {string} Topic creator
+ */
+mw.flow.dm.Topic.prototype.getCreator = function () {
+       return this.creator;
+};
+
+/**
+ * Check if topic is moderated
+ * @return {boolean} Topic is moderated
+ */
+mw.flow.dm.Topic.prototype.isModerated = function () {
+       return this.moderated;
+};
+
+/**
+ * Toggle the moderated state of a topic
+ * @param {boolean} [moderate] Moderate the topic
+ * @fires moderated
+ */
+mw.flow.dm.Topic.prototype.toggleModerated = function ( moderate ) {
+       this.moderated = moderate || !this.moderated;
+       if ( !this.moderated ) {
+               this.setModerationReason( '' );
+       }
+       this.emit( 'moderated', this.moderated );
+};
+
+/**
+ * Get topic moderation reason
+ *
+ * @return {string} Moderation reason
+ */
+mw.flow.dm.Topic.prototype.getModerationReason = function () {
+       return this.moderationReason;
+};
+
+/**
+ * Set topic moderation reason
+ *
+ * @return {string} Moderation reason
+ */
+mw.flow.dm.Topic.prototype.setModerationReason = function ( reason ) {
+       this.moderationReason = reason;
+};
+
+/**
+ * Get topic moderation state
+ *
+ * @return {string} Moderation state
+ */
+mw.flow.dm.Topic.prototype.getModerationState = function () {
+       return this.moderationState;
+};
+
+/**
+ * Set topic moderation state
+ *
+ * @return {string} Moderation state
+ */
+mw.flow.dm.Topic.prototype.setModerationReason = function ( state ) {
+       this.moderationState = state;
+};
+
+/**
+ * Get topic moderator
+ *
+ * @return {mw.User} Moderator
+ */
+mw.flow.dm.Topic.prototype.getModerator = function () {
+       return this.moderator;
+};
+
+/**
+ * Get topic moderator
+ *
+ * @param {mw.User} mod Moderator
+ */
+mw.flow.dm.Topic.prototype.setModerator = function ( mod ) {
+       this.moderator = mod;
+};
+
+/**
+ * Check topic watched status
+ *
+ * @return {boolean} Topic is watched
+ */
+mw.flow.dm.Topic.prototype.isWatched = function () {
+       return this.watched;
+};
+
+/**
+ * Toggle the watched state of a topic
+ * @param {boolean} [watch] Watch the topic
+ * @fires watched
+ */
+mw.flow.dm.Topic.prototype.toggleWatched = function ( watch ) {
+       this.watched = watch || !this.watched;
+
+       this.emit( 'watched', this.moderated );
+};
+
+/**
+ * Check topic watchable status
+ *
+ * @return {boolean} Topic is watchable
+ */
+mw.flow.dm.Topic.prototype.isWatchable = function () {
+       return this.watchable;
+};
+
+/**
+ * Toggle the watchable state of a topic
+ * @param {boolean} [watchable] Topic is watchable
+ * @fires watchable
+ */
+mw.flow.dm.Topic.prototype.toggleWatchable = function ( watchable ) {
+       this.watchable = watchable || !this.watchable;
+
+       this.emit( 'watchable', this.watchable );
+};
+
+/**
+ * Get topic last update
+ *
+ * @return {string} Topic last update
+ */
+mw.flow.dm.Topic.prototype.getLastUpdate = function () {
+       return this.lastUpdate;
+};
+
+/**
+ * Set topic last update
+ *
+ * @param {string} lastUpdate Topic last update
+ */
+mw.flow.dm.Topic.prototype.setLastUpdate = function ( lastUpdate ) {
+       this.lastUpdate = lastUpdate;
+};
+
+/**
+ * Get the topic summary
+ *
+ * @return {string} Topic summary
+ */
+mw.flow.dm.Topic.prototype.getSummary = function () {
+       return this.summary;
+};
+
+/**
+ * Get the topic summary
+ *
+ * @param {string} Topic summary
+ * @fires summary
+ */
+mw.flow.dm.Topic.prototype.setSummary = function ( summary ) {
+       this.summary = summary;
+       this.emit( 'summary', this.summary );
+};
+
+/**
+ * Get the topic action
+ *
+ * @return {object} Topic actions
+ */
+mw.flow.dm.Topic.prototype.getActions = function () {
+       return this.actions;
+};
diff --git a/modules/flow/dm/mw.flow.dm.js b/modules/flow/dm/mw.flow.dm.js
new file mode 100644
index 0000000..c482a37
--- /dev/null
+++ b/modules/flow/dm/mw.flow.dm.js
@@ -0,0 +1,2 @@
+mw.flow = mw.flow || {};
+mw.flow.dm = {};
diff --git a/modules/flow/mw.flow.js b/modules/flow/mw.flow.js
new file mode 100644
index 0000000..b3e3996
--- /dev/null
+++ b/modules/flow/mw.flow.js
@@ -0,0 +1,50 @@
+mw.flow = mw.flow || {};
+
+mw.flow.totalInstanceCount = 0;
+/**
+ * Initialize Flow components into the data model
+ *
+ * @param {jQuery} [$container] Flow board container.
+ */
+mw.flow.Initialize = function mwFlowInitialize( $container ) {
+       var board, boardId,
+//             uri = new mw.Uri( window.location.href ),
+               uid = String( window.location.hash.match( /[0-9a-z]{16,19}$/i ) 
|| '' );
+
+       $container = $container || $( '.flow-component' );
+
+       boardId = $container.data( 'flowId' );
+       if ( !boardId ) {
+               // We probably don't need this for a Flow Board, as there is
+               // only one and it always exists where it should, even when it's
+               // empty.
+
+               // Generate an ID for this component
+               boardId = 'flow-generated-' + mw.flow.totalInstanceCount++;
+               $container.data( 'flowId', boardId );
+       }
+
+       // TODO: Migrate the logic (this should be in the ui widget
+       // but the logic of whether we need to see the newer posts
+       // or only the highlighted post should probably be here)
+
+       // if ( uri.query.fromnotif ) {
+       //      _flowHighlightPost( $container, uid, 'newer' );
+       // } else {
+       //      _flowHighlightPost( $container, uid );
+       // }
+       board = new mw.flow.dm.Board( {
+               id: boardId,
+               pageTitle: mw.Title.newFromText( mw.config.get( 'wgPageName' ) 
) || '',
+               tocPostsLimit: 20,
+               visibleItems: 10,
+               // Handle URL parameters
+               highlight: uid || null
+       } );
+       // Initialize
+       board.initialize()
+               .then( function () {
+                       // Debugging
+                       console.log( board.getItems() );
+               } );
+};

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Iba245037078f548edd2dc10519e8f2c0c650cb00
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <mor...@gmail.com>

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

Reply via email to