Merge "Save the reply directly to the transcluded page"

This commit is contained in:
jenkins-bot 2020-04-03 14:07:44 +00:00 committed by Gerrit Code Review
commit 6020bef2b6
6 changed files with 159 additions and 59 deletions

View file

@ -90,7 +90,8 @@
"discussiontools-replywidget-placeholder-reply",
"discussiontools-replywidget-preview",
"discussiontools-replywidget-reply",
"discussiontools-replywidget-terms-click"
"discussiontools-replywidget-terms-click",
"discussiontools-replywidget-transcluded"
],
"dependencies": [
"ext.discussionTools.init",

View file

@ -17,6 +17,7 @@
"discussiontools-replywidget-preview": "Preview",
"discussiontools-replywidget-reply": "Reply",
"discussiontools-replywidget-terms-click": "By clicking \"$1\", you agree to the terms of use for this wiki.",
"discussiontools-replywidget-transcluded": "Your comment will be saved at [[$1]].",
"discussiontools-error-comment-disappeared": "Could not find the comment you're replying to on the page. It might have been deleted or moved to another page. Please reload the page and try again.",
"discussiontools-error-comment-is-transcluded": "This comment can't be replied to using this tool. Please try using the full page editor instead.",
"discussiontools-error-comment-is-transcluded-title": "This comment can't be replied to here (yet), because it is loaded from another page. Please go to [[$1]] to reply to it.",

View file

@ -22,6 +22,7 @@
"discussiontools-replywidget-preview": "Label for the preview area of the reply widget",
"discussiontools-replywidget-reply": "Label for the button to submit a reply in the reply widget",
"discussiontools-replywidget-terms-click": "Terms of use for posting a reply.\n\n* $1 is the label of the button to be clicked, e.g. {{msg-mw|discussiontools-replywidget-reply}}.",
"discussiontools-replywidget-transcluded": "Message explaining that the comment will be saved on a different page than the one you're viewing right now (because it was transcluded from it). Parameter: $1 page name",
"discussiontools-error-comment-disappeared": "Error message.",
"discussiontools-error-comment-is-transcluded": "Error message.",
"discussiontools-error-comment-is-transcluded-title": "Error message. Parameter: $1 page name",

View file

@ -61,17 +61,15 @@ function setupComment( comment ) {
if ( !widgetPromise ) {
// eslint-disable-next-line no-use-before-define
parsoidPromise = getParsoidCommentData( comment.id );
parsoidPromise = getParsoidTranscludedCommentData( comment.id );
widgetPromise = parsoidPromise.then( function () {
widgetPromise = parsoidPromise.then( function ( parsoidData ) {
return replyWidgetPromise.then( function () {
var
ReplyWidget = config.useVisualEditor ?
require( 'ext.discussionTools.ReplyWidgetVisual' ) :
require( 'ext.discussionTools.ReplyWidgetPlain' ),
replyWidget = new ReplyWidget(
comment
);
replyWidget = new ReplyWidget( parsoidData );
replyWidget.on( 'teardown', teardown );
@ -181,6 +179,25 @@ function postReply( widget, parsoidData ) {
return $.Deferred().resolve().promise();
}
/**
* Get the latest revision ID of the page.
*
* @param {string} pageName
* @return {jQuery.Promise}
*/
function getLatestRevId( pageName ) {
return ( new mw.Api() ).get( {
action: 'query',
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
titles: pageName,
formatversion: 2
} ).then( function ( resp ) {
return resp.query.pages[ 0 ].revisions[ 0 ].revid;
} );
}
function save( widget, parsoidData ) {
var root, summaryPrefix, summary, promise,
mode = widget.getMode(),
@ -226,21 +243,9 @@ function save( widget, parsoidData ) {
// comment has been deleted from the page, or if retry also fails for some other reason, the
// error is handled as normal below.
if ( code === 'editconflict' ) {
return widget.api.get( {
action: 'query',
prop: 'revisions',
rvprop: 'ids',
rvlimit: 1,
titles: mw.config.get( 'wgRelevantPageName' ),
formatversion: 2
} ).then( function ( resp ) {
var latestRevId = resp.query.pages[ 0 ].revisions[ 0 ].revid;
mw.config.set( {
wgCurRevisionId: latestRevId,
wgRevisionId: latestRevId
} );
return getLatestRevId( pageData.pageName ).then( function ( latestRevId ) {
// eslint-disable-next-line no-use-before-define
return getParsoidCommentData( comment.id ).then( function ( parsoidData ) {
return getParsoidCommentData( pageData.pageName, latestRevId, comment.id ).then( function ( parsoidData ) {
return save( widget, parsoidData );
} );
} );
@ -316,17 +321,17 @@ function getPageData( pageName, oldId ) {
/**
* Get the Parsoid document DOM, parse comments and threads, and find a specific comment in it.
*
* @param {string} commentId Comment ID, from a comment parsed in the local document
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @param {string} commentId Comment ID
* @return {jQuery.Promise}
*/
function getParsoidCommentData( commentId ) {
var parsoidPageData, parsoidDoc, parsoidComments, parsoidCommentsById,
pageName = mw.config.get( 'wgRelevantPageName' ),
oldId = mw.config.get( 'wgCurRevisionId' );
function getParsoidCommentData( pageName, oldId, commentId ) {
var parsoidPageData, parsoidDoc, parsoidComments, parsoidCommentsById;
return getPageData( pageName, oldId )
.then( function ( response ) {
var data, comment, transcludedFrom, transcludedErrMsg, mwTitle;
var data, comment, transcludedFrom, transcludedErrMsg, mwTitle, follow;
data = response.visualeditor;
parsoidDoc = ve.parseXhtml( data.content );
@ -362,10 +367,10 @@ function getParsoidCommentData( commentId ) {
transcludedFrom = parser.getTranscludedFrom( comment );
if ( transcludedFrom ) {
mwTitle = transcludedFrom === true ? null : mw.Title.newFromText( transcludedFrom );
// If this refers to a template rather than a subpage, we never want to edit it
if ( mwTitle && mwTitle.getNamespaceId() !== mw.config.get( 'wgNamespaceIds' ).template ) {
// TODO: Post the reply to the target page instead
follow = mwTitle && mwTitle.getNamespaceId() !== mw.config.get( 'wgNamespaceIds' ).template;
if ( follow ) {
transcludedErrMsg = mw.message( 'discussiontools-error-comment-is-transcluded-title',
mwTitle.getPrefixedText() ).parse();
} else {
@ -373,6 +378,10 @@ function getParsoidCommentData( commentId ) {
}
return $.Deferred().reject( 'comment-is-transcluded', { errors: [ {
data: {
transcludedFrom: transcludedFrom,
follow: follow
},
code: 'comment-is-transcluded',
html: transcludedErrMsg
} ] } ).promise();
@ -386,6 +395,41 @@ function getParsoidCommentData( commentId ) {
} );
}
/**
* Like #getParsoidCommentData, but assumes the comment was found on the current page,
* and then follows transclusions to determine the source page where it is written.
*
* @param {string} commentId Comment ID, from a comment parsed in the local document
* @return {jQuery.Promise}
*/
function getParsoidTranscludedCommentData( commentId ) {
var promise,
pageName = mw.config.get( 'wgRelevantPageName' ),
oldId = mw.config.get( 'wgCurRevisionId' );
function followTransclusion( recursionLimit, code, data ) {
var errorData;
if ( recursionLimit > 0 && code === 'comment-is-transcluded' ) {
errorData = data.errors[ 0 ].data;
if ( errorData.follow && typeof errorData.transcludedFrom === 'string' ) {
return getLatestRevId( errorData.transcludedFrom ).then( function ( latestRevId ) {
// Fetch the transcluded page, until we cross the recursion limit
return getParsoidCommentData( errorData.transcludedFrom, latestRevId, commentId )
.catch( followTransclusion.bind( null, recursionLimit - 1 ) );
} );
}
}
return $.Deferred().reject( code, data );
}
// Arbitrary limit of 10 steps, which should be more than anyone could ever need
// (there are reasonable use cases for at least 2)
promise = getParsoidCommentData( pageName, oldId, commentId )
.catch( followTransclusion.bind( null, 10 ) );
return promise;
}
function init( $container, state ) {
var
pageComments, pageThreads, pageCommentsById,

View file

@ -8,11 +8,11 @@ var controller = require( 'ext.discussionTools.init' ).controller,
* @class mw.dt.ReplyWidget
* @extends OO.ui.Widget
* @constructor
* @param {Object} comment Parsed comment object
* @param {Object} parsoidData Result from controller#getParsoidCommentData
* @param {Object} [config] Configuration options
* @param {Object} [config.input] Configuration options for the comment input widget
*/
function ReplyWidget( comment, config ) {
function ReplyWidget( parsoidData, config ) {
var returnTo, contextNode, inputConfig;
config = config || {};
@ -21,12 +21,13 @@ function ReplyWidget( comment, config ) {
ReplyWidget.super.call( this, config );
this.pending = false;
this.comment = comment;
this.comment = parsoidData.comment;
this.pageData = parsoidData.pageData;
contextNode = utils.closestElement( this.comment.range.endContainer, [ 'dl', 'ul', 'ol' ] );
this.context = contextNode ? contextNode.nodeName.toLowerCase() : 'dl';
inputConfig = $.extend(
{ placeholder: mw.msg( 'discussiontools-replywidget-placeholder-reply', comment.author ) },
{ placeholder: mw.msg( 'discussiontools-replywidget-placeholder-reply', this.comment.author ) },
config.input
);
this.replyBodyWidget = this.createReplyBodyWidget( inputConfig );
@ -46,10 +47,16 @@ function ReplyWidget( comment, config ) {
this.cancelButton.$element,
this.replyButton.$element
);
this.$terms = $( '<div>' ).addClass( 'dt-ui-replyWidget-terms' ).append(
this.$footer = $( '<div>' ).addClass( 'dt-ui-replyWidget-footer' );
if ( this.pageData.pageName !== mw.config.get( 'wgRelevantPageName' ) ) {
this.$footer.append( $( '<p>' ).append(
mw.message( 'discussiontools-replywidget-transcluded', this.pageData.pageName ).parseDom()
) );
}
this.$footer.append( $( '<p>' ).append(
mw.message( 'discussiontools-replywidget-terms-click', mw.msg( 'discussiontools-replywidget-reply' ) ).parseDom()
);
this.$actionsWrapper.append( this.$terms, this.$actions );
) );
this.$actionsWrapper.append( this.$footer, this.$actions );
// Events
this.replyButton.connect( this, { click: 'onReplyClick' } );
@ -84,7 +91,7 @@ function ReplyWidget( comment, config ) {
.parseDom()
} );
this.anonWarning.$element.append( this.$actions );
this.$element.append( this.anonWarning.$element, this.$terms );
this.$element.append( this.anonWarning.$element, this.$footer );
this.$actionsWrapper.detach();
}
@ -291,39 +298,81 @@ ReplyWidget.prototype.onReplyClick = function () {
logger( { action: 'saveIntent' } );
// TODO: When editing a transcluded page, VE API returning the page HTML is a waste, since we won't use it
// We must get a new copy of the document every time, otherwise any unsaved replies will pile up
controller.getParsoidCommentData( this.comment.id ).then( function ( parsoidData ) {
controller.getParsoidCommentData( this.pageData.pageName, this.pageData.oldId, this.comment.id ).then( function ( parsoidData ) {
logger( { action: 'saveAttempt' } );
return controller.save( widget, parsoidData );
} ).then( function ( data ) {
// eslint-disable-next-line no-jquery/no-global-selector
var $container = $( '#mw-content-text' );
var
pageUpdated = $.Deferred(),
// eslint-disable-next-line no-jquery/no-global-selector
$container = $( '#mw-content-text' );
widget.teardown();
// TODO: Tell controller to teardown all other open widgets
// Update page state
$container.html( data.content );
mw.config.set( {
wgCurRevisionId: data.newrevid,
wgRevisionId: data.newrevid
} );
mw.config.set( data.jsconfigvars );
mw.loader.load( data.modules );
// TODO update categories, lastmodified
// (see ve.init.mw.DesktopArticleTarget.prototype.replacePageContent)
if ( widget.pageData.pageName === mw.config.get( 'wgRelevantPageName' ) ) {
// We can use the result from the VisualEditor API
$container.html( data.content );
mw.config.set( {
wgCurRevisionId: data.newrevid,
wgRevisionId: data.newrevid
} );
mw.config.set( data.jsconfigvars );
// Note: VE API merges 'modules' and 'modulestyles'
mw.loader.load( data.modules );
// TODO update categories, displaytitle, lastmodified
// (see ve.init.mw.DesktopArticleTarget.prototype.replacePageContent)
// Re-initialize
controller.init( $container.find( '.mw-parser-output' ), {
repliedTo: widget.comment.id
} );
mw.hook( 'wikipage.content' ).fire( $container );
pageUpdated.resolve();
logger( {
action: 'saveSuccess',
// eslint-disable-next-line camelcase
revision_id: data.newrevid
} else {
// We saved to another page, we must purge and then fetch the current page
widget.api.post( {
action: 'purge',
titles: mw.config.get( 'wgRelevantPageName' )
} ).then( function () {
return widget.api.get( {
formatversion: 2,
action: 'parse',
prop: [ 'text', 'modules', 'jsconfigvars' ],
page: mw.config.get( 'wgRelevantPageName' )
} );
} ).then( function ( parseResp ) {
$container.html( parseResp.parse.text );
mw.config.set( parseResp.parse.jsconfigvars );
mw.loader.load( parseResp.parse.modulestyles );
mw.loader.load( parseResp.parse.modules );
// TODO update categories, displaytitle, lastmodified
// We may not be able to use prop=displaytitle without making changes in the action=parse API,
// VE API has some confusing code that changes the HTML escaping on it before returning???
pageUpdated.resolve();
} ).catch( function () {
// We saved the reply, but couldn't purge or fetch the updated page. Seems difficult to
// explain this problem. Redirect to the page where the user can at least see their reply…
window.location = mw.util.getUrl( widget.pageData.pageName );
} );
}
pageUpdated.then( function () {
// Re-initialize
controller.init( $container.find( '.mw-parser-output' ), {
repliedTo: widget.comment.id
} );
mw.hook( 'wikipage.content' ).fire( $container );
logger( {
action: 'saveSuccess',
// eslint-disable-next-line camelcase
revision_id: data.newrevid
} );
} );
}, function ( code, data ) {
var typeMap = {
// Compare to ve.init.mw.ArticleTargetEvents.js in VisualEditor.

View file

@ -17,10 +17,14 @@
white-space: nowrap;
}
&-terms {
&-footer {
flex-grow: 1;
font-size: 0.75em;
color: #54595d;
> p:first-child {
margin-top: 0;
}
}
&-preview {