/**
* User script to stash and replay conflicts.
*
* The conflicting edit text is stored under a special title like,
* Conflict:<title>/<base_revision_id>/<conflict_sequence>
* Where the placeholders are, the page title where the conflict occurred, the base revision ID that was being edited,
* and a 1-based sequence which is ignored for now, and lets multiple conflicts share the same base revision.
*
* TODO:
* - User input for theTake edit summary from the saved conflict content edit summary.?
* - Controls to do advanced things like tweaking the branchpoint and choosing an "other" edit other than
* the target page's latest revision.
* - Proper namespace for these pages.
* - UI labels from messages.
*/
mw.loader.using( [
'mediawiki.api',
'moment',
'oojs-ui-core'
] ).done( function () {
$( var api = new mw.Api();
function fetchMyRevisionContent() {
// TODO: The regex is missing the start anchor "^" so that it can be used as a subpage in various places,return api.get( {
action: 'query',
prop: 'revisions',
revids: mw.config.get( 'wgCurRevisionId' ),
rvprop: 'content'
// until the Conflict namespace exists.} ).then(
function ( data ) {
for ( var pageId in data.query.pages ) {
for ( var index in data.query.pages[pageId].revisions ) {
return data.query.pages[pageId].revisions[index];
}
}
}
var conflictTitleParams = /Conflict:(.*)\/(\d+)\/(\d+)$/.exec( mw.config.get('wgPageName' ) ););
}
/**
* Get timestamp of the original base revision, to convince EditPage we need to conflict.
* Edit time turns out to be important, conflict cannot be triggered without it!
*/
function fetchBaseRevisionTimestamp( baseRevisionId ) {
if ( conflictTitleParams !== null ) {return api.get( {
varaction: 'query',
prop: 'revisions',
revids: baseTitle = conflictTitleParams[1],eRevisionId,
rvprop: 'timestamp'
} ).then(
function ( data ) {
baseRevisionId = conflictTitleParams[2] /*,for ( var pageId in data.query.pages ) {
for ( var index in data.query.pages[pageId].revisions ) {
var revision = data.query.pages[pageId].revisions[index];
return moment( revision.timestamp ).utc().format( 'YYYYMMDDHHmmss' );
}
conflictSequence = conflictTitleParams[3] */;
}
}
);
}
function buildConflictNamespaceForm( baseTitle, postFields ) {
var form = new OO.ui.FormLayout( {
// TODO: urlencode the title, or build the query using a helper.
action: mw.config.get('wgScript' ) + '?title=' + baseTitle + '&action=submit',
method: 'post'
} );} ),
form.addItems( [recreateButton = new OO.ui.FieldsetLayout( {
new OO.ui.FieldsetLayout( {
label: 'Conflict',
items: [
new OO.ui.ButtonWidget( {
label: 'Recreate conflict',
title: 'Submit',
} ).on( 'click', function () {
// TODO: clean up binding
form.$element.submit();
} )
]
} ) } ),
] );keys = Object.keys( postFields );
form.addItems( [ recreateButton ] );
for ( var i = 0; i < keys.length; i++ ) {
var postFields = {form.addItems( [
wpUnicodeCheck: '鈩仇潚测櫏饾搳饾搩饾捑饾捀鈩答潚光劘',new OO.ui.HiddenInputWidget( {
editRevId: baseRevisionId name: keys[i],
parentRevId: baseRevisionId value: postFields[keys[i]],
// TODO: stored / input / automatic summary field.} )
//wpSummary: '', ] );
wpSave: 'Save changes', }
return form;
mode: 'text', }
function initializeConflictNamespaceActions() {
wpUltimateParam: '1',// TODO: The regex is missing the start anchor "^" so that it can be used as a subpage in various places,
// until the Conflict namespace exists.
var conflictTitleParams = /Conflict:(.*)\/(\d+)\/(\d+)$/.exec( mw.config.get('wgPageName' ) );
if ( conflictTitleParams !== null ) {
};
var api = new mw.Api();var baseTitle = conflictTitleParams[1],
baseRevisionId = conflictTitleParams[2],
conflictSequence = conflictTitleParams[3];
$.when(
api.getEditToken().then(,
function ( editToken ) {fetchMyRevisionContent(),
postFields.wpEditToken = editToken;fetchBaseRevisionTimestamp( baseRevisionId )
} ).done(
),
// Pull content from the conflict page.// TODO: error handling
api.get( {function ( editToken, myRevision, editTime ) {
action: 'query',var postFields = {
prop: 'revisions' wpUnicodeCheck: '鈩仇潚测櫏饾搳饾搩饾捑饾捀鈩答潚光劘',
revids: mw.config.get( 'wgCur editRevId: baseRevisionId' ),
rvprop: 'content' parentRevId: baseRevisionId,
} ).then( // TODO: stored / input / automatic summary field.
function ( data ) { //wpSummary: '',
for ( var pageId in data.query.pages ) { wpSave: 'Save changes',
for ( var index in data.query.pages[pageId].revisions ) {mode: 'text',
var revision = data.query.pages[pageId].revisions[index];wpEditToken: editToken,
postFields.wpTextbox1 = r: myRevision['*'];,
postFields.format = r: myRevision.contentformat;,
postFields.model = r: myRevision.contentmodel;,
return;wpEdittime: editTime,
}wpUltimateParam: '1',
},
}
), form = buildConflictNamespaceForm( baseTitle, postFields );
// Get timestamp of the original base revision, to convince EditPage we need to conflict. $( '#bodyContent' ).prepend( form.$element );
// Edit time turns out to be important, conflict cannot be triggered without it!}
api.get( { );
action: 'query', }
prop: 'revisions', }
function handlePostpone() {
revids: baseRevisionId var baseTitle = mw.config.get( 'wgPageName' ),
rvprop: 'timestamp' baseRevisionId = $( '#editform input[name="parentRevId"]' ).val(),
} ).then( // TODO: Detect existing, deduplicate, and autoincrement.
function ( data ) { conflictSequenceId = 1,
for ( var pageId in data.query.pages ) { // TODO: Preference to put here or global "Conflict:"
for ( var index in data.query.pages[pageId].revisions ) { userName = mw.config.get( 'wgUserName' ),
var revision = data.query.pages[pageId].revisions[index]; conflictTitle = 'User:' + userName + '/Conflict:' + baseTitle + '/' + baseRevisionId + '/' + conflictSequenceId,
// FIXME: date conversion from a library conflictUrl = mw.config.get( 'wgArticlePath' ).replace( '$1', conflictTitle ),
postFields.wpEdittime = moment( revision.timestamp ).utc().format( 'YYYYMMDDHHmmss' ); // TODO: Use TwoColConflict merger to build potentially edited content, also compat with legacy workflow.
return; content = $( '#wpTextbox2' ).val() ||
}$( '#editform input[name="mw-twocolconflict-your-text"]' ).val();
api.create(
} conflictTitle,
} {
)
).done(// TODO: Take from attempted edit.
function () {summary: 'Created by conflict userscript',
var keys = Object.keys( postFields ); },
for ( var i = 0; i < keys.length; i++ ) { content
form.addItems( [ ).then( function () {
new OO.ui.HiddenInputWidget( { window.location.href = conflictUrl;
name: keys[i], } );
value: postFields[keys[i]], }
function buildStashButton() {
} ) return new OO.ui.ButtonWidget( {
] ); label: 'Postpone resolution',
} ).on( 'click', handlePostpone );
}
$( '#bodyContent' ).prepend( form.$element ); function initializeConflictWorkflowActions() {
} if ( mw.config.get( 'wgEditMessage' ) !== 'editconflict' ) {
);return;
}
$( '.cancelLink' ).prepend( buildStashButton().$element );
}
$( function () {
initializeConflictNamespaceActions();
initializeConflictWorkflowActions();
} );
});