Implement add a reference edit check

Change-Id: I4cebc5bbaa34300d1c5bb5fde8277269b14779c9
This commit is contained in:
Ed Sanders 2023-05-10 15:23:42 +01:00
parent 228596cba0
commit 3ece481e71
12 changed files with 552 additions and 17 deletions

View file

@ -6,6 +6,6 @@
/docs/
# Language files written automatically by TranslateWiki
/i18n/**/*.json
!/i18n/**/en.json
!/i18n/**/qqq.json
**/i18n/**/*.json
!**/i18n/**/en.json
!**/i18n/**/qqq.json

View file

@ -180,6 +180,10 @@
"value": false,
"description": "For testing only. Tag edits for the Edit Check project."
},
"VisualEditorEditCheck": {
"value": false,
"description": "Enable experimental Edit Check feature."
},
"VisualEditorUseSingleEditTab": {
"value": false
}
@ -218,7 +222,8 @@
"i18n/ve-mw",
"i18n/ve-mw/api",
"i18n/ve-mw/mwlanguagevariant",
"i18n/ve-wmf"
"i18n/ve-wmf",
"modules/editcheck/i18n"
]
},
"ExtensionMessagesFiles": {
@ -598,10 +603,32 @@
"ext.visualEditor.editCheck": {
"group": "visualEditorA",
"scripts": [
"modules/editcheck/init.js"
"modules/editcheck/init.js",
"modules/editcheck/EditCheckContextItem.js",
"modules/editcheck/EditCheckInspector.js"
],
"styles": [
"modules/editcheck/EditCheck.less"
],
"dependencies": [
"ext.visualEditor.core",
"ext.visualEditor.mwsave"
],
"messages": [
"editcheck-dialog-action-no",
"editcheck-dialog-action-yes",
"editcheck-dialog-addref-description",
"editcheck-dialog-addref-reject-question",
"editcheck-dialog-addref-reject-description",
"editcheck-dialog-addref-reject-no-info",
"editcheck-dialog-addref-reject-already-cited",
"editcheck-dialog-addref-reject-not-sure",
"editcheck-dialog-addref-reject-other",
"editcheck-dialog-addref-success-notify",
"editcheck-dialog-addref-title",
"editcheck-dialog-title",
"visualeditor-backbutton-tooltip"
],
"dependencies": [],
"messages": [],
"targets": [
"desktop",
"mobile"

View file

@ -1113,6 +1113,7 @@ class Hooks implements TextSlotDiffRendererTablePrefixHook {
),
'useChangeTagging' => $veConfig->get( 'VisualEditorUseChangeTagging' ),
'editCheckTagging' => $veConfig->get( 'VisualEditorEditCheckTagging' ),
'editCheck' => $veConfig->get( 'VisualEditorEditCheck' ),
'namespacesWithSubpages' => $namespacesWithSubpagesEnabled,
'specialBooksources' => urldecode( SpecialPage::getTitleFor( 'Booksources' )->getPrefixedURL() ),
'rebaserUrl' => $coreConfig->get( 'VisualEditorRebaserURL' ),

View file

@ -0,0 +1,35 @@
/* Toolbar */
.ve-ui-toolbar-group-title {
font-weight: bold;
flex: 5 !important; /* stylelint-disable-line declaration-no-important */
line-height: 3em;
}
/* Context item */
.ve-ui-editCheckContextItem {
> .ve-ui-linearContextItem-head {
background: #fce7fe;
}
&-actions {
margin-top: 16px;
}
}
/* Selections */
.ve-ce-surface-selections-editCheck .ve-ce-surface-selection {
opacity: 0.2;
}
.ve-ce-surface-selections-editCheck .ve-ce-surface-selection > div {
mix-blend-mode: darken;
// Adjust target colours to account for 50% opacity
background: ( #fce7fe - 0.8 * ( #fff ) ) / 0.2;
// border: 1px solid ( ( #d02aac - 0.8 * ( #fff ) ) / 0.2 );
border-radius: 2px;
padding: 2px;
margin: -2px 0 0 -2px;
}

View file

@ -0,0 +1,126 @@
/*!
* VisualEditor EditCheckContextItem class.
*
* @copyright 2011-2019 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* Context item shown after a rich text paste.
*
* @class
* @extends ve.ui.PersistentContextItem
*
* @constructor
* @param {ve.ui.LinearContext} context Context the item is in
* @param {ve.dm.Model} model Model the item is related to
* @param {Object} [config]
*/
ve.ui.EditCheckContextItem = function VeUiEditCheckContextItem() {
// Parent constructor
ve.ui.EditCheckContextItem.super.apply( this, arguments );
// Initialization
this.$element.addClass( 've-ui-editCheckContextItem' );
};
/* Inheritance */
OO.inheritClass( ve.ui.EditCheckContextItem, ve.ui.PersistentContextItem );
/* Static Properties */
ve.ui.EditCheckContextItem.static.name = 'editCheck';
ve.ui.EditCheckContextItem.static.icon = 'quotes';
ve.ui.EditCheckContextItem.static.label = OO.ui.deferMsg( 'editcheck-dialog-addref-title' );
/* Methods */
/**
* @inheritdoc
*/
ve.ui.EditCheckContextItem.prototype.renderBody = function () {
// Prompt panel
var acceptButton = new OO.ui.ButtonWidget( {
label: ve.msg( 'editcheck-dialog-action-yes' ),
icon: 'check'
} );
var rejectButton = new OO.ui.ButtonWidget( {
label: ve.msg( 'editcheck-dialog-action-no' ),
icon: 'close'
} );
acceptButton.connect( this, { click: 'onAcceptClick' } );
rejectButton.connect( this, { click: 'onRejectClick' } );
// HACK: Suppress close button on mobile context
if ( this.context.isMobile() ) {
this.context.closeButton.toggle( false );
}
this.$body.append(
$( '<p>' ).text( ve.msg( 'editcheck-dialog-addref-description' ) ),
$( '<div>' ).addClass( 've-ui-editCheckContextItem-actions' ).append(
acceptButton.$element, rejectButton.$element
)
);
};
ve.ui.EditCheckContextItem.prototype.close = function ( data ) {
// HACK: Un-suppress close button on mobile context
if ( this.context.isMobile() ) {
this.context.closeButton.toggle( true );
}
this.data.saveProcessDeferred.resolve( data );
};
ve.ui.EditCheckContextItem.prototype.onAcceptClick = function () {
var contextItem = this;
var fragment = this.data.fragment;
var windowAction = ve.ui.actionFactory.create( 'window', this.context.getSurface() );
fragment.collapseToEnd().select();
windowAction.open( 'citoid' ).then( function ( instance ) {
return instance.closing;
} ).then( function ( data ) {
if ( !data ) {
// Reference was not inserted - re-open this context
setTimeout( function () {
// Deactivate again for mobile after teardown has modified selections
contextItem.context.getSurface().getView().deactivate();
contextItem.context.afterContextChange();
}, 500 );
} else {
// Edit check inspector is already closed by this point, but
// we need to end the workflow.
contextItem.close( data );
}
} );
};
ve.ui.EditCheckContextItem.prototype.onRejectClick = function () {
var contextItem = this;
var windowAction = ve.ui.actionFactory.create( 'window', this.context.getSurface() );
windowAction.open(
'editCheckInspector',
{
fragment: this.data.fragment,
saveProcessDeferred: this.data.saveProcessDeferred
}
).then( function ( instance ) {
// contextItem.openingCitoid = false;
return instance.closing;
} ).then( function ( data ) {
if ( !data ) {
// Form was closed, re-open this context
contextItem.context.afterContextChange();
} else {
contextItem.close( data );
}
} );
};
/* Registration */
ve.ui.contextItemFactory.register( ve.ui.EditCheckContextItem );

View file

@ -0,0 +1,151 @@
/*!
* VisualEditor UserInterface EditCheckInspector class.
*
* @copyright 2011-2020 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* Edit check inspector
*
* @class
* @extends ve.ui.FragmentInspector
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.EditCheckInspector = function VeUiEditCheckInspector( config ) {
// Parent constructor
ve.ui.EditCheckInspector.super.call( this, config );
// Pre-initialization
this.$element.addClass( 've-ui-editCheckInspector' );
};
/* Inheritance */
OO.inheritClass( ve.ui.EditCheckInspector, ve.ui.FragmentInspector );
ve.ui.EditCheckInspector.static.name = 'editCheckInspector';
// ve.ui.EditCheckInspector.static.title = OO.ui.deferMsg( 'editcheck-dialog-title' );
ve.ui.EditCheckInspector.static.title = OO.ui.deferMsg( 'editcheck-dialog-addref-title' );
// ve.ui.EditCheckInspector.static.size = 'context';
ve.ui.EditCheckInspector.static.actions = [
{
label: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
flags: [ 'safe', 'back' ],
modes: [ 'mobile', 'desktop' ]
},
{
action: 'continue',
icon: 'next',
flags: [ 'primary', 'progressive' ],
modes: [ 'mobile' ]
}
];
/* Methods */
/**
* @inheritdoc
*/
ve.ui.EditCheckInspector.prototype.initialize = function () {
// Parent method
ve.ui.EditCheckInspector.super.prototype.initialize.call( this );
// Survey panel
this.answerRadioSelect = new OO.ui.RadioSelectWidget( {
items: [
new OO.ui.RadioOptionWidget( {
data: 'no-info',
label: ve.msg( 'editcheck-dialog-addref-reject-no-info' )
} ),
new OO.ui.RadioOptionWidget( {
data: 'already-cited',
label: ve.msg( 'editcheck-dialog-addref-reject-already-cited' )
} ),
new OO.ui.RadioOptionWidget( {
data: 'not-sure',
label: ve.msg( 'editcheck-dialog-addref-reject-not-sure' )
} ),
new OO.ui.RadioOptionWidget( {
data: 'other',
label: ve.msg( 'editcheck-dialog-addref-reject-other' )
} )
]
} );
this.answerRadioSelect.connect( this, { select: 'updateActions' } );
this.answerConfirm = new OO.ui.ButtonWidget( {
flags: [ 'progressive' ],
framed: false,
label: 'Continue',
disabled: true
} );
this.answerConfirm.toggle( !OO.ui.isMobile() );
this.answerConfirm.connect( this, { click: [ 'executeAction', 'continue' ] } );
this.form.addItems(
new OO.ui.FieldsetLayout( {
label: ve.msg( 'editcheck-dialog-addref-reject-question' ),
items: [
new OO.ui.FieldLayout( this.answerRadioSelect, {
label: ve.msg( 'editcheck-dialog-addref-reject-description' ),
align: 'top'
} ),
new OO.ui.FieldLayout( this.answerConfirm, {
align: 'left'
} )
]
} )
);
};
ve.ui.EditCheckInspector.prototype.updateActions = function () {
this.answerConfirm.setDisabled( !this.answerRadioSelect.findSelectedItem() );
};
/**
* @inheritdoc
*/
ve.ui.EditCheckInspector.prototype.getSetupProcess = function ( data ) {
data = data || {};
return ve.ui.EditCheckInspector.super.prototype.getSetupProcess.call( this, data )
.first( function () {
this.surface = data.surface;
this.saveProcessDeferred = data.saveProcessDeferred;
this.answerRadioSelect.selectItem( null );
}, this );
};
/**
* @inheritdoc
*/
ve.ui.EditCheckInspector.prototype.getReadyProcess = function ( data ) {
return ve.ui.EditCheckInspector.super.prototype.getReadyProcess.call( this, data )
.first( function () {
this.actions.setMode( OO.ui.isMobile() ? 'mobile' : 'desktop' );
}, this );
};
ve.ui.EditCheckInspector.prototype.getActionProcess = function ( action ) {
if ( action === '' ) {
return new OO.ui.Process( function () {
this.close();
}, this );
}
if ( action === 'continue' ) {
return new OO.ui.Process( function () {
this.close( { action: 'reject', reason: this.answerRadioSelect.findSelectedItem().getData() } );
}, this );
}
return ve.ui.EditCheckInspector.super.prototype.getActionProcess.call( this, action );
};
/* Registration */
ve.ui.windowFactory.register( ve.ui.EditCheckInspector );

View file

@ -0,0 +1,14 @@
{
"editcheck-dialog-action-no": "No",
"editcheck-dialog-action-yes": "Yes",
"editcheck-dialog-addref-description": "Help readers understand where this information is coming from by adding a citation.",
"editcheck-dialog-addref-reject-question": "Why are you not adding a citation?",
"editcheck-dialog-addref-reject-description": "Other editors would value learning more about your decision to dismiss the citation.",
"editcheck-dialog-addref-reject-no-info": "I didn't add new information",
"editcheck-dialog-addref-reject-already-cited": "My changes are already cited earlier",
"editcheck-dialog-addref-reject-not-sure": "I'm not sure what citation to add",
"editcheck-dialog-addref-reject-other": "Other",
"editcheck-dialog-addref-success-notify": "Thank you for adding a citation!",
"editcheck-dialog-addref-title": "Add a citation",
"editcheck-dialog-title": "Before publishing"
}

View file

@ -0,0 +1,14 @@
{
"editcheck-dialog-action-no": "Non",
"editcheck-dialog-action-yes": "Oui",
"editcheck-dialog-addref-description": "Aidez les lecteurs à comprendre d'où proviennent ces informations en ajoutant une source.",
"editcheck-dialog-addref-reject-question": "Pourquoi n'ajoutez-vous pas une source?",
"editcheck-dialog-addref-reject-description": "Les autres éditeurs aimeraient en savoir plus sur votre décision de ne pas ajouter une source.",
"editcheck-dialog-addref-reject-no-info": "Je n'ai pas ajouté de nouvelles informations",
"editcheck-dialog-addref-reject-already-cited": "Mes modifications sont déjà sourcées plus haut",
"editcheck-dialog-addref-reject-not-sure": "Je ne sais pas quelle source ajouter",
"editcheck-dialog-addref-reject-other": "Autre",
"editcheck-dialog-addref-success-notify": "Merci d'avoir ajouté une source!",
"editcheck-dialog-addref-title": "Ajouter une source",
"editcheck-dialog-title": "Avant de publier"
}

View file

@ -0,0 +1,14 @@
{
"editcheck-dialog-action-no": "Label for the no option when asking users if they want to add a citation.",
"editcheck-dialog-action-yes": "Label for the no option when asking users if they want to add a citation.",
"editcheck-dialog-addref-description": "Help text explaining why it is helpful to add a citation.",
"editcheck-dialog-addref-reject-question": "Heading for form question asking why the user didn't add a citation.",
"editcheck-dialog-addref-reject-description": "Help text for form question asking why the user didn't add a citation.",
"editcheck-dialog-addref-reject-no-info": "Answer option in repsonse to {{msg-mw|editcheck-dialog-addref-reject-question}}",
"editcheck-dialog-addref-reject-already-cited": "Answer option in repsonse to {{msg-mw|editcheck-dialog-addref-reject-question}}",
"editcheck-dialog-addref-reject-not-sure": "Answer option in repsonse to {{msg-mw|editcheck-dialog-addref-reject-question}}",
"editcheck-dialog-addref-reject-other": "Answer option in repsonse to {{msg-mw|editcheck-dialog-addref-reject-question}}",
"editcheck-dialog-addref-success-notify": "Notification messages shown after a citation is added successfully.",
"editcheck-dialog-addref-title": "Title for the edit check context asking user to add a citation.",
"editcheck-dialog-title": "Title shown in the toolbar while the user is in the add a citation workflow."
}

View file

@ -1,20 +1,20 @@
mw.editcheck = {};
/**
* Check if added content in the document model might need a reference
* Find added content in the document model that might need a reference
*
* @param {ve.dm.DocumentModel} documentModel Document model
* @param {boolean} [includeReferencedContent] Include content ranges that already
* have a reference.
* @return {boolean}
* @return {ve.dm.Selection[]} Content ranges that might need a reference
*/
mw.editcheck.doesAddedContentNeedReference = function ( documentModel, includeReferencedContent ) {
mw.editcheck.findAddedContentNeedingReference = function ( documentModel, includeReferencedContent ) {
if ( mw.config.get( 'wgNamespaceNumber' ) !== mw.config.get( 'wgNamespaceIds' )[ '' ] ) {
return false;
return [];
}
if ( !documentModel.completeHistory.getLength() ) {
return false;
return [];
}
var operations;
try {
@ -23,7 +23,7 @@ mw.editcheck.doesAddedContentNeedReference = function ( documentModel, includeRe
// TransactionSquasher can sometimes throw errors; until T333710 is
// fixed just count this as not needing a reference.
mw.errorLogger.logError( err, 'error.visualeditor' );
return false;
return [];
}
var ranges = [];
@ -45,7 +45,7 @@ mw.editcheck.doesAddedContentNeedReference = function ( documentModel, includeRe
// Reached the end of the doc / start of internal list, stop searching
return offset < endOffset;
} );
return ranges.some( function ( range ) {
var addedTextRanges = ranges.filter( function ( range ) {
var minimumCharacters = 50;
// 1. Check that at least minimumCharacters characters have been inserted sequentially
if ( range.getLength() >= minimumCharacters ) {
@ -66,6 +66,10 @@ mw.editcheck.doesAddedContentNeedReference = function ( documentModel, includeRe
}
return false;
} );
return addedTextRanges.map( function ( range ) {
return new ve.dm.LinearSelection( range );
} );
};
/**
@ -118,3 +122,152 @@ if ( mw.config.get( 'wgVisualEditorConfig' ).editCheckTagging ) {
};
} );
}
if ( mw.config.get( 'wgVisualEditorConfig' ).editCheck ) {
mw.hook( 've.preSaveProcess' ).add( function ( saveProcess, target ) {
var surface = target.getSurface();
var selections = mw.editcheck.findAddedContentNeedingReference( surface.getModel().getDocument() );
if ( selections.length ) {
var surfaceView = surface.getView();
var toolbar = target.getToolbar();
var reviewToolbar = new ve.ui.PositionedTargetToolbar( target, target.toolbarConfig );
reviewToolbar.setup( [
{
name: 'back',
type: 'bar',
include: [ 'editCheckBack' ]
},
// Placeholder toolbar groups
// TODO: Make a proper TitleTool?
{
name: 'title',
type: 'bar',
include: []
},
{
name: 'save',
// TODO: MobileArticleTarget should ignore 'align'
align: OO.ui.isMobile() ? 'before' : 'after',
type: 'bar',
include: [ 'showSaveDisabled' ]
}
], surface );
reviewToolbar.items[ 1 ].$element.removeClass( 'oo-ui-toolGroup-empty' );
reviewToolbar.items[ 1 ].$group.append(
$( '<span>' ).addClass( 've-ui-editCheck-toolbar-title' ).text( ve.msg( 'editcheck-dialog-title' ) )
);
if ( OO.ui.isMobile() ) {
reviewToolbar.$element.addClass( 've-init-mw-mobileArticleTarget-toolbar' );
}
target.toolbar.$element.before( reviewToolbar.$element );
target.toolbar = reviewToolbar;
var selection = selections[ 0 ];
var highlightNodes = surfaceView.getDocument().selectNodes( selection.getCoveringRange(), 'branches' ).map( function ( spec ) {
return spec.node;
} );
surfaceView.drawSelections( 'editCheck', [ ve.ce.Selection.static.newFromModel( selection, surfaceView ) ] );
surfaceView.setReviewMode( true, highlightNodes );
toolbar.toggle( false );
target.onContainerScroll();
saveProcess.next( function () {
var saveProcessDeferred = ve.createDeferred();
var fragment = surface.getModel().getFragment( selection, true );
var context = surface.getContext();
// Select the found content to correctly the context on desktop
fragment.select();
// Deactivate to prevent selection suppressing mobile context
surface.getView().deactivate();
context.addPersistentSource( {
embeddable: false,
data: {
fragment: fragment,
saveProcessDeferred: saveProcessDeferred
},
name: 'editCheck'
} );
// Once the context is positioned, clear the selection
setTimeout( function () {
surface.getModel().setNullSelection();
} );
return saveProcessDeferred.promise().then( function ( data ) {
context.removePersistentSource( 'editCheck' );
surfaceView.drawSelections( 'editCheck', [] );
surfaceView.setReviewMode( false );
reviewToolbar.$element.remove();
toolbar.toggle( true );
target.toolbar = toolbar;
target.onContainerScroll();
// Check the user inserted a citation
if ( data && data.action ) {
if ( data.action !== 'reject' ) {
mw.notify( ve.msg( 'editcheck-dialog-addref-success-notify' ), { type: 'success' } );
}
var delay = ve.createDeferred();
// If they inserted, wait 2 seconds on desktop before showing save dialog
setTimeout( function () {
delay.resolve();
}, !OO.ui.isMobile() && data.action !== 'reject' ? 2000 : 0 );
return delay.promise();
} else {
return ve.createDeferred().reject().promise();
}
} );
} );
}
} );
}
ve.ui.EditCheckBack = function VeUiEditCheckBack() {
// Parent constructor
ve.ui.EditCheckBack.super.apply( this, arguments );
this.setDisabled( false );
};
OO.inheritClass( ve.ui.EditCheckBack, ve.ui.Tool );
ve.ui.EditCheckBack.static.name = 'editCheckBack';
ve.ui.EditCheckBack.static.icon = 'previous';
ve.ui.EditCheckBack.static.autoAddToCatchall = false;
ve.ui.EditCheckBack.static.autoAddToGroup = false;
ve.ui.EditCheckBack.static.title =
OO.ui.deferMsg( 'visualeditor-backbutton-tooltip' );
ve.ui.EditCheckBack.prototype.onSelect = function () {
var context = this.toolbar.getSurface().getContext();
if ( context.inspector ) {
context.inspector.close();
} else {
context.items[ 0 ].close();
}
this.setActive( false );
};
ve.ui.EditCheckBack.prototype.onUpdateState = function () {
this.setDisabled( false );
};
ve.ui.toolFactory.register( ve.ui.EditCheckBack );
ve.ui.EditCheckSaveDisabled = function VeUiEditCheckSaveDisabled() {
// Parent constructor
ve.ui.EditCheckSaveDisabled.super.apply( this, arguments );
};
OO.inheritClass( ve.ui.EditCheckSaveDisabled, ve.ui.MWSaveTool );
ve.ui.EditCheckSaveDisabled.static.name = 'showSaveDisabled';
ve.ui.EditCheckSaveDisabled.static.autoAddToCatchall = false;
ve.ui.EditCheckSaveDisabled.static.autoAddToGroup = false;
ve.ui.EditCheckSaveDisabled.prototype.onUpdateState = function () {
this.setDisabled( true );
};
ve.ui.toolFactory.register( ve.ui.EditCheckSaveDisabled );

View file

@ -1548,11 +1548,11 @@ ve.init.mw.ArticleTarget.prototype.save = function ( doc, options ) {
) {
var documentModel = this.getSurface().getModel().getDocument();
// New content needing a reference
if ( mw.editcheck.doesAddedContentNeedReference( documentModel ) ) {
if ( mw.editcheck.findAddedContentNeedingReference( documentModel ).length ) {
taglist.push( 'editcheck-references' );
}
// New content, regardless of if it needs a reference
if ( mw.editcheck.doesAddedContentNeedReference( documentModel, true ) ) {
if ( mw.editcheck.findAddedContentNeedingReference( documentModel, true ).length ) {
taglist.push( 'editcheck-newcontent' );
}
}

View file

@ -37,7 +37,7 @@
modules.push( 'ext.visualEditor.mwwikitext' );
}
if ( conf.editCheckTagging ) {
if ( conf.editCheckTagging || conf.editCheck ) {
modules.push( 'ext.visualEditor.editCheck' );
}