Move reply widget controlling logic to CommentController

Some of what happens when you click reply happens in
CommentController (in #save) and some error handling
happens in ReplyWidget (in #onReplyClick).

Move most of the logic to CommenController.

Change-Id: Ib6208c0b8d2ddbbcf08adfcca7875ab8b026f598
This commit is contained in:
Ed Sanders 2024-05-02 15:05:45 +01:00 committed by David Lynch
parent 2578d00536
commit 6a17f6121c
3 changed files with 144 additions and 89 deletions

View file

@ -335,6 +335,7 @@ CommentController.prototype.setupReplyWidget = function ( replyWidget, data, sup
replyWidget.setup( data, suppressNotifications );
replyWidget.updateNewCommentsWarning( this.newComments );
replyWidget.updateParentRemovedError( this.parentRemoved );
replyWidget.connect( this, { submit: 'onReplySubmit' } );
this.replyWidget = replyWidget;
};
@ -463,6 +464,81 @@ CommentController.prototype.getApiQuery = function ( pageName, checkboxes ) {
return data;
};
/**
* Handle the reply widget being submitted
*/
CommentController.prototype.onReplySubmit = function () {
if ( !this.replyWidget ) {
return;
}
this.replyWidget.clearSaveErrorMessage();
this.saveInitiated = mw.now();
this.replyWidget.setPending( true );
mw.track( 'editAttemptStep', { action: 'saveIntent' } );
mw.track( 'editAttemptStep', { action: 'saveAttempt' } );
// TODO: When editing a transcluded page, VE API returning the page HTML is a waste, since we won't use it
this.save( this.replyWidget.pageName )
.then( null, this.saveFail.bind( this ) )
.always( () => {
this.replyWidget.setPending( false );
} );
};
/**
* Handle save failures from the API
*
* @param {string} code Error code
* @param {Object} data Error data
*/
CommentController.prototype.saveFail = function ( code, data ) {
this.replyWidget.clearCaptcha();
const captchaData = OO.getProp( data, 'discussiontoolsedit', 'edit', 'captcha' );
if ( captchaData ) {
code = 'captcha';
this.replyWidget.setCaptcha( captchaData );
} else {
this.setSaveErrorMessage( code, data );
}
if ( code instanceof Error ) {
code = 'exception';
}
// Log more precise error codes, mw.Api just gives us 'http' in all of these cases
if ( data ) {
if ( data.textStatus === 'timeout' || data.textStatus === 'abort' || data.textStatus === 'parsererror' ) {
code = data.textStatus;
} else if ( data.xhr ) {
code = 'http-' + ( data.xhr.status || 0 );
}
}
// Compare to ve.init.mw.ArticleTargetEvents.js in VisualEditor.
const typeMap = {
badtoken: 'userBadToken',
assertanonfailed: 'userNewUser',
assertuserfailed: 'userNewUser',
assertnameduserfailed: 'userNewUser',
'abusefilter-disallowed': 'extensionAbuseFilter',
'abusefilter-warning': 'extensionAbuseFilter',
captcha: 'extensionCaptcha',
spamblacklist: 'extensionSpamBlacklist',
'titleblacklist-forbidden': 'extensionTitleBlacklist',
pagedeleted: 'editPageDeleted',
editconflict: 'editConflict'
};
mw.track( 'editAttemptStep', {
action: 'saveFailure',
timing: mw.now() - this.saveInitiated,
message: code,
type: typeMap[ code ] || 'responseUnknown'
} );
};
/**
* Save the comment in the comment controller
*

View file

@ -796,7 +796,7 @@ function update( data, threadItem, pageName, replyWidget ) {
function logSaveSuccess() {
mw.track( 'editAttemptStep', {
action: 'saveSuccess',
timing: mw.now() - replyWidget.saveInitiated,
timing: mw.now() - replyWidget.commentController.saveInitiated,
// eslint-disable-next-line camelcase
revision_id: data.newrevid
} );

View file

@ -348,6 +348,12 @@ OO.inheritClass( ReplyWidget, OO.ui.Widget );
* @event reloadPage
*/
/**
* The reply widget is submitted
*
* @event submit
*/
/* Methods */
/**
@ -393,6 +399,16 @@ ReplyWidget.prototype.isEmpty = null;
*/
ReplyWidget.prototype.getMode = null;
/**
* Clear the save error message
*/
ReplyWidget.prototype.clearSaveErrorMessage = function () {
if ( this.saveErrorMessage ) {
this.saveErrorMessage.$element.remove();
this.saveErrorMessage = null;
}
};
/**
* Restore the widget to its original state
*
@ -402,10 +418,8 @@ ReplyWidget.prototype.getMode = null;
* @param {boolean} [preserveStorage] Preserve auto-save storage
*/
ReplyWidget.prototype.clear = function ( preserveStorage ) {
if ( this.saveErrorMessage ) {
this.saveErrorMessage.$element.remove();
this.saveErrorMessage = null;
}
this.clearSaveErrorMessage();
if ( this.previewRequest ) {
this.previewRequest.abort();
this.previewRequest = null;
@ -1041,56 +1055,54 @@ ReplyWidget.prototype.createErrorMessage = function ( message ) {
/**
* Handle clicks on the reply button
*
* @fires submit
*/
ReplyWidget.prototype.onReplyClick = function () {
if ( this.pending || this.isEmpty() ) {
return;
}
if ( this.saveErrorMessage ) {
this.saveErrorMessage.$element.remove();
this.saveErrorMessage = null;
this.emit( 'submit' );
};
/**
* Set the save error message
*
* @param {string} code Error code
* @param {Object} data Error data
*/
ReplyWidget.setSaveErrorMessage = function ( code, data ) {
if ( !(
// Don't duplicate the parentRemovedErrorMessage
code === 'discussiontools-commentname-notfound' && this.parentRemovedErrorMessage
) ) {
this.saveErrorMessage = this.createErrorMessage(
code instanceof Error ? code.toString() : controller.getApi().getErrorMessage( data )
);
}
};
this.saveInitiated = mw.now();
this.setPending( true );
mw.track( 'editAttemptStep', { action: 'saveIntent' } );
// TODO: When editing a transcluded page, VE API returning the page HTML is a waste, since we won't use it
const pageName = this.pageName;
mw.track( 'editAttemptStep', { action: 'saveAttempt' } );
this.commentController.save( pageName ).fail( ( code, data ) => {
// Compare to ve.init.mw.ArticleTargetEvents.js in VisualEditor.
const typeMap = {
badtoken: 'userBadToken',
assertanonfailed: 'userNewUser',
assertuserfailed: 'userNewUser',
assertnameduserfailed: 'userNewUser',
'abusefilter-disallowed': 'extensionAbuseFilter',
'abusefilter-warning': 'extensionAbuseFilter',
captcha: 'extensionCaptcha',
spamblacklist: 'extensionSpamBlacklist',
'titleblacklist-forbidden': 'extensionTitleBlacklist',
pagedeleted: 'editPageDeleted',
editconflict: 'editConflict'
};
/**
* Clear the captcha input
*/
ReplyWidget.prototype.clearCaptcha = function () {
if ( this.captchaMessage ) {
this.captchaMessage.$element.detach();
}
this.captchaInput = undefined;
};
if ( OO.getProp( data, 'discussiontoolsedit', 'edit', 'captcha' ) ) {
code = 'captcha';
this.captchaInput = new mw.libs.confirmEdit.CaptchaInputWidget(
OO.getProp( data, 'discussiontoolsedit', 'edit', 'captcha' )
);
/**
* Set the captcha input
*
* @param {Object} captchaData Captcha data
*/
ReplyWidget.prototype.setCaptcha = function ( captchaData ) {
this.captchaInput = new mw.libs.confirmEdit.CaptchaInputWidget( captchaData );
// Save when pressing 'Enter' in captcha field as it is single line.
this.captchaInput.on( 'enter', () => {
this.onReplyClick();
this.emit( 'reply' );
} );
this.captchaMessage = new OO.ui.MessageWidget( {
@ -1102,39 +1114,6 @@ ReplyWidget.prototype.onReplyClick = function () {
this.captchaInput.focus();
this.captchaInput.scrollElementIntoView();
} else {
if ( !(
// Don't duplicate the parentRemovedErrorMessage
code === 'discussiontools-commentname-notfound' && this.parentRemovedErrorMessage
) ) {
this.saveErrorMessage = this.createErrorMessage(
code instanceof Error ? code.toString() : controller.getApi().getErrorMessage( data )
);
}
}
if ( code instanceof Error ) {
code = 'exception';
}
// Log more precise error codes, mw.Api just gives us 'http' in all of these cases
if ( data ) {
if ( data.textStatus === 'timeout' || data.textStatus === 'abort' || data.textStatus === 'parsererror' ) {
code = data.textStatus;
} else if ( data.xhr ) {
code = 'http-' + ( data.xhr.status || 0 );
}
}
mw.track( 'editAttemptStep', {
action: 'saveFailure',
timing: mw.now() - this.saveInitiated,
message: code,
type: typeMap[ code ] || 'responseUnknown'
} );
} ).always( () => {
this.setPending( false );
} );
};
/**