Support '&preload=...' etc. in new topic tool when '&dtpreload=1' is set

To avoid affecting existing preload forms, the new topic tool is only
used when the 'dtpreload' query parameter is also set.

Bug: T269310
Change-Id: I4ee024cc4760542790319f302f42b1b2389ac897
This commit is contained in:
Bartosz Dziewoński 2021-06-29 17:35:37 +02:00 committed by Martin Urbanec
parent 0814fd89ee
commit 5af3e90fec
5 changed files with 133 additions and 52 deletions

View file

@ -483,19 +483,18 @@ class HookUtils {
public static function shouldOpenNewTopicTool( IContextSource $context ): bool {
$req = $context->getRequest();
$out = $context->getOutput();
$hasPreload = $req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) ||
$req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) ||
// Switching or previewing from an external tool (T316333)
$req->getCheck( 'wpTextbox1' );
return (
// ?title=...&action=edit&section=new
// ?title=...&veaction=editsource&section=new
( $req->getRawVal( 'action' ) === 'edit' || $req->getRawVal( 'veaction' ) === 'editsource' ) &&
$req->getRawVal( 'section' ) === 'new' &&
// Adding a new topic with preloaded text is not supported yet (T269310)
!(
$req->getCheck( 'editintro' ) || $req->getCheck( 'preload' ) ||
$req->getCheck( 'preloadparams' ) || $req->getCheck( 'preloadtitle' ) ||
// Switching or previewing from an external tool (T316333)
$req->getCheck( 'wpTextbox1' )
) &&
// Handle new topic with preloaded text only when requested (T269310)
( $req->getCheck( 'dtpreload' ) || !$hasPreload ) &&
// User has new topic tool enabled (and not using &dtenable=0)
static::isFeatureEnabledForOutput( $out, static::NEWTOPICTOOL )
);

View file

