User:DannyS712/EFFPRH/sandbox.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
// Script to respond to edit filter false positive reports
// @author DannyS712
$(() => {
const EFFPRH = {};
window.EFFPRH = EFFPRH;

EFFPRH.config = {
	debug: false,
	version: '0-dev'
};
EFFPRH.editSummary = 'Respond to false positive report via [[User:DannyS712/EFFPRH]]'
	+ ' (v ' + EFFPRH.config.version + ')';

EFFPRH.init = function () {
	mw.loader.using(
		[ 'vue', '@wikimedia/codex', 'mediawiki.util', 'mediawiki.api' ],
		EFFPRH.run
	);
};

EFFPRH.run = function () {
	EFFPRH.addStyle();
	// Add links to each section to open a dialog
	$('span.mw-headline').each( function () {
		const $editSectionLinks = $( this ).parent().find( '.mw-editsection' );
		if ( $editSectionLinks.length === 0 ) {
			// Missing links span, nothing to do
			return;
		}
		const sectionNum = EFFPRH.getHeadingSectionNum( $editSectionLinks );
		if ( sectionNum === -1 ) {
			// Missing link, no idea what section this is
			return;
		}
		// Add a hidden div after the headline that will be where the Vue
		// display goes
		$( this ).parent().after(
			$( '<div>' ).attr( 'id', 'script-EFFPRH-' + sectionNum )
		);
		const reporterName = $( this ).text();
		EFFPRH.addHandlerLink( $editSectionLinks, reporterName, sectionNum );
	} );
};

/**
 * Add styles for our interface.
 */
EFFPRH.addStyle = function () {
	mw.util.addCSS(`
		.script-EFFPRH-handler {
			background-color: #e0e0e0;
			border: 1px solid black;
			margin: 10px 0 10px 0;
		}
		/* Override normal rules for indenting lists */
		.cdx-menu ul {
			margin-left: 0px;
		}
		/* Separate the dropdown and input */
		.cdx-menu {
			margin-bottom: 10px;
		}
		/* Reduce vertical space in the dropdown options */
		.cdx-menu-item__content {
			line-height: 1em;
		}
		/* Center form elements and labels */
		.script-EFFPRH-handler td {
			vertical-align: middle;
		}
		/* Don't use the grey background in the preview */
		.script-EFFPRH-preview {
			background-color: white;
		}
	`);
};

/**
 * Get the section number for a response, given the jQuery element for the
 * <span> with the edit section link. Returns -1 on failure.
 */
EFFPRH.getHeadingSectionNum = function ( $editSectionLinks ) {
	const editSectionUrl = $editSectionLinks.find( 'a:first' ).attr( 'href' );
	if ( editSectionUrl === undefined ) {
		return -1;
	}
	const sectionMatch = editSectionUrl.match( /&section=(\d+)(?:$|&)/ );
	if ( sectionMatch === null ) {
		return -1;
	}
	return parseInt( sectionMatch[1] );
};

/**
 * Add a link next to the edit section link that will launch the report handler.
 */
EFFPRH.addHandlerLink = function ( $editSectionLinks, reporterName, sectionNum ) {
	const $handlerLink = $( '<a>' )
		.attr( 'id', 'script-EFFPRH-launch-' + sectionNum )
		.text( 'Review report' );
	$handlerLink.click(
		function () {
			// Only allow running once per link (until the Vue handler is removed)
			if ( $( this ).hasClass( 'script-EFFPRH-disabled' ) ) {
				return;
			}
			$( this ).addClass( 'script-EFFPRH-disabled' );
			EFFPRH.showHandler( reporterName, sectionNum );
		}
	);
	// Add before the closing ] of the links
	$editSectionLinks.children().last().before(
		' | ',
		$handlerLink
	);
};

// Handler options, see {{EFFP}}
EFFPRH.responseOptions = [
	{ value: 'none', label: 'None' },
	{ value: 'done', label: 'Done (no change to filter)' },
	{ value: 'defm', label: 'Done (may need a change to filter)' },
	{ value: 'notdone', label: 'Not Done (filter working properly)' },
	{ value: 'ndefm', label: 'Not Done (may need a change to filter)' },
	{ value: 'redlink', label: 'Not Done (notable people)' },
	{ value: 'alreadydone', label: 'Already Done' },
	{ value: 'denied', label: 'Decline (edits are vandalism)' },
	{ value: 'checking', label: 'Checking' },
	{ value: 'blocked', label: 'User blocked' },
	{ value: 'talk', label: 'Request on article talk page' },
	{ value: 'fixed', label: 'Fixed filter' },
	{ value: 'question', label: 'Question' },
	{ value: 'note', label: 'Note' },
	{ value: 'private', label: 'Private filter' },
	{ value: 'pin', label: 'Pin' },
	{ value: 'moot', label: 'Moot (filter working properly)' },
	{ value: 'mootefm', label: 'Moot (may need a change to filter)' }
];

/**
 * Actually show the handler for a given reporter name and section number.
 */
EFFPRH.showHandler = function ( reporterName, sectionNum ) {
	const targetDivId = 'script-EFFPRH-' + sectionNum;
	// Need a reference so that it can be unmounted
	let vueAppInstance;
	// We shouldn't use the mw.loader access directly, but I'm not
	// pasing around the `require` function everywhere
	const cdx = mw.loader.require( '@wikimedia/codex' );
	// Extra component to render wikitext preview
	const previewRenderer = EFFPRH.getPreviewComponent();
	const handlerApp = {
		components: {
			CdxButton: cdx.CdxButton,
			CdxSelect: cdx.CdxSelect,
			CdxTextInput: cdx.CdxTextInput,
			CdxToggleButton: cdx.CdxToggleButton,
			previewRenderer: previewRenderer
		},
		data: function () {
			return {
				reporterName: reporterName,
				sectionNum: sectionNum,
				responseOptions: EFFPRH.responseOptions,
				selectedResponse: 'none',
				commentValue: '',

				// Debug information of the state
				showDebug: EFFPRH.config.debug,

				// Preview
				showPreview: false,

				// Overall state
				haveSubmitted: false,
				editMade: false,
				editError: false
			};
		},
		computed: {
			canSubmit: function () {
				return !this.haveSubmitted && this.selectedResponse !== 'none';
			},
			previewToggleLabel: function () {
				return ( this.showPreview ? 'Hide preview' : 'Show preview' );
			},
			responseWikiText: function () {
				// Computed here so that we can use it for the api preview,
				// does not include the leading newline
				let responseText = ': {{EFFP|' + this.selectedResponse + '}}';
				if ( this.commentValue ) {
					responseText += ' ' + this.commentValue;
				}
				responseText += ' --~~~~';
				return responseText;
			}
		},
		methods: {
			reloadPage: function () {
				// Needs to be a function instead of using href so that we
				// can force the page to reload
				location.assign(
					mw.util.getUrl( mw.config.get( 'wgPageName' ) + '#' + this.reporterName )
				);
				location.reload();
			},
			submitHandler: function () {
				this.haveSubmitted = true;
				EFFPRH.respondToReport(
					this.reporterName,
					this.sectionNum,
					this.responseWikiText
				).then(
					// arrow functions to simplify `this`
					() => this.editMade = true,
					() => this.editError = true
				);
			},
			cancelHandler: function () {
				if ( vueAppInstance === undefined ) {
					console.log( 'Cannot unmount, no vueAppInstance' );
				} else {
					vueAppInstance.unmount();
					// Restore link
					$( '#script-EFFPRH-launch-' + sectionNum ).removeClass(
						'script-EFFPRH-disabled'
					);
				}
			}
		},
		template: `
<div class="script-EFFPRH-handler">
<p>Responding to report by {{ reporterName }}.</p>
<p v-if="showDebug">Section {{ sectionNum }}, selected response: {{ selectedResponse }}, comment: {{ commentValue }}.</p>
<!-- Table so that we can align the labels and fields -->
<table><tbody>
<tr>
	<td><span>Action:</span></td>
	<td><cdx-select v-model:selected="selectedResponse" :menu-items="responseOptions" default-label="Response to report" :disabled="haveSubmitted" /></td>
</tr>
<tr>
	<td><span>Comment:</span></td>
	<td><cdx-text-input v-model="commentValue" :disabled="haveSubmitted" /></td>
</tr>
</tbody></table>
<br />
<ul v-show="haveSubmitted">
<li>Submitting...</li>
<li v-show="editMade">Success! <a v-on:click="reloadPage"><strong>Reload the page</strong></a></li>
<li v-show="editError">Uh-oh, something went wrong. Please check the console for details.</li>
</ul>
<cdx-button weight="primary" action="progressive" :disabled="!canSubmit" v-on:click="submitHandler">Submit</cdx-button>
<cdx-button weight="primary" action="destructive" :disabled="haveSubmitted" v-on:click="cancelHandler">Cancel</cdx-button>
<cdx-toggle-button v-model="showPreview" :disabled="!canSubmit">{{ previewToggleLabel }}</cdx-toggle-button>
<!-- v-if so that we don't call the api to parse and render a preview when its not needed, do not render with no response template chosen -->
<preview-renderer v-if="showPreview && canSubmit" :wikitext="responseWikiText"></preview-renderer>
</div>`
	};
	vueAppInstance = Vue.createMwApp( handlerApp );
	vueAppInstance.mount( '#' + targetDivId );
};

/**
 * Extra component: preview of wikitext being added.
 */
EFFPRH.getPreviewComponent = function () {
	return {
		props: {
			wikitext: { type: String, default: '' }
		},
		data: function () {
			return {
				previewHtml: '',
				haveHtml: false
			};
		},
		methods: {
			// Separate from the watcher so that can be called on mounted too
			loadPreview: function ( wikitextToPreview ) {
				new mw.Api().get( {
					action: 'parse',
					formatversion: 2,
					title: mw.config.get( 'wgPageName' ),
					text: wikitextToPreview,
					prop: 'text|wikitext',
					pst: true,
					disablelimitreport: true,
					disableeditsection: true,
					sectionpreview: true
				} ).then(
					( res ) => {
						console.log( res );
						if ( res
							&& res.parse
							&& res.parse.wikitext === this.wikitext
							&& res.parse.text
						) {
							this.previewHtml = res.parse.text;
							this.haveHtml = true;
						}
					}
				);
			}
		},
		watch: {
			wikitext: function ( newValue ) {
				// Reset when the wikitext to preview changes
				this.previewHtml = '';
				this.haveHtml = false;
				this.loadPreview( newValue );
			}
		},
		mounted: function () {
			// Preview starting wikitext
			this.loadPreview( this.wikitext );
		},
		template: `
<div class="script-EFFPRH-preview">
<hr>
<div v-if="haveHtml" v-html="previewHtml"></div>
<div v-else>Loading preview of {{ wikitext }}</div>
</div>`
	};
};

/**
 * Actually make the page edit to respond to the report. Returns a promise
 * for the edit succeeding or not.
 */
EFFPRH.respondToReport = function (
	reporterName,
	sectionNum,
	responseWikiText
) {
	return new Promise( function ( resolve, reject ) {
		// wikitext is computed in Vue app so that it can have a preview too,
		// we just need to add the leading newline
		const wikitextToAdd = '\n' + responseWikiText;
		const editParams = {
			action: 'edit',
			title: mw.config.get( 'wgPageName' ),
			section: sectionNum,
			summary: '/* ' + reporterName + ' */ ' + EFFPRH.editSummary,
			notminor: true,
			baserevid: mw.config.get( 'wgCurRevisionId' ),
			nocreate: true,
			appendtext: wikitextToAdd,
			assert: 'user',
			assertuser: mw.config.get( 'wgUserName' )
		};
		if ( EFFPRH.config.debug ) {
			console.log( { ...editParams } );
		}
		new mw.Api().postWithEditToken( editParams )
			.then(
				( res ) => { console.log( res ); resolve(); },
				( err ) => { console.log( err ); reject(); }
			);
	} );
};

});

$( document ).ready( () => {
	if (
		mw.config.get( 'wgPageName' ) === 'Wikipedia:Edit_filter/False_positives/Reports'
		|| mw.config.get( 'wgPageName' ) === 'User:DannyS712/EFFPRH/sandbox'
	) {
		window.EFFPRH.init();
	}
});

// </nowiki>