mw.ForeignStructuredUpload: Provide category suggestions from the right wiki
authorBartosz Dziewoński <matma.rex@gmail.com>
Tue, 20 Oct 2015 22:32:19 +0000 (00:32 +0200)
committerOri.livneh <ori@wikimedia.org>
Wed, 21 Oct 2015 02:39:36 +0000 (02:39 +0000)
Also check for category existence on the right wiki, and generate
links pointing to the right wiki. Usually.

Bug: T116075
Change-Id: I85da301db4cb407b011277b0c00eb09a8bf3829f

resources/Resources.php
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js

index fee1e7c..5ed82db 100644 (file)
@@ -2031,6 +2031,8 @@ return array(
                'dependencies' => array(
                        'oojs-ui',
                        'mediawiki.api',
+                       'mediawiki.ForeignApi',
+                       'mediawiki.Title',
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
index f1c4f6f..24b0e72 100644 (file)
@@ -7,42 +7,45 @@
 ( function ( $, mw ) {
 
        /**
-        * @class mw.widgets.CategoryCapsuleItemWidget
+        * @class mw.widgets.PageExistenceCache
+        * @private
+        * @param {mw.Api} [api]
         */
-
-       var processExistenceCheckQueueDebounced,
-               api = new mw.Api(),
-               currentRequest = null,
-               existenceCache = {},
-               existenceCheckQueue = {};
-
-       // The existence checking code really could be refactored into a separate class.
+       function PageExistenceCache( api ) {
+               this.api = api || new mw.Api();
+               this.processExistenceCheckQueueDebounced = OO.ui.debounce( this.processExistenceCheckQueue );
+               this.currentRequest = null;
+               this.existenceCache = {};
+               this.existenceCheckQueue = {};
+       }
 
        /**
+        * Check for existence of pages in the queue.
+        *
         * @private
         */
-       function processExistenceCheckQueue() {
+       PageExistenceCache.prototype.processExistenceCheckQueue = function () {
                var queue, titles;
-               if ( currentRequest ) {
+               if ( this.currentRequest ) {
                        // Don't fire off a million requests at the same time
-                       currentRequest.always( function () {
-                               currentRequest = null;
-                               processExistenceCheckQueueDebounced();
-                       } );
+                       this.currentRequest.always( function () {
+                               this.currentRequest = null;
+                               this.processExistenceCheckQueueDebounced();
+                       }.bind( this ) );
                        return;
                }
-               queue = existenceCheckQueue;
-               existenceCheckQueue = {};
+               queue = this.existenceCheckQueue;
+               this.existenceCheckQueue = {};
                titles = Object.keys( queue ).filter( function ( title ) {
-                       if ( existenceCache.hasOwnProperty( title ) ) {
-                               queue[ title ].resolve( existenceCache[ title ] );
+                       if ( this.existenceCache.hasOwnProperty( title ) ) {
+                               queue[ title ].resolve( this.existenceCache[ title ] );
                        }
-                       return !existenceCache.hasOwnProperty( title );
-               } );
+                       return !this.existenceCache.hasOwnProperty( title );
+               }.bind( this ) );
                if ( !titles.length ) {
                        return;
                }
-               currentRequest = api.get( {
+               this.currentRequest = this.api.get( {
                        action: 'query',
                        prop: [ 'info' ],
                        titles: titles
                        var index, curr, title;
                        for ( index in response.query.pages ) {
                                curr = response.query.pages[ index ];
-                               title = mw.Title.newFromText( curr.title ).getPrefixedText();
-                               existenceCache[ title ] = curr.missing === undefined;
-                               queue[ title ].resolve( existenceCache[ title ] );
+                               title = new ForeignTitle( curr.title ).getPrefixedText();
+                               this.existenceCache[ title ] = curr.missing === undefined;
+                               queue[ title ].resolve( this.existenceCache[ title ] );
                        }
-               } );
-       }
-
-       processExistenceCheckQueueDebounced = OO.ui.debounce( processExistenceCheckQueue );
+               }.bind( this ) );
+       };
 
        /**
         * Register a request to check whether a page exists.
         * @param {mw.Title} title
         * @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
         */
-       function checkPageExistence( title ) {
+       PageExistenceCache.prototype.checkPageExistence = function ( title ) {
                var key = title.getPrefixedText();
-               if ( !existenceCheckQueue[ key ] ) {
-                       existenceCheckQueue[ key ] = $.Deferred();
+               if ( !this.existenceCheckQueue[ key ] ) {
+                       this.existenceCheckQueue[ key ] = $.Deferred();
                }
-               processExistenceCheckQueueDebounced();
-               return existenceCheckQueue[ key ].promise();
+               this.processExistenceCheckQueueDebounced();
+               return this.existenceCheckQueue[ key ].promise();
+       };
+
+       /**
+        * @class mw.widgets.ForeignTitle
+        * @private
+        * @extends mw.Title
+        *
+        * @constructor
+        * @inheritdoc
+        */
+       function ForeignTitle() {
+               ForeignTitle.parent.apply( this, arguments );
        }
+       OO.inheritClass( ForeignTitle, mw.Title );
+       ForeignTitle.prototype.getNamespacePrefix = function () {
+               // We only need to handle categories here...
+               return 'Category:'; // HACK
+       };
 
        /**
+        * @class mw.widgets.CategoryCapsuleItemWidget
+        *
         * Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
         * to the given page, and to show its existence status (i.e., whether it is a redlink).
         *
         * @constructor
         * @param {Object} config Configuration options
         * @cfg {mw.Title} title Page title to use (required)
+        * @cfg {string} [apiUrl] API URL, if not the current wiki's API
         */
        mw.widgets.CategoryCapsuleItemWidget = function MWWCategoryCapsuleItemWidget( config ) {
                // Parent constructor
 
                // Properties
                this.title = config.title;
+               this.apiUrl = config.apiUrl || '';
                this.$link = $( '<a>' )
                        .text( this.label )
                        .attr( 'target', '_blank' )
                this.setMissing( false );
                this.$label.replaceWith( this.$link );
                this.setLabelElement( this.$link );
-               checkPageExistence( this.title ).done( function ( exists ) {
-                       this.setMissing( !exists );
-               }.bind( this ) );
+
+               /*jshint -W024*/
+               if ( !this.constructor.static.pageExistenceCaches[ this.apiUrl ] ) {
+                       this.constructor.static.pageExistenceCaches[ this.apiUrl ] =
+                               new PageExistenceCache( new mw.ForeignApi( this.apiUrl ) );
+               }
+               this.constructor.static.pageExistenceCaches[ this.apiUrl ]
+                       .checkPageExistence( new ForeignTitle( this.title.getPrefixedText() ) )
+                       .done( function ( exists ) {
+                               this.setMissing( !exists );
+                       }.bind( this ) );
+               /*jshint +W024*/
        };
 
        /* Setup */
 
        OO.inheritClass( mw.widgets.CategoryCapsuleItemWidget, OO.ui.CapsuleItemWidget );
 
+       /* Static Properties */
+
+       /*jshint -W024*/
+       /**
+        * Map of API URLs to PageExistenceCache objects.
+        *
+        * @static
+        * @inheritable
+        * @property {Object}
+        */
+       mw.widgets.CategoryCapsuleItemWidget.static.pageExistenceCaches = {
+               '': new PageExistenceCache()
+       };
+       /*jshint +W024*/
+
        /* Methods */
 
        /**
         * @param {boolean} missing Whether the page is missing (does not exist)
         */
        mw.widgets.CategoryCapsuleItemWidget.prototype.setMissing = function ( missing ) {
+               var
+                       title = new ForeignTitle( this.title.getPrefixedText() ), // HACK
+                       prefix = this.apiUrl.replace( '/w/api.php', '' ); // HACK
+
                if ( !missing ) {
                        this.$link
-                               .attr( 'href', this.title.getUrl() )
+                               .attr( 'href', prefix + title.getUrl() )
                                .removeClass( 'new' );
                } else {
                        this.$link
-                               .attr( 'href', this.title.getUrl( { action: 'edit', redlink: 1 } ) )
+                               .attr( 'href', prefix + title.getUrl( { action: 'edit', redlink: 1 } ) )
                                .addClass( 'new' );
                }
        };
index 89fcc0b..59f1d50 100644 (file)
@@ -30,6 +30,7 @@
         *
         * @constructor
         * @param {Object} [config] Configuration options
+        * @cfg {mw.Api} [api] Instance of mw.Api (or subclass thereof) to use for queries
         * @cfg {number} [limit=10] Maximum number of results to load
         * @cfg {mw.widgets.CategorySelector.SearchType[]} [searchTypes=[mw.widgets.CategorySelector.SearchType.OpenSearch]]
         *   Default search API to use when searching.
@@ -61,7 +62,7 @@
                this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
 
                // Initialize
-               this.api = new mw.Api();
+               this.api = config.api || new mw.Api();
        }
 
        /* Setup */
         */
        CSP.createItemWidget = function ( data ) {
                return new mw.widgets.CategoryCapsuleItemWidget( {
+                       apiUrl: this.api.apiUrl || undefined,
                        title: mw.Title.newFromText( data, NS_CATEGORY )
                } );
        };
index 5a7e62e..3e573bb 100644 (file)
 
        /* Uploading */
 
+       /**
+        * @inheritdoc
+        */
+       mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
+               mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this );
+               // Point the CategorySelector to the right wiki as soon as we know what the right wiki is
+               this.upload.apiPromise.done( function ( api ) {
+                       // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
+                       if ( api.apiUrl ) {
+                               // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
+                               this.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
+                       }
+               }.bind( this ) );
+       };
+
        /**
         * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
         * with the {@link #cfg-target target} specified in config.
                        mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
                } );
                this.categoriesWidget = new mw.widgets.CategorySelector( {
+                       // Can't be done here because we don't know the target wiki yet... done in #initialize.
+                       // api: new mw.ForeignApi( ... ),
                        $overlay: this.$overlay
                } );