@ -7,11 +7,15 @@
* @param {number} oldId Revision ID of page at time of editing
* @param {Object.<string,string>} notices Edit notices for the page where the reply is being saved.
* Keys are message names; values are HTML to display.
* @param {string} preloadContent Preload content, may be wikitext or HTML depending on `preloadContentMode`
* @param {string} preloadContentMode 'source' or 'visual'
*/
function CommentDetails( pageName, oldId, notices ) {
function CommentDetails( pageName, oldId, notices, preloadContent, preloadContentMode ) {
this.pageName = pageName;
this.oldId = oldId;
this.notices = notices;
this.preloadContent = preloadContent;
this.preloadContentMode = preloadContentMode;
}
OO.initClass( CommentDetails );

View file

@ -1,5 +1,4 @@
var
utils = require( './utils.js' ),
logger = require( './logger.js' ),
controller = require( './controller.js' ),
CommentController = require( './CommentController.js' ),
@ -9,9 +8,10 @@ var
* Handles setup, save and teardown of new topic widget
*
* @param {jQuery} $pageContainer Page container
* @param {HeadingItem} threadItem
* @param {ThreadItemSet} threadItemSet
*/
function NewTopicController( $pageContainer, threadItemSet ) {
function NewTopicController( $pageContainer, threadItem, threadItemSet ) {
this.container = new OO.ui.PanelLayout( {
classes: [ 'ext-discussiontools-ui-newTopic' ],
expanded: false,
@ -36,14 +36,10 @@ function NewTopicController( $pageContainer, threadItemSet ) {
this.container.$element.append( this.$notices, this.sectionTitleField.$element );
// HeadingItem representing the heading being added, so that we can pretend we're replying to it
var threadItem = new HeadingItem( {
startContainer: this.sectionTitleField.$element[ 0 ],
startOffset: 0,
endContainer: this.sectionTitleField.$element[ 0 ],
endOffset: this.sectionTitleField.$element[ 0 ].childNodes.length
}, 2 );
threadItem.id = utils.NEW_TOPIC_COMMENT_ID;
threadItem.isNewTopic = true;
threadItem.range.startContainer = this.sectionTitleField.$element[ 0 ];
threadItem.range.startOffset = 0;
threadItem.range.endContainer = this.sectionTitleField.$element[ 0 ];
threadItem.range.endOffset = this.sectionTitleField.$element[ 0 ].childNodes.length;
NewTopicController.super.call( this, $pageContainer, threadItem, threadItemSet );
}
@ -90,6 +86,10 @@ NewTopicController.prototype.setup = function ( mode ) {
NewTopicController.super.prototype.setup.call( this, mode );
if ( this.threadItem.preloadtitle ) {
this.sectionTitle.setValue( this.threadItem.preloadtitle );
}
// The section title field is added to the page immediately, we can scroll to the bottom and focus
// it while the content field is still loading.
rootScrollable.scrollTop = rootScrollable.scrollHeight;
@ -115,7 +115,16 @@ NewTopicController.prototype.setup = function ( mode ) {
/**
* @inheritdoc
*/
NewTopicController.prototype.setupReplyWidget = function ( replyWidget ) {
NewTopicController.prototype.setupReplyWidget = function ( replyWidget, data ) {
if ( replyWidget.commentDetails.preloadContent && ( !data || data.value === undefined ) ) {
if ( replyWidget.commentDetails.preloadContentMode !== replyWidget.getMode() ) {
// This should never happen
throw new Error( 'Preload content was loaded for wrong mode' );
}
data = $.extend( {}, data, {
value: replyWidget.commentDetails.preloadContent
} );
}
NewTopicController.super.prototype.setupReplyWidget.apply( this, arguments );
this.$notices.empty();
@ -144,6 +153,7 @@ NewTopicController.prototype.setupReplyWidget = function ( replyWidget ) {
this.replyWidget.editSummaryInput.setValue( generatedSummary );
}
}
this.replyWidget.storage.set( this.replyWidget.storagePrefix + '/title', this.sectionTitle.getValue() );
if ( this.replyWidget.modeTabSelect ) {
// Start with the mode-select widget not-tabbable so focus will go from the title to the body
@ -270,6 +280,11 @@ NewTopicController.prototype.teardown = function ( abandoned ) {
url.searchParams.delete( 'action' );
url.searchParams.delete( 'veaction' );
url.searchParams.delete( 'section' );
url.searchParams.delete( 'dtpreload' );
url.searchParams.delete( 'editintro' );
url.searchParams.delete( 'preload' );
url.searchParams.delete( 'preloadparams[]' );
url.searchParams.delete( 'preloadtitle' );
history.replaceState( null, '', url );
mw.config.set( 'wgDiscussionToolsStartNewTopicTool', false );
}

View file

@ -56,6 +56,8 @@ OO.mixinClass( ReplyLinksController, OO.EventEmitter );
* @event link-click
* @param {string} id
* @param {jQuery} $linkSet
* @param {jQuery} $link
* @param {Object} [data]
*/
/* Methods */
@ -104,11 +106,32 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) {
if ( !href ) {
return;
}
var data = this.parseNewTopicLink( href );
if ( !data ) {
return;
}
if ( !this.isActivationEvent( e ) ) {
return;
}
e.preventDefault();
this.emit( 'link-click', utils.NEW_TOPIC_COMMENT_ID, $( e.currentTarget ), data );
};
/**
* Check if the given URL is a new topic link, and if so, return parsed parameters.
*
* @param {string} href
* @return {Object|null} `null` if not a new topic link, parameters otherwise
*/
ReplyLinksController.prototype.parseNewTopicLink = function ( href ) {
var url = new URL( href );
var title = mw.Title.newFromText( utils.getTitleFromUrl( href ) || '' );
if ( !title ) {
return;
return null;
}
// Recognize links to add a new topic:
@ -121,7 +144,7 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) {
var param = title.getMainText().slice( parserData.specialNewSectionName.length + 1 );
title = mw.Title.newFromText( param );
if ( !title ) {
return;
return null;
}
} else if (
@ -135,28 +158,34 @@ ReplyLinksController.prototype.onAnyLinkClick = function ( e ) {
} else {
// Not a link to add a new topic
return;
return null;
}
if ( title.getPrefixedDb() !== mw.config.get( 'wgRelevantPageName' ) ) {
// Link to add a section on another page, not supported yet (T282205)
return;
return null;
}
if (
url.searchParams.get( 'editintro' ) || url.searchParams.get( 'preload' ) ||
url.searchParams.getAll( 'preloadparams[]' ).length || url.searchParams.get( 'preloadtitle' )
) {
// Adding a new topic with preloaded text is not supported yet (T269310)
return;
var data = {};
if ( url.searchParams.get( 'editintro' ) ) {
data.editintro = url.searchParams.get( 'editintro' );
}
if ( url.searchParams.get( 'preload' ) ) {
data.preload = url.searchParams.get( 'preload' );
}
if ( url.searchParams.getAll( 'preloadparams[]' ).length ) {
data.preloadparams = url.searchParams.getAll( 'preloadparams[]' );
}
if ( url.searchParams.get( 'preloadtitle' ) ) {
data.preloadtitle = url.searchParams.get( 'preloadtitle' );
}
if ( !this.isActivationEvent( e ) ) {
return;
// Handle new topic with preloaded text only when requested (T269310)
if ( !url.searchParams.get( 'dtpreload' ) && !$.isEmptyObject( data ) ) {
return null;
}
e.preventDefault();
this.emit( 'link-click', utils.NEW_TOPIC_COMMENT_ID, $( e.currentTarget ) );
return data;
};
ReplyLinksController.prototype.isActivationEvent = function ( e ) {

View file

@ -10,6 +10,7 @@ var
Parser = require( './Parser.js' ),
ThreadItemSet = require( './ThreadItemSet.js' ),
CommentDetails = require( './CommentDetails.js' ),
HeadingItem = require( './HeadingItem.js' ),
ReplyLinksController = require( './ReplyLinksController.js' ),
logger = require( './logger.js' ),
utils = require( './utils.js' ),
@ -43,13 +44,15 @@ function getApi() {
*
* @param {string} pageName Page title
* @param {number} oldId Revision ID
* @param {Object} [apiParams] Additional parameters for the API
* @return {jQuery.Promise}
*/
function getPageData( pageName, oldId ) {
function getPageData( pageName, oldId, apiParams ) {
var api = getApi();
apiParams = apiParams || {};
pageDataCache[ pageName ] = pageDataCache[ pageName ] || {};
if ( pageDataCache[ pageName ][ oldId ] ) {
if ( pageDataCache[ pageName ][ oldId ] && $.isEmptyObject( apiParams ) ) {
return pageDataCache[ pageName ][ oldId ];
}
@ -77,15 +80,15 @@ function getPageData( pageName, oldId ) {
transcludedFromPromise = $.Deferred().resolve( {} ).promise();
}
var veMetadataPromise = api.get( {
var veMetadataPromise = api.get( $.extend( {
action: 'visualeditor',
paction: 'metadata',
page: pageName
} ).then( function ( response ) {
}, apiParams ) ).then( function ( response ) {
return OO.getProp( response, 'visualeditor' ) || [];
} );
pageDataCache[ pageName ][ oldId ] = $.when( lintPromise, transcludedFromPromise, veMetadataPromise )
var promise = $.when( lintPromise, transcludedFromPromise, veMetadataPromise )
.then( function ( linterrors, transcludedfrom, metadata ) {
return {
linterrors: linterrors,
@ -98,7 +101,12 @@ function getPageData( pageName, oldId ) {
// Let caller handle the error
return $.Deferred().rejectWith( this, arguments );
} );
return pageDataCache[ pageName ][ oldId ];
if ( $.isEmptyObject( apiParams ) ) {
pageDataCache[ pageName ][ oldId ] = promise;
}
return promise;
}
/**
@ -112,8 +120,19 @@ function getPageData( pageName, oldId ) {
*/
function checkThreadItemOnPage( pageName, oldId, threadItem ) {
var isNewTopic = threadItem.id === utils.NEW_TOPIC_COMMENT_ID;
var defaultMode = mw.user.options.get( 'discussiontools-editmode' ) || mw.config.get( 'wgDiscussionToolsFallbackEditMode' );
var apiParams = null;
if ( isNewTopic ) {
apiParams = {
section: 'new',
editintro: threadItem.editintro,
preload: threadItem.preload,
preloadparams: threadItem.preloadparams,
paction: defaultMode === 'source' ? 'wikitext' : 'parse'
};
}
return getPageData( pageName, oldId )
return getPageData( pageName, oldId, apiParams )
.then( function ( response ) {
var metadata = response.metadata,
lintErrors = response.linterrors,
@ -192,7 +211,7 @@ function checkThreadItemOnPage( pageName, oldId, threadItem ) {
} ] } ).promise();
}
return new CommentDetails( pageName, oldId, metadata.notices );
return new CommentDetails( pageName, oldId, metadata.notices, metadata.content, defaultMode );
} );
}
@ -286,26 +305,26 @@ function init( $container, state ) {
/**
* Setup comment controllers for each comment, and the new topic controller
*
* @param {string} commentId Comment ID, or NEW_TOPIC_COMMENT_ID constant
* @param {ThreadItem} comment
* @param {jQuery} $link Add section link for new topic controller
* @param {string} [mode] Optionally force a mode, 'visual' or 'source'
* @param {boolean} [hideErrors] Suppress errors, e.g. when restoring auto-save
* @param {boolean} [suppressNotifications] Don't notify the user if recovering auto-save
*/
function setupController( commentId, $link, mode, hideErrors, suppressNotifications ) {
function setupController( comment, $link, mode, hideErrors, suppressNotifications ) {
var commentController, $addSectionLink;
if ( commentId === utils.NEW_TOPIC_COMMENT_ID ) {
if ( comment.id === utils.NEW_TOPIC_COMMENT_ID ) {
// eslint-disable-next-line no-jquery/no-global-selector
$addSectionLink = $( '#ca-addsection' ).find( 'a' );
// When opening new topic tool using any link, always activate the link in page tabs too
$link = $link.add( $addSectionLink );
commentController = new NewTopicController( $pageContainer, pageThreads );
commentController = new NewTopicController( $pageContainer, comment, pageThreads );
} else {
commentController = new CommentController( $pageContainer, pageThreads.findCommentById( commentId ), pageThreads );
commentController = new CommentController( $pageContainer, comment, pageThreads );
}
activeCommentId = commentId;
activeCommentId = comment.id;
activeController = commentController;
linksController.setActiveLink( $link );
@ -345,11 +364,19 @@ function init( $container, state ) {
}
}
function newTopicComment( data ) {
var comment = new HeadingItem( {}, 2 );
comment.id = utils.NEW_TOPIC_COMMENT_ID;
comment.isNewTopic = true;
$.extend( comment, data );
return comment;
}
// Hook up each link to open a reply widget
//
// TODO: Allow users to use multiple reply widgets simultaneously.
// Currently submitting a reply from one widget would also destroy the other ones.
linksController.on( 'link-click', function ( commentId, $link ) {
linksController.on( 'link-click', function ( commentId, $link, data ) {
// If the reply widget is already open, activate it.
// Reply links are also made unclickable using 'pointer-events' in CSS, but that doesn't happen
// for new section links, because we don't have a good way of visually disabling them.
@ -372,7 +399,13 @@ function init( $container, state ) {
if ( activeController ) {
return;
}
setupController( commentId, $link );
var comment;
if ( commentId !== utils.NEW_TOPIC_COMMENT_ID ) {
comment = parser.findCommentById( comment.id );
} else {
comment = newTopicComment( data );
}
setupController( comment, $link );
} );
} );
@ -389,7 +422,7 @@ function init( $container, state ) {
if ( storage.get( 'reply/' + comment.id + '/saveable' ) ) {
mode = storage.get( 'reply/' + comment.id + '/mode' );
$link = $( commentNodes[ i ] );
setupController( comment.id, $link, mode, true, !state.firstLoad );
setupController( comment, $link, mode, true, !state.firstLoad );
break;
}
}
@ -398,9 +431,10 @@ function init( $container, state ) {
storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/title' )
) {
mode = storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/mode' );
setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ), mode, true, !state.firstLoad );
setupController( newTopicComment(), $( [] ), mode, true, !state.firstLoad );
} else if ( mw.config.get( 'wgDiscussionToolsStartNewTopicTool' ) ) {
setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ) );
var data = linksController.parseNewTopicLink( location.href );
setupController( newTopicComment( data ), $( [] ) );
}
}() );