jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/340961 )

Change subject: Add mw.cx.TargetArticle class
......................................................................


Add mw.cx.TargetArticle class

MediaWiki specific validations, publishing, success and error
handlers. Supposed to replace ext.cx.publish module in old CX

The success/error message system is not ready so those can't be
tested now - to be done in follow up.

Captcha handler and title conflict errors need to present a dialog
to user to resolve it - to be done in followup.

Change-Id: I5eab6fec79895591ea0b404196fb30f17452f75a
---
M extension.json
M modules/dm/mw.cx.dm.TargetPage.js
M modules/dm/mw.cx.dm.Translation.js
M modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
A modules/mw.cx.TargetArticle.js
M modules/ui/mw.cx.ui.TranslationView.js
6 files changed, 612 insertions(+), 14 deletions(-)

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



diff --git a/extension.json b/extension.json
index 4bd5fbc..4260df6 100644
--- a/extension.json
+++ b/extension.json
@@ -1360,7 +1360,19 @@
                        ],
                        "dependencies": [
                                "mw.cx.dm.Translation",
-                               "mw.cx.ui.TranslationView"
+                               "mw.cx.ui.TranslationView",
+                               "mw.cx.TargetArticle"
+                       ]
+               },
+               "mw.cx.TargetArticle": {
+                       "scripts": [
+                               "mw.cx.TargetArticle.js"
+                       ],
+                       "dependencies": [
+                               "easy-deflate.deflate",
+                               "mw.cx.dm.Translation",
+                               "mw.cx.ui.TranslationView",
+                               "mediawiki.api.edit"
                        ]
                },
                "mw.cx.MwApiRequestManager": {
diff --git a/modules/dm/mw.cx.dm.TargetPage.js 
b/modules/dm/mw.cx.dm.TargetPage.js
index f8e7a28..aa45b24 100644
--- a/modules/dm/mw.cx.dm.TargetPage.js
+++ b/modules/dm/mw.cx.dm.TargetPage.js
@@ -28,11 +28,32 @@
 
 /**
  * Build the target document for publishing
+ * @param {mw.cx.dm.TranslationUnit[]} translationUnits
  */
-mw.cx.dm.TargetPage.prototype.build = function () {
+mw.cx.dm.TargetPage.prototype.build = function ( translationUnits ) {
+       var i, targetDoc,
+               wrapper = document.createElement( 'div' );
+
+       for ( i = 0; i < translationUnits.length; i++ ) {
+               targetDoc = translationUnits[ i ].getTargetDocument();
+               if ( !targetDoc ) {
+                       continue;
+               }
+               wrapper.appendChild( targetDoc.cloneNode( true ) );
+       }
+
+       this.targetPageDocument = wrapper;
 };
 
-mw.cx.dm.TargetPage.prototype.publish = function () {};
+/**
+ * Get the HTML content for publishing
+ * @param {mw.cx.dm.Translation} translation
+ * @return {string}
+ */
+mw.cx.dm.TargetPage.prototype.getContent = function ( translation ) {
+       this.build( translation.getTranslationUnits() );
+       return this.targetPageDocument.innerHTML;
+};
 
 /**
  * Get categories for the target article.
diff --git a/modules/dm/mw.cx.dm.Translation.js 
b/modules/dm/mw.cx.dm.Translation.js
index 49cf76f..ffc65a0 100644
--- a/modules/dm/mw.cx.dm.Translation.js
+++ b/modules/dm/mw.cx.dm.Translation.js
@@ -60,6 +60,10 @@
        return this.translationUnits;
 };
 
+mw.cx.dm.Translation.prototype.getTargetPage = function() {
+       return this.targetPage;
+};
+
 /**
  * Get Translation id
  *
diff --git a/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js 
b/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
index 55ceec2..7d5fe3d 100644
--- a/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
@@ -92,6 +92,10 @@
        return this.translationUnits;
 };
 
+mw.cx.dm.TranslationUnit.prototype.getTargetDocument = function () {
+       return this.targetDocument;
+};
+
 mw.cx.dm.TranslationUnit.prototype.getParentTranslationUnit = function () {
        return this.parentTranslationUnit;
 };
diff --git a/modules/mw.cx.TargetArticle.js b/modules/mw.cx.TargetArticle.js
new file mode 100644
index 0000000..04fd8a0
--- /dev/null
+++ b/modules/mw.cx.TargetArticle.js
@@ -0,0 +1,561 @@
+/**
+ * Target Article for CX - Validation, Publishing, Success and Error handling.
+ * @param {mw.cx.dm.Translation} translation
+ * @param {mw.cx.ui.TranslationView} translationView
+ * @param {object} config Translation configuration
+ */
+mw.cx.TargetArticle = function MWCXTargetArticle( translation, 
translationView, config ) {
+       this.translation = translation;
+       this.view = translationView;
+       this.config = config;
+       this.siteMapper = config.siteMapper;
+       this.sourceTitle = config.sourceTitle;
+       this.targetTitle = config.targetTitle;
+       this.sourceLanguage = config.sourceLanguage;
+       this.targetLanguage = config.targetLanguage;
+       this.sourceRevision = config.sourceRevision;
+       // Mixin constructors
+       OO.EventEmitter.call( this );
+       // Properties
+       this.publishDeferred = null;
+       this.captcha = null;
+};
+
+/* Inheritance */
+
+OO.mixinClass( mw.cx.TargetArticle, OO.EventEmitter );
+
+/**
+ * Publish the translated content to target wiki.
+ * @return {jQuery.Promise}
+ */
+mw.cx.TargetArticle.prototype.publish = function () {
+       var apiParams;
+
+       apiParams = $.extend( {}, this.captcha || {}, {
+               assert: 'user',
+               action: 'cxpublish',
+               from: this.sourceLanguage,
+               to: this.targetLanguage,
+               sourcetitle: this.sourceTitle,
+               html: this.getContent( true ),
+               categories: this.getCategories()
+       } );
+
+       this.publishDeferred = $.Deferred();
+       // Check for title conflicts
+       this.checkTargetTitle( this.targetTitle ).then( function ( title ) {
+               apiParams.title = title;
+               // Post the content to publish.
+               return mw.Api().postWithToken( 'csrf', apiParams, {
+                       // A bigger timeout since publishing after converting 
html to wikitext
+                       // parsoid is not a fast operation.
+                       timeout: 100 * 1000 // in milliseconds
+               } ).then( this.publishSuccess.bind( this ) )
+                       // Failure handler
+               .fail( this.publishFail.bind( this ) );
+       }.bind( this ) );
+
+       return this.publishDeferred.promise();
+};
+
+/**
+ * Publish success handler
+ * @param  {Object} response Response object from the publishing api
+ * @return {null|jQuery.Promise}
+ */
+mw.cx.TargetArticle.prototype.publishSuccess = function ( response ) {
+       var publishResult = response.cxpublish;
+       if ( publishResult.result === 'success' ) {
+               return this.publishComplete();
+       }
+
+       if ( publishResult.edit.captcha ) {
+               // If there is a captcha challenge, get the solution and retry.
+               return this.showErrorCaptcha( publishResult.edit.captcha )
+                       .then( this.publish.bind( this ) );
+       }
+
+       // Any other failure
+       return this.publishFail( publishResult );
+};
+
+/**
+ * @fires publish
+ */
+mw.cx.TargetArticle.prototype.publishComplete = function () {
+       this.publishDeferred.resolve( true );
+       this.emit( 'publish' );
+       // TODO: Event logging the publishing success
+};
+
+/**
+ * Publish failure handler
+ * @param {Object} jqXHR
+ * @param {string} status Text status message
+ * @param {Object|null} data API response data
+ */
+mw.cx.TargetArticle.prototype.publishFail = function ( jqXHR, status, data ) {
+       var editError, editResult;
+
+       // Handle empty response
+       if ( !data ) {
+               this.showErrorEmpty();
+               return;
+       }
+
+       if ( data.exception instanceof Error ) {
+               data.exception = data.exception.toString();
+       }
+
+       editResult = data.edit;
+       if ( editResult ) {
+               // Handle spam blacklist error (either from core or from 
Extension:SpamBlacklist)
+               // 
{"result":"error","edit":{"spamblacklist":"facebook.com","result":"Failure"}}
+               if ( editResult.spamblacklist ) {
+                       this.showErrorSpamBlacklist( data.edit );
+                       return;
+               }
+               // Handle abusefilter errors.
+               if ( editResult.code && editResult.code.indexOf( 'abusefilter' 
) === 0 ) {
+                       this.showErrorAbuseFilter( editResult.info, 
editResult.warning );
+               }
+       }
+
+       // Handle token errors
+       editError = data.error;
+       if ( editError && editError.code === 'badtoken' || editError.code === 
'assertuserfailed' ) {
+               this.showErrorBadToken( null, true );
+               return;
+       } else if ( data.error && data.error.code === 
'titleblacklist-forbidden' ) {
+               this.showErrorTitleBlacklist();
+               return;
+       } else if ( data.error && data.error.code === 'readonly' ) {
+               this.showErrorReadOnly( data.error.readonlyreason );
+               return;
+       }
+
+       // Handle captcha
+       // Captcha "errors" usually aren't errors. We simply don't know about 
them ahead of time,
+       // so we save once, then (if required) we get an error with a captcha 
back and try again after
+       // the user solved the captcha.
+       // TODO: ConfirmEdit API is horrible, there is no reliable way to know 
whether it is a "math",
+       // "question" or "fancy" type of captcha. They all expose differently 
named properties in the
+       // API for different things in the UI. At this point we only support 
the SimpleCaptcha and FancyCaptcha
+       // which we very intuitively detect by the presence of a "url" property.
+       if ( editResult && editResult.captcha && (
+               editResult.captcha.url ||
+               editResult.captcha.type === 'simple' ||
+               editResult.captcha.type === 'math' ||
+               editResult.captcha.type === 'question'
+       ) ) {
+               this.showErrorCaptcha( editResult );
+               return;
+       }
+
+       // Handle (other) unknown and/or unrecoverable errors
+       this.showErrorUnknown( editResult, data );
+       // TODO: Event logging the publishing error
+};
+
+/**
+ * AbuseFilter error handler
+ * @method
+ * @fires publishErrorAbuseFilter
+ */
+mw.cx.TargetArticle.prototype.showErrorAbuseFilter = function () {
+       this.showPublishError( mw.msg( 'cx-publishError-abusefilter' ), true );
+       // Don't disable the publish button. If the action is not disallowed 
the user may save the
+       // edit by pressing Publish again. The AbuseFilter API currently has no 
way to distinguish
+       // between filter triggers that are and aren't disallowing the action.
+       this.emit( 'publishErrorAbuseFilter' );
+};
+
+/**
+ * Handle publish error with empty API response
+ * @method
+ * @fires publishErrorEmpty
+ */
+mw.cx.TargetArticle.prototype.showErrorEmpty = function () {
+       this.showSaveError( mw.msg( 'cx-publisherror-empty', 'Empty server 
response' ), false /* prevents reapply */ );
+       this.emit( 'publishErrorEmpty' );
+};
+
+/**
+ * Handle title blacklist save error
+ *
+ * @method
+ * @fires publishErrorTitleBlacklist
+ */
+mw.cx.TargetArticle.prototype.showErrorTitleBlacklist = function () {
+       this.showPublishError( mw.msg( 'cx-publishError-titleblacklist' ) );
+       this.emit( 'publishErrorTitleBlacklist' );
+};
+
+/**
+ * Handle captcha challenge error
+ *
+ * @method
+ * @param {Object} apiResult publishing api result
+ * @fires publishErrorTitleBlacklist
+ */
+mw.cx.TargetArticle.prototype.showErrorCaptcha = function ( apiResult ) {
+       var $captchaDiv = $( '<div>' ),
+               $captchaParagraph = $( '<p>' );
+
+       // TODO Not tested
+       this.captcha = {
+               input: new OO.ui.TextInputWidget(),
+               id: apiResult.captcha.id
+       };
+       $captchaDiv.append( $captchaParagraph );
+       $captchaParagraph.append(
+               $( '<strong>' ).text( mw.msg( 'captcha-label' ) ),
+               document.createTextNode( mw.msg( 'colon-separator' ) )
+       );
+       if ( apiResult.captcha.url ) {
+               // FancyCaptcha
+               // Based on FancyCaptcha::getFormInformation() 
(https://git.io/v6mml) and
+               // ext.confirmEdit.fancyCaptcha.js in the ConfirmEdit extension.
+               mw.loader.load( 'ext.confirmEdit.fancyCaptcha' );
+               $captchaDiv.addClass( 'fancycaptcha-captcha-container' );
+               $captchaParagraph.append(
+                       $( $.parseHTML( mw.message( 'fancycaptcha-edit' 
).parse() ) )
+                               .filter( 'a' ).attr( 'target', '_blank' ).end()
+               );
+               $captchaDiv.append(
+                       $( '<img>' ).attr( 'src', apiResult.captcha.url 
).addClass( 'fancycaptcha-image' ),
+                       ' ',
+                       $( '<a>' ).addClass( 'fancycaptcha-reload' ).text( 
mw.msg( 'fancycaptcha-reload-text' ) )
+               );
+       } else if ( apiResult.captcha.type === 'simple' || 
apiResult.captcha.type === 'math' ) {
+               // SimpleCaptcha and MathCaptcha
+               $captchaParagraph.append(
+                       mw.message( 'captcha-edit' ).parse(),
+                       '<br>',
+                       document.createTextNode( apiResult.captcha.question )
+               );
+       } else if ( apiResult.captcha.type === 'question' ) {
+               // QuestyCaptcha
+               $captchaParagraph.append(
+                       mw.message( 'questycaptcha-edit' ).parse(),
+                       '<br>',
+                       apiResult.captcha.question
+               );
+       }
+
+       $captchaDiv.append( this.captcha.input.$element );
+
+       // ProcessDialog's error system isn't great for this yet.
+       this.publishDialog.clearMessage( 'api-save-error' );
+       this.publishDialog.showMessage( 'api-save-error', $captchaDiv );
+       this.publishDialog.popPending();
+
+       this.publishDialog.updateSize();
+
+       this.captcha.input.focus();
+
+       this.emit( 'publishErrorCaptcha' );
+};
+
+/**
+ * Handle token fetch errors.
+ *
+ * @method
+ * @param {boolean} [error=false] Whether there was an error trying to figure 
out who we're logged in as
+ * @fires publishErrorBadToken
+ */
+mw.cx.TargetArticle.prototype.showErrorBadToken = function ( error ) {
+       var $msg = $( document.createTextNode( mw.msg( 
'cx-publisherror-badtoken' ) + ' ' ) );
+
+       if ( error ) {
+               this.emit( 'publishErrorBadToken', false );
+               $msg = $msg.add( document.createTextNode( mw.msg( 
'cx-publisherror-identify-trylogin' ) ) );
+       }
+       this.showSaveError( $msg );
+       this.emit( 'publishErrorBadToken', error );
+};
+
+/**
+ * Handle unknown publish error
+ *
+ * @method
+ * @param {Object} editResult
+ * @param {Object|null} data API response data
+ * @fires saveErrorUnknown
+ */
+mw.cx.TargetArticle.prototype.saveErrorUnknown = function ( editResult, data ) 
{
+       var errorMsg = ( editResult && editResult.info ) || ( data && 
data.error && data.error.info ),
+               errorCode = ( editResult && editResult.code ) || ( data && 
data.error && data.error.code ),
+               unknown = 'Unknown error';
+
+       if ( data.xhr.status !== 200 ) {
+               unknown += ', HTTP status ' + data.xhr.status;
+       }
+
+       this.showPublishError(
+               $( document.createTextNode( errorMsg || errorCode || unknown ) 
),
+               false // prevents reapply
+       );
+       this.emit( 'publishErrorUnknown', errorCode || errorMsg || unknown );
+};
+
+/**
+ * Show an publish process error message
+ *
+ * @method
+ * @param {string|jQuery|Node[]} msg Message content (string of HTML, jQuery 
object or array of
+ *  Node objects)
+ * @param {boolean} [allowReapply=true] Whether or not to allow the user to 
reapply.
+ *  Reset when swapping panels. Assumed to be true unless explicitly set to 
false.
+ * @param {boolean} [warning=false] Whether or not this is a warning.
+ */
+mw.cx.TargetArticle.prototype.showPublishError = function ( msg, allowReapply, 
warning ) {
+       this.publishDeferred.reject(
+               [ new OO.ui.Error( msg, { recoverable: allowReapply, warning: 
warning } ) ]
+       );
+};
+
+/**
+ * Get content for publishing
+ *
+ * @param {boolean} deflate Whether the content need to deflated
+ * @return {string} Content for publishing, may be deflated
+ */
+mw.cx.TargetArticle.prototype.getContent = function ( deflate ) {
+       var content;
+       content = this.translation.getTargetPage().getContent( this.translation 
);
+       if ( deflate ) {
+               content = EasyDeflate.deflate( content );
+       }
+       return content;
+};
+
+/**
+ * Increase the version number of a title starting with 1.
+ *
+ * @param {string} title The title to increase the version on.
+ * @return {string}
+ */
+mw.cx.TargetArticle.prototype.increaseVersion = function( title ) {
+       var match, version;
+
+       match = title.match( /^.*\((\d+)\)$/ );
+       if ( match ) {
+               version = parseInt( match[ 1 ], 10 ) + 1;
+
+               return title.replace( /\(\d+\)$/, '(' + version + ')' );
+       }
+
+       return title + ' (1)';
+};
+
+/**
+ * Generate an alternate title in case of title collision.
+ *
+ * @param {string} title The title
+ * @return {string}
+ */
+mw.cx.TargetArticle.prototype.getAlternateTitle = function ( title ) {
+       var username, mwTitle;
+
+       username = mw.user.getName();
+       mwTitle = mw.Title.newFromText( title );
+
+       if ( mwTitle && mwTitle.getNamespaceId() === 2 ) {
+               return this.increaseVersion( title );
+       } else {
+               return 'User:' + username + '/' + title;
+       }
+};
+
+/**
+ * Get categories for the target article
+ *
+ * @return {string[]} Category titles
+ */
+mw.cx.TargetArticle.prototype.getCategories = function () {
+       return this.translation.getTargetPage().getCategories();
+};
+
+/**
+ * Checks to see if there is already a published article with the title.
+ * If exists ask the translator a resolution for the conflict.
+ *
+ * @param {string} title The title to check
+ * @return {jQuery.Promise}
+ */
+mw.cx.TargetArticle.prototype.checkTargetTitle = function ( title ) {
+       return this.isTitleExistInLanguage( this.targetLanguage, title ).then( 
function ( titleExists ) {
+               var $dialog;
+
+               if ( !titleExists ) {
+                       return title;
+               }
+
+               // Show a dialog to decide what to do now
+               this.view.publishButton.$element.cxPublishingDialog();
+               $dialog = this.translationView.publishButton.$element.data( 
'cxPublishingDialog' );
+
+               return $dialog.listen().then( function ( overwrite ) {
+                       if ( overwrite ) {
+                               return title;
+                       }
+
+                       return this.getAlternateTitle( title );
+               }.bind( this ) );
+       }.bind( this ) );
+};
+
+/**
+ * Link the source and target articles in the Wikibase repo
+ * @param {string} sourceLanguage
+ * @param {string} targetLanguage
+ * @param {string} sourceTitle
+ * @param {string} targetTitle
+ * @return {jQuery.Promise}
+ */
+mw.cx.TargetArticle.prototype.linkAtWikibase = function ( sourceLanguage, 
targetLanguage, sourceTitle, targetTitle ) {
+       var title, sourceApi;
+
+       // Link only pages in the main space
+       title = new mw.Title( targetTitle );
+       if ( title.getNamespaceId() !== 0 ) {
+               return;
+       }
+
+       sourceApi = this.siteMapper.getApi( this.sourceLanguage );
+
+       // TODO: Use action=query&meta=wikibase API
+       // that expose siteid as per
+       // https://gerrit.wikimedia.org/r/#/c/214517/
+       return sourceApi.get( {
+               action: 'query',
+               meta: 'siteinfo',
+               siprop: 'general'
+       } ).then( function ( result ) {
+               /* global wikibase  */
+               var repoApi, targetWikiId, sourceWikiId, pageConnector;
+
+               repoApi = new wikibase.api.RepoApi( 
wikibase.client.getMwApiForRepo() );
+               targetWikiId = mw.config.get( 'wbCurrentSite' ).globalSiteId;
+               sourceWikiId = result.query.general.wikiid;
+
+               pageConnector = new wikibase.PageConnector(
+                       repoApi,
+                       targetWikiId,
+                       targetTitle,
+                       sourceWikiId,
+                       sourceTitle
+               );
+
+               return pageConnector.linkPages().then( function () {
+                       var api = new mw.Api();
+
+                       // Purge the newly-created page after adding the link,
+                       // so that they will appear as soon as possible without 
manual purging
+                       api.post( {
+                               action: 'purge',
+                               titles: targetTitle
+                       } );
+               } );
+       } );
+};
+
+/**
+ * Checks to see if a title exists in the specified language wiki. Returns
+ * the normalised title and resolves redirects.
+ *
+ * @param {string} language The language of the wiki to check
+ * @param {string} title The title to look for
+ * @return {jQuery.promise}
+ * @return {Function} return.done If title exists
+ * @return {string|false} return.done.title
+ */
+mw.cx.TargetArticle.prototype.isTitleExistInLanguage = function ( language, 
title ) {
+       var api = this.siteMapper.getApi( language );
+
+       // Short circuit empty titles
+       if ( title === '' ) {
+               return $.Deferred().resolve( false ).promise();
+       }
+
+       // Reject titles with pipe in the name, as it has special meaning in 
the api
+       if ( /\|/.test( title ) ) {
+               return $.Deferred().resolve( false ).promise();
+       }
+
+       return api.get( {
+               formatversion: 2,
+               action: 'query',
+               titles: title,
+               redirects: 1
+       } ).then( function ( response ) {
+               var page = response.query.pages[ 0 ];
+
+               if ( page.missing || page.invalid ) {
+                       return false;
+               }
+
+               return page.title;
+       } );
+};
+
+/**
+ * Checks for an equivalent page in the target wiki based on source title.
+ *
+ * @param {string} sourceLanguage the source language
+ * @param {string} targetLanguage the target language
+ * @param {string} sourceTitle the title to check
+ * @return {jQuery.promise}
+ */
+mw.cx.TargetArticle.prototype.isTitleConnectedInLanguages = function (
+       sourceLanguage,
+       targetLanguage,
+       sourceTitle
+) {
+       var api = this.siteMapper.getApi( sourceLanguage );
+
+       return api.get( {
+               action: 'query',
+               prop: 'langlinks',
+               titles: sourceTitle,
+               lllang: this.siteMapper.getWikiDomainCode( targetLanguage ),
+               lllimit: 1,
+               redirects: true
+       } ).then( function ( response ) {
+               var equivalentTargetPage = false;
+
+               if ( response.query && response.query.pages ) {
+                       $.each( response.query.pages, function ( pageId, page ) 
{
+                               if ( page.langlinks ) {
+                                       equivalentTargetPage = page.langlinks[ 
0 ][ '*' ];
+                               }
+                       } );
+               }
+
+               return equivalentTargetPage;
+       } );
+};
+
+/**
+ * Check whether a page with the same title already exists
+ * and show a warning if needed.
+ */
+mw.cx.TargetArticle.prototype.validateTargetTitle = function () {
+       var viewTargetUrl = this.siteMapper.getPageUrl( this.targetLanguage, 
this.targetTitle );
+
+       this.isTitleExistInLanguage( this.targetLanguage, this.targetTitle )
+               .then( function ( pageExist ) {
+                       // If page doesn't exist, it's OK
+                       if ( !pageExist ) {
+                               return;
+                       }
+
+                       mw.hook( 'mw.cx.warning' ).fire( mw.message(
+                               'cx-translation-target-page-exists',
+                               viewTargetUrl,
+                               this.targetTitle
+                       ) );
+               } );
+};
diff --git a/modules/ui/mw.cx.ui.TranslationView.js 
b/modules/ui/mw.cx.ui.TranslationView.js
index c85cf06..c73177d 100644
--- a/modules/ui/mw.cx.ui.TranslationView.js
+++ b/modules/ui/mw.cx.ui.TranslationView.js
@@ -18,6 +18,7 @@
        // Parent constructor
        mw.cx.ui.TranslationView.parent.call( this, this.config );
        this.translation = null;
+       this.targetArticle = null;
        this.publishButton = null;
        this.init();
        this.listen();
@@ -119,7 +120,7 @@
                label: mw.msg( 'cx-publish-button' )
        } );
        this.publishButton.connect( this, {
-               click: 'onPublishButtonClick'
+               click: 'publish'
        } );
        mw.hook( 'mw.cx.progress' ).add( function ( weights ) {
                self.publishButton.setDisabled( weights.any === 0 );
@@ -140,22 +141,17 @@
        } ).$element );
 };
 
-mw.cx.ui.TranslationView.prototype.onPublishButtonClick = function () {
-       this.publish();
-};
-
 /**
  * Publish the translation
  */
 mw.cx.ui.TranslationView.prototype.publish = function () {
-       var publisher, self = this;
-
        // Disable the trigger button
        this.publishButton.setDisabled( true ).setLabel( mw.msg( 
'cx-publish-button-publishing' ) );
-       publisher = new mw.cx.Publish( this.publishButton, 
this.config.siteMapper );
-       publisher.publish().always( function () {
-               self.publishButton.setDisabled( true ).setLabel( mw.msg( 
'cx-publish-button' ) );
-       } );
+       this.targetArticle = this.targetArticle ||
+               new mw.cx.TargetArticle( this.translation, this, this.config );
+       this.targetArticle.publish().always( function () {
+               this.publishButton.setDisabled( true ).setLabel( mw.msg( 
'cx-publish-button' ) );
+       }.bind( this ) );
 };
 
 /**

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I5eab6fec79895591ea0b404196fb30f17452f75a
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <santhosh.thottin...@gmail.com>
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