User:DaxServer/DiscussionCloser-new.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>
//
// DiscussionCloser
//
// Credits to DannyS712, Equazcion, Evad37, and Abelmoschus Esculentus
//            User:DannyS712/DiscussionCloser.js and further upstreams
//
// Maintained by DaxServer
//
// A Codex implementation
//         https://doc.wikimedia.org/codex/latest/
//
mw.loader.using(
	['mediawiki.util', 'mediawiki.api', 'mediawiki.Title', 'mediawiki.Uri', '@wikimedia/codex', 'vue', ],
).then( function( require ) {
	// Allowed on these pages
	const allowedPages = [
		'Wikipedia:In the news/Candidates',
	].map(page => page.replace(/ /g, '_'));

	// Allowed on these pages whose title starts with
	const allowedPagesStartWith = [
		'Wikipedia:Requests for adminship/'
	].map(page => page.replace(/ /g, '_'));
	const currentPage = mw.config.get('wgPageName');

	// Execute only if the page is valid for discussions
	if (mw.config.get('wgAction') !== 'view') {
		return;
	}

	if ($('#ca-addsection').length === 0
		&& !allowedPages.includes(currentPage)
		&& !allowedPagesStartWith.some(page => currentPage.startsWith(page))
	) {
		return;
	}

	const Vue = require( 'vue' );
	const Codex = require( '@wikimedia/codex' );
	const mountPoint = document.body.appendChild( document.createElement( 'div' ) );
	const api = new mw.Api();
	const uri = new mw.Uri();

	const config = {
		name: '[[User:DaxServer/DiscussionCloser|DiscussionCloser]]',
		version: '0.3.0',
	};
	const attribution = `(${config.name} ${config.version})`;
	const defaultEditSummary = 'Closing discussion';

	const app = Vue.createMwApp( {
			data: function() {
				return {
					section: 0,
					showDialog: false,
					radios: [
						{
							label: 'Generic (blue)',
							value: 'atop',
						},
						{
							label: 'Generic (green)',
							value: 'atopg',
						},
						{
							label: 'Generic (yellow)',
							value: 'atopy',
						},
						{
							label: 'Generic (red)',
							value: 'atopr',
						},
						{
							label: 'RfC',
							value: 'closed rfc top',
						},
						{
							label: 'Hidden archive',
							value: 'hat',
						},
						{
							label: 'Discussion',
							value: 'dtop',
						}
					],
					radioValue: Vue.ref( 'atop' ),
					status: '',
					statusEnabled: true,
					nac: true,
					comment: '',
					editSummary: '',
					preview: {
						message: {
							type: 'notice',
							text: 'This is a preview of the section after closure',
						},
						html: null,
					},
					inProgress: false,
					alreadyClosed: false,
					footer: {
						text: null,
					},
				};
			},
			template: `
<template>
	<cdx-dialog
		v-model:open="showDialog"
		title="Discussion Closer"
		class="disc-closer-dialog"
		close-button-label="Close"
		:show-dividers="true"
		:primary-action="primaryAction"
		:default-action="defaultAction"
		@update:open="resetApp"
		@default="onDefaultActionClicked"
		@primary="closeDiscussion"
		:primary-action-disabled="inProgress"
		:default-action-disabled="inProgress"
	>
		<cdx-progress-bar v-if="inProgress" aria-label="Indeterminate progress bar" />
		<div class="disc-closer-div" v-if="preview.html">
			<cdx-message :type="preview.message.type">{{ preview.message.text }}</cdx-message>
			<div v-html="preview.html"></div>
			<div class="disc-closer-div" v-if="footer.text">
				<cdx-message type="error">{{ footer.text }}</cdx-message>
			</div>
		</div>
		<div class="disc-closer-div" v-else>
			<cdx-field :is-fieldset="true" :hide-label="true">
				<cdx-radio
					v-for="radio in radios"
					:key="'radio-' + radio.value"
					v-model="radioValue"
					name="inline-radios"
					:input-value="radio.value"
					:inline="true"
					@update:model-value="onRadioValueUpdate"
				>
					{{ radio.label }}
				</cdx-radio>
				<template #help-text>
					The discussion will be closed using the <a :href="templateLink"><span v-pre>{{</span>{{ radioValue }}<span v-pre>}}</span></a> template
				</template>
			</cdx-field>
			<cdx-field optionalFlag="(optional)">
				<template #label>Status</template>
				<cdx-text-input v-model="status" :disabled="!statusEnabled"></cdx-text-input>
			</cdx-field>
			<cdx-field>
				<template #label>Closing comment</template>
				<cdx-text-area v-model="comment" :rows="5"></cdx-text-area>
				<template #help-text>Signature is automatically added</template>
			</cdx-field>
			<cdx-field v-if="shouldAddNac">
				<template #label>Non-administrator closure</template>
				<cdx-checkbox v-model="nac">Add <span v-pre>{{subst:nac}}</span> before signature</cdx-checkbox>
				<template #help-text>Since you are not an administrator, <a title="Template:Non-admin closure" href="https://en.wikipedia.org/wiki/Template:Non-admin_closure"><span v-pre>{{subst:nac}}</span></a> will be added before signature, if a closing comment is added.</template>
			</cdx-field>
			<cdx-field optionalFlag="(optional)">
				<template #label>Edit summary</template>
				<cdx-text-input v-model="editSummary"></cdx-text-input>
				<template #help-text>"${defaultEditSummary}", if left blank.</template>
			</cdx-field>
			<div class="disc-closer-div" v-if="footer.text">
				<cdx-message type="error">{{ footer.text }}</cdx-message>
			</div>
		</div>
	</cdx-dialog>
</template>`,
			computed: {
				primaryAction() {
					return {
						label: this.alreadyClosed ? 'Close discussion again' : 'Close discussion',
						actionType: this.alreadyClosed ? 'destructive' : 'progressive',
						disabled: this.inProgress,
					};
				},
				defaultAction() {
					return {
						label: this.preview.html === null ? 'Preview' : 'Modify inputs',
						actionType: 'progressive',
						disabled: this.inProgress,
					};
				},
				shouldAddNac() {
					return !mw.config.get('wgUserGroups').includes('sysop');
				},
				templateLink() {
					return `https://en.wikipedia.org/wiki/Template:${this.radioValue}`;
				},
			},
			methods: {
				resetApp(...args) {
					Object.assign(this.$data, this.$options.data.apply(this));
				},
				openDialog(section) {
					this.showDialog = true;
					this.section = section;
				},
				onRadioValueUpdate(value) {
					this.statusEnabled = !(value === 'closed rfc top' || value === 'hat' || value === 'subst:RMT' || value === 'dtop');
				},
				_closeDiscussionAgain() {
					this.alreadyClosed = true;
					this.footer.text = 'The discussion seem to have already been closed. Would you like to close again?';
					this.inProgress = false;
				},
				async onDefaultActionClicked() {
					if (this.preview.html === null) {
						await this.renderPreview();
					} else {
						this.preview.html = null;
					}
				},
				async closeDiscussion() {
					this.inProgress = true;

					let editSummary = this.editSummary.trim().length > 0 ? this.editSummary.trim() : defaultEditSummary;
					editSummary = `${editSummary} ${attribution}`;
					const { content, sectiontitle, wikitext } = await this._getWikiText();

					if (!this.alreadyClosed && this._alreadyClosed(content)) {
						// Toggle close discussion to destructive
						this._closeDiscussionAgain();
						return;
					}

					api.post({
						action: 'edit',
						section: this.section,
						title: mw.config.get('wgPageName'),
						text: wikitext,
						summary: `/* ${sectiontitle} */ ${editSummary}`,
						token: mw.user.tokens.get('csrfToken'),
					}).done(function() {
						this.showDialog = false;
						uriReload = uri.clone();
						uriReload.fragment = sectiontitle.replace(/\s/g, '_');
						window.location.href = uriReload.toString();
						window.location.reload();
					});
				},
				async renderPreview() {
					this.inProgress = true;
					this.preview.html = null;

					const { content, wikitext } = await this._getWikiText();

					const preview = await api.post({
						format: 'json',
						action: 'parse',
						pst: 1,
						text: wikitext,
						title: mw.config.get('wgPageName'),
						prop: 'text',
					});

					this.preview.html = preview.parse.text['*'];
					this.inProgress = false;

					if (this._alreadyClosed(content)) {
						this._closeDiscussionAgain();
					}
				},
				async _getWikiText() {
					const response = await api.get({
						action: 'query',
						titles: mw.config.get('wgPageName'),
						rvsection: this.section,
						prop: 'revisions|info',
						rvslots: 'main',
						rvprop: 'content',
						formatversion: 2,
					});

					const top = this._make_top();
					const bottom = this._make_bottom();
					const content = response.query.pages[0].revisions[0].slots.main.content;
					const discussiontext = content.substring(content.indexOf('\n'));
					const title = content.substring(0, content.indexOf('\n'));
					const sectiontitle = title.replace(/=/g, '').trim();

					const wikitext = title + '\n' + top + discussiontext + '\n' + bottom;

					return {
						content,
						sectiontitle,
						wikitext,
					};
				},
				_alreadyClosed(content) {
					content = content.toLowerCase();
					return content.includes('{{atop') ||
						content.includes('{{archive') ||
						content.includes('{{dtop') ||
						content.includes('{{discussion top') ||
						content.includes('{{hat') ||
						content.includes('{{hidden archive top') ||
						content.includes('{{crt') ||
						content.includes('{{rfctop') ||
						content.includes('{{closed rfc') ||
						content.includes('{{ptop') ||
						content.includes('{{polltop') ||
						content.includes('{{poll top') ||
						content.includes('<!-- template:rm top -->')
					;
				},
				_make_top() {
					const comment = this.comment.trim().replace(/\s*{{(\s*subst\s*:\s*)?(Non-admin closure|nac)\s*}}\s*(~~~~)?$/i, '');
					const nac = this.nac ? ' {{subst:nac}}' : '';
					const status = this.statusEnabled && this.status.trim().length > 0 ? this.status.trim() : '';

					let args = {};
					switch (this.radioValue) {
						case 'atop':
						case 'atopg':
						case 'atopr':
						case 'atopy':
							if (status.length > 0) {
								args.status = status;
							}
							if (comment.length > 0) {
								args.result = `${comment}${nac} ~~~~`;
							}
							break;
						case 'dtop':
							if (comment.length > 0) {
								args.result = `${comment}${nac} ~~~~`;
							}
							break;
						case 'hat':
							if (comment.length > 0) {
								args.result = this.comment;
							}
							args.closer = mw.config.get('wgUserName');
							break;
						case 'closed rfc top':
							if (comment.length > 0) {
								args.result = `${comment}${nac} ~~~~`;
							}
							break;
						default:
							if (status.length > 0) {
								args.status = status;
							}
							if (comment.length > 0) {
								args.result = `${comment}${nac} ~~~~`;
							}
					}

					let arg = Object.keys(args).map((key) => `| ${key} = ${args[key]}`).join(`\n`);
					arg = arg.length > 0 ? `\n${arg}\n` : '';

					return `{{${this.radioValue}${arg}}}`.replace(/\n+/g, `\n`);
				},
				_make_bottom() {
					let bottom;
					switch (this.radioValue) {
						case 'atop':
						case 'atopr':
						case 'atopy':
						case 'atopg':
							bottom = 'abot';
							break;
						case 'dtop':
							bottom = 'dbot';
							break;
						case 'hat':
							bottom = 'hab';
							break;
						case 'closed rfc top':
							bottom = 'closed rfc bottom';
							break;
						default:
							bottom = 'abot';
					}
					return `{{${bottom}}}`;
				},
			},
		})
		.component( 'cdx-button', Codex.CdxButton )
		.component( 'cdx-checkbox', Codex.CdxCheckbox )
		.component( 'cdx-dialog', Codex.CdxDialog )
		.component( 'cdx-field', Codex.CdxField )
		.component( 'cdx-message', Codex.CdxMessage )
		.component( 'cdx-progress-bar', Codex.CdxProgressBar )
		.component( 'cdx-radio', Codex.CdxRadio )
		.component( 'cdx-text-input', Codex.CdxTextInput )
		.component( 'cdx-text-area', Codex.CdxTextArea )
		.mount(mountPoint)
	;

	const styles = `
.DC-close-widget {
	display: inline-block;
	float: right;
	font-weight: normal;
	font-size: 0.8rem;

	.mw-editsection-divider {
		margin: 0 0.2rem;
		display: inline;
	}
}

.disc-closer-dialog {
	max-width: 75%;
}

.disc-closer-div {
	margin-top: 16px;
}
`;
	mw.loader.addStyleTag(styles);

	$('div:not(.mw-archivedtalk) > div.mw-heading').each(function(index, value) {
		const editSectionUrl = $(this).find('.mw-editsection a:first').attr('href');
		const sectionRaw = /&section=(\d+)/.exec(editSectionUrl);
		if (sectionRaw === null || typeof sectionRaw[1] === 'undefined') {
			return;
		}
		const sectionHeadingRaw = /mw-heading mw-heading(\d)/.exec($(this).attr('class'));
		if (sectionHeadingRaw === null || typeof sectionHeadingRaw[1] === 'undefined' || ![2, 3, 4].includes(parseInt(sectionHeadingRaw[1]))) {
			return;
		}
		const section = parseInt(sectionRaw[1]);
		if (isNaN(section)) {
			return;
		}
		const uriFragment = $(this).find('h'+section).attr('id');
		$(this).append('<div class="DC-close-widget"><span class="mw-editsection-divider">|</span><a class="DC-closeLink">Close</a></div>');
		$(this).find('a.DC-closeLink').click(function() {
			app.openDialog(section);
		});
	});
} );
// </nowiki>