// <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 = /§ion=(\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>