Jump to content

User:AramilFeraxa/ReportSolver.js

From Meta, a Wikimedia project coordination wiki
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// <nowiki>
$(function () {
    const RS = {};
    const RSConfig = window.RSConfig || { allowedPages: [] };
    window.ReportSolver = RS;

    const wgPageName = mw.config.get('wgPageName');

    RS.summary = ' - with [[User:AramilFeraxa/ReportSolver|ReportSolver]]';

    const MSG = {
        dialogTitle: 'Close with a comment',
        dialogCancel: 'Cancel',
        dialogConfirm: 'Submit',
        dialogInfo: 'Template and signature will be inserted automatically.'
    };

    // Page-specific action sets. Some request pages require different templates,
    // labels or status values than the generic Steward request workflow.
    const pageConfigs = {
        'Meta:Requests_for_deletion': [
            { class: 'deleted', label: 'Mark as deleted', template: '{{Icon|delete|Deleted}}', summary: 'Deleted', status: 'deleted' },
            { class: 'kept', label: 'kept', template: '{{Icon|keep|Kept}}', summary: 'Kept', status: 'kept' },
            { class: 'redirected', label: 'redirected', template: '{{Icon|redirect|Redirected}}', summary: 'Redirected', status: 'redirected' },
            { class: 'close', label: 'Close discussion', template: '{{section resolved|1=~~~~}}', summary: 'Closed', status: '' }
        ],
        'Steward_requests/Permissions': [
            { class: 'grant', label: 'Mark as granted', template: '', summary: 'Granted temporary permissions', status: 'done' },
            { class: 'done', label: 'done', template: '{{done}}', summary: 'Marked as done', status: 'done' },
            { class: 'rejected', label: 'not done', template: '{{notdone}}', summary: 'Marked as not done', status: 'not done' },
            { class: 'on-hold', label: 'on hold', template: '{{onhold}}', summary: 'Marked as on hold', status: 'onhold' }
        ]
    };

    const defaultConfig = [
        { class: 'done', label: 'Mark as done', template: '{{done}}', summary: 'Marked as done', status: 'done' },
        { class: 'rejected', label: 'not done', template: '{{notdone}}', summary: 'Marked as not done', status: 'not done' },
        { class: 'already-done', label: 'already done', template: '{{already done}}', summary: 'Marked as already done', status: 'already done' },
        { class: 'on-hold', label: 'on hold', template: '{{onhold}}', summary: 'Marked as on hold', status: 'onhold' },
        { class: 'stale', label: 'stale', template: '{{stale}}', summary: 'Marked as stale', status: 'not done' },
        { class: 'withdrawn', label: 'withdrawn', template: '{{withdrawn}}', summary: 'Marked as withdrawn', status: 'withdrawn' },
        { class: 'close', label: 'Close discussion', template: '{{section resolved|1=~~~~}}', summary: 'Closed', status: '' }
    ];

    mw.util.addCSS(`
        .mw-editsection .ReportSolver-buttons a::before,
        .mw-editsection .ReportSolver-buttons a::after {
            content: none !important;
            display: none !important;
        }
    `);

    RS.setup = function () {

        // Enable the default action set only on request pages where the workflow is known
        // to be compatible. Additional pages may be explicitly allowed through RSConfig.
        const useDefault = (
            wgPageName === 'Meta:Requests_for_help_from_a_sysop_or_bureaucrat' ||
            wgPageName.startsWith('Steward_requests/Global') ||
            wgPageName.startsWith('Steward_requests/Miscellaneous') ||
			wgPageName.startsWith('Steward_requests/Bot_status') ||
            wgPageName.startsWith("Stewards'_noticeboard") ||
            RSConfig.allowedPages.includes(wgPageName) ||
            [1, 4, 5, 15, 11, 9].includes(mw.config.get('wgNamespaceNumber'))
        );

        let config = pageConfigs[wgPageName] || (useDefault ? defaultConfig : []);

        // Global permissions requests have a custom closing format, so the generic
        // "Close discussion" action is intentionally hidden there.
        if (wgPageName === 'Steward_requests/Global_permissions') {
            config = config.filter(option => option.class !== 'close');
        }

        $('span.mw-editsection-bracket:first-child').each(function () {
            try {
                const sectionNumber = $(this).siblings('a').attr('href').match(/section=(\d+)/)[1];

                const $buttons = $('<span>', {
                    class: 'ReportSolver-buttons'
                });

                config.forEach((option, index) => {
                    if (index > 0) {
                        $buttons.append(document.createTextNode(' | '));
                    }

                    $buttons.append($('<a>', {
                        href: 'javascript:void(0)',
                        class: `ReportSolver-mark-${option.class}`,
                        'data-section': sectionNumber,
                        text: option.label
                    }));

                    if (!['close', 'grant'].includes(option.class)) {
                        $buttons.append(document.createTextNode(' '));

                        $buttons.append($('<a>', {
                            href: 'javascript:void(0)',
                            class: `ReportSolver-edit-${option.class}`,
                            'data-section': sectionNumber,
                            text: '(C) '
                        }));
                    }
                });

                $(this).after($buttons);
            } catch (e) {
                console.error(e);
            }
        });

        config.forEach(option => {
            $(`a.ReportSolver-mark-${option.class}`).click(function () {
                const sectionNumber = $(this).data('section');
                $(this).text('Processing...');

                if (option.class === 'grant') {
                    RS.openGrantDialog(sectionNumber);
                } else {
                    RS.doEdit(sectionNumber, option.template, option.summary, option.status);
                }
            });
        });

        RS.handleEditButtonClick();
    };

    RS.handleEditButtonClick = function () {
        $(document).off('click', '[class^="ReportSolver-edit-"]').on('click', '[class^="ReportSolver-edit-"]', function () {
            const sectionNumber = $(this).data('section');
            const action = $(this).attr('class').split('ReportSolver-edit-')[1];

            // The comment button may be rendered from either the default or page-specific
            // configuration, so both sources are searched here.
            const config = [...defaultConfig, ...(pageConfigs[wgPageName] || [])].find(c => c.class === action);

            RS.openDialog(sectionNumber, config);
        });
    };

    RS.openDialog = function (sectionNumber, config) {
        function ReportSolverDialog(config) {
            ReportSolverDialog.super.call(this, config);
        }

        OO.inheritClass(ReportSolverDialog, OO.ui.ProcessDialog);

        ReportSolverDialog.static.name = "ReportSolverDialog";
        ReportSolverDialog.static.title = MSG.dialogTitle;
        ReportSolverDialog.static.actions = [
            { label: MSG.dialogCancel, flags: "safe" },
            { action: 'submit', label: MSG.dialogConfirm, flags: ["primary", "progressive"] }
        ];

        ReportSolverDialog.prototype.initialize = function () {
            ReportSolverDialog.super.prototype.initialize.call(this);

            var rootPanel = new OO.ui.PanelLayout({ padded: true, expanded: false });

            var commentInput = new OO.ui.MultilineTextInputWidget({ rows: 5, placeholder: 'Enter a comment...' });
            var selectedOptionLabel = new OO.ui.LabelWidget({ label: "Chosen option:", classes: ['centered-label'] });
            var editSummaryElement = $("<span>", { html: config.summary, class: "bold-text " + config.class });
            var infoElement = $("<span>", { html: MSG.dialogInfo, class: "red-text" });

            rootPanel.$element.append(commentInput.$element, infoElement, selectedOptionLabel.$element, editSummaryElement);

            this.commentInput = commentInput;
            this.$body.append(rootPanel.$element);

            // Dialog-only styles are injected here to keep the gadget self-contained
            // and avoid requiring a separate CSS page for a small UI surface.
            const styles = [
                { class: "centered-label", css: "display: flex; align-items: center; justify-content: center; margin: 10px;" },
                { class: "bold-text", css: "display: flex; align-items: center; justify-content: center; font-weight: bold; margin: 10px;" },
                { class: "red-text", css: "display: flex; color: lightblue; align-items: center; justify-content: center; font-weight: bold; margin: 10px;" },
                { class: "done", css: "color: green;" },
                { class: "rejected", css: "color: red;" },
                { class: "already-done", css: "color: green;" },
                { class: "on-hold", css: "color: blue;" },
                { class: "stale", css: "color: orange;" },
                { class: "withdrawn", css: "color: purple;" },
                { class: "deleted", css: "color: red;" },
                { class: "kept", css: "color: green;" },
                { class: "redirected", css: "color: orange;" },
                { class: "close", css: "color: gray;" }
            ];

            styles.forEach(style => mw.util.addCSS(`.${style.class} { ${style.css} font-weight: bold; margin: 10px; }`));
        };

        ReportSolverDialog.prototype.getActionProcess = function (action) {
            var dialog = this;

            if (action === 'submit') {
                dialog.actions.setAbilities({ submit: false, cancel: false });
                dialog.pushPending();

                var comment = dialog.commentInput.getValue();
                var wikitext = config.template + (comment ? ' ' + comment : '');

                RS.doEdit(sectionNumber, wikitext, config.summary, config.status);
            }

            return ReportSolverDialog.super.prototype.getActionProcess.call(this, action);
        };

        var dialog = new ReportSolverDialog({ size: "large" });
        var windowManager = new OO.ui.WindowManager();

        $(document.body).append(windowManager.$element);
        windowManager.addWindows([dialog]);
        windowManager.openWindow(dialog);
    };

    RS.openGrantDialog = function (sectionNumber) {
        function GrantDialog(config) {
            GrantDialog.super.call(this, config);
        }

        OO.inheritClass(GrantDialog, OO.ui.ProcessDialog);

        GrantDialog.static.name = 'GrantDialog';
        GrantDialog.static.title = 'Temporary Permission Duration';
        GrantDialog.static.actions = [
            { label: MSG.dialogCancel, flags: "safe" },
            { action: 'submit', label: MSG.dialogConfirm, flags: ['primary', 'progressive'] }
        ];

        GrantDialog.prototype.initialize = function () {
            GrantDialog.super.prototype.initialize.call(this);

            const layout = new OO.ui.PanelLayout({ padded: true, expanded: false });

            this.input = new OO.ui.TextInputWidget({
                placeholder: 'Number of months',
                type: 'number',
                inputmode: 'numeric'
            });

            layout.$element.append(this.input.$element);
            this.$body.append(layout.$element);
        };

        GrantDialog.prototype.getActionProcess = function (action) {
            const dialog = this;

            if (action === 'submit') {
                dialog.actions.setAbilities({ submit: false, cancel: false });
                dialog.pushPending();

                const months = parseInt(dialog.input.getValue(), 10);

                if (isNaN(months) || months <= 0) {
                    alert('Please enter a valid number greater than 0.');
                    dialog.popPending();
                    dialog.actions.setAbilities({ submit: true, cancel: true });
                    return;
                }

                const date = new Date();
                date.setMonth(date.getMonth() + months);

                const y = date.getFullYear(),
                    m = String(date.getMonth() + 1).padStart(2, '0'),
                    d = String(date.getDate()).padStart(2, '0');

                const wikitext = `{{TempSysop|${months}|${y}|${m}|${d}||automsg=1}}`;

                RS.doEdit(sectionNumber, wikitext, 'Granted temporary permissions', 'done');
            }

            return GrantDialog.super.prototype.getActionProcess.call(this, action);
        };

        const dialog = new GrantDialog({ size: 'medium' });
        const windowManager = new OO.ui.WindowManager();

        $(document.body).append(windowManager.$element);
        windowManager.addWindows([dialog]);
        windowManager.openWindow(dialog);
    };

    RS.doEdit = function (sectionNumber, comment, editSummary, status) {
        var $editSectionLink = $('span.mw-editsection a[href$="&section=' + sectionNumber + '"]');
        var $mwEditSection = $editSectionLink.closest('.mw-editsection');
        var $headline = $mwEditSection.prevAll('.mw-headline').first();

        if ($headline.length === 0) {
            $headline = $mwEditSection.prevAll('h1, h2, h3, h4, h5, h6').first();
        }

        var sectionTitle = $headline.attr('id').replace(/_/g, ' ');
        const pageTitle = mw.config.get('wgPageName');

        new mw.Api().postWithEditToken({
            action: 'parse',
            page: pageTitle,
            prop: 'wikitext',
            section: sectionNumber
        }).done(function (result) {
            let fullWikitext = result.parse.wikitext['*'];
            let wikitext;

            // Global permissions discussions use sr-closed and status parameters rather
            // than a simple appended closure comment. This branch preserves the heading,
            // updates the request status, and wraps the section in the expected template.
            if (
                wgPageName === 'Steward_requests/Global_permissions' &&
                /Global (sysop|rename|rollback|abuse)|Abuse filter (helper|maintainer) for/i.test(sectionTitle)
            ) {
                comment = comment.trim().replace(/([^\.\!\?}])$/, '$1.') + ' ~~~~';

                const match = fullWikitext.match(/^((={2,6}\s.*?\s={2,6}))\n([\s\S]*)$/m);
                const heading = match ? match[1] + '\n' : '';
                const body = match ? match[3] : fullWikitext;

                const updated = body.replace(/\|\s*status\s*=\s*[^|}]*([\|}])/i, `|status = ${status}\n $1`);

                wikitext = `${heading}{{sr-closed|text=\n${updated}\n----\n${comment}\n}}`;
            } else if (editSummary === 'Closed') {
                wikitext = fullWikitext + '\n' + comment;
            } else if (['Deleted', 'Kept', 'Redirected'].includes(editSummary)) {
                comment = comment.trim().replace(/([^\.\!\?}])$/, '$1.') + ' ~~~~';
                wikitext = fullWikitext + '\n----\n' + comment;
            } else {
                // Generic request handling: update common status templates and request
                // parameters where possible, then append the closing comment.
                wikitext = fullWikitext.replace(/\{\{\s*[Ss]tatus\s*(?:\|[^}]*)?\}\}/g, `{{Status|${status}}}`);

                if (/\{\{(sr-request|SRUC|CU request|interwiki request)/i.test(fullWikitext)) {
                    wikitext = wikitext.replace(/\|\s*[Ss]tatus\s*=\s*[^|]*\|/i, `|status = ${status}\n |`);
                }

                comment = comment.trim().replace(/([^\.\!\?}])$/, '$1.') + ' ~~~~';
                wikitext += '\n:' + comment;
            }

            new mw.Api().postWithEditToken({
                action: 'edit',
                title: pageTitle,
                section: sectionNumber,
                text: wikitext,
                summary: `/* ${sectionTitle} */ ${editSummary}${RS.summary}`,
                minor: true,
                nocreate: true
            }).done(() => location.reload());
        });
    };

    mw.loader.using('mediawiki.api', () => $(RS.setup));
});
// </nowiki>