Help:MassRollback (gadget)/new.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
 
(One intermediate revision by the same user not shown)
Line 5: Line 5:


init: function() {
init: function() {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {
return;
return;
Line 15: Line 14:


createUI: function() {
createUI: function() {
const $container = $(`
const $container = $(`
<div id="mass-rollback-container" style="border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; border-radius: 4px; background: #f9f9f9;">
<div id="mass-rollback-container" style="border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; border-radius: 4px; background: #f9f9f9; display: none;">
<h3 style="margin-top: 0;">Mass Rollback Tool</h3>
<h3 style="margin-top: 0;">Mass Rollback Tool</h3>
Line 129: Line 129:
</div>
</div>
`);
`);
$('#mw-content-text').prepend($container);
const $toggleButton = $(`
<button id="mass-rollback-toggle" style="padding: 5px 10px; margin-bottom: 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer;">
Show/Hide MassRollback
</button>
`);
$toggleButton.on('click', function() {
$('#mass-rollback-container').slideToggle();
const btn = $(this);
btn.text(btn.text() === 'Open MassRollback' ? 'Show/Hide MassRollback' : 'Show/Hide MassRollback');
});
$('#mw-content-text').prepend($toggleButton).prepend($container);
},
},


Line 152: Line 168:
$('#rollback-all').on('click', this.rollbackAll.bind(this));
$('#rollback-all').on('click', this.rollbackAll.bind(this));


$(document).on('change', '#select-all', function() {
$(document).on('change', '#select-all', function() {
const isChecked = $(this).is(':checked');
const isChecked = $(this).is(':checked');
Line 158: Line 173:
});
});


$('li[data-mw-revid]').each(function() {
$('li[data-mw-revid]').each(function() {
const $li = $(this);
const $li = $(this);
Line 196: Line 210:
},
},


getNamespaceName: function(ns) {
getNamespaceName: function(ns) {
const nsMapping = {
const nsMapping = {
Line 228: Line 241:
const sortOrder = $('#sort-order').val();
const sortOrder = $('#sort-order').val();


if (namespaceFilter.includes('all')) {
if (namespaceFilter.includes('all')) {
namespaceFilter = [];
namespaceFilter = [];
Line 249: Line 261:
});
});


filtered.sort((a, b) => {
filtered.sort((a, b) => {
return sortOrder === 'asc'
return sortOrder === 'asc'
Line 293: Line 304:
});
});
}
}
$('#filtered-edits-container').style = "display:block;";
$('#filtered-edits-container').css("display", "block");
$('#filtered-edits-container').slideDown();
$('#filtered-edits-container').slideDown();
});
});

Latest revision as of 21:50, 24 March 2025

(function($, mw) {
    'use strict';

    const MassRollback = {

        init: function() {
            if (mw.config.get('wgCanonicalSpecialPageName') !== 'Contributions') {
                return;
            }
            this.createUI();
            this.bindEvents();
            this.fetchUserStats();
        },

        createUI: function() {
            
            const $container = $(`
                <div id="mass-rollback-container" style="border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; border-radius: 4px; background: #f9f9f9; display: none;">
                    <h3 style="margin-top: 0;">Mass Rollback Tool</h3>
                    
                    <div id="stats-section" style="margin-bottom: 15px;">
                        <h4>User statistics</h4>
                        <p>Total edits: <strong id="total-edits">-</strong></p>
                        <p>First edit: <span id="first-edit">-</span></p>
                        <p>Latest edit: <span id="last-edit">-</span> <button id="refresh-stats" style="padding: 3px 8px; background-color: #aaa; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Refresh</button></p>
                    </div>
                    
                    <hr style="margin: 15px 0;">
                    
                    <div id="filters-section" style="margin-bottom: 15px;">
                        <h4>Filter Edits</h4>
                        <div style="margin-bottom: 10px;">
                            <label style="margin-right: 5px;">Date From:</label>
                            <input type="date" id="start-date" style="padding: 4px;">
                            <label style="margin: 0 5px;">To:</label>
                            <input type="date" id="end-date" style="padding: 4px;">
                        </div>
                        <div style="margin-bottom: 10px;">
                            <label style="margin-right: 5px;">Namespace:</label>
                            <select id="namespace-filter" multiple style="padding: 4px; min-width: 150px;">
                                <option value="all">All</option>
                                <option value="0">Main</option>
                                <option value="1">Talk</option>
                                <option value="2">User</option>
                                <option value="3">User talk</option>
                                <option value="4">Project</option>
                                <option value="5">Project talk</option>
                                <option value="6">File</option>
                                <option value="7">File talk</option>
                                <option value="8">MediaWiki</option>
                                <option value="9">MediaWiki talk</option>
                                <option value="10">Template</option>
                                <option value="11">Template talk</option>
                                <option value="12">Help</option>
                                <option value="13">Help talk</option>
                                <option value="14">Category</option>
                                <option value="15">Category talk</option>
                                <option value="100">Portal</option>
                                <option value="101">Portal talk</option>
                            </select>
                        </div>
                        <div style="margin-bottom: 10px;">
                            <label style="margin-right: 5px;">Edit Size:</label>
                            <select id="size-filter" style="padding: 4px;">
                                <option value="">Any</option>
                                <option value="small">Small (&lt; 50 bytes)</option>
                                <option value="medium">Medium (50-500 bytes)</option>
                                <option value="large">Large (&gt; 500 bytes)</option>
                            </select>
                        </div>
                        <div style="margin-bottom: 10px;">
                            <label style="margin-right: 5px;">Sort Order:</label>
                            <select id="sort-order" style="padding: 4px;">
                                <option value="desc">Latest first</option>
                                <option value="asc">Oldest first</option>
                            </select>
                        </div>
                        <div style="margin-bottom: 10px;">
                            <button id="clear-filters" style="padding: 5px 10px; background-color: #aaa; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Clear filters</button>
                            <button id="show-filtered" style="padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer; margin-left: 5px;">Show filtered edits</button>
                        </div>
                    </div>
                    
                    <hr style="margin: 15px 0;">
                    
                    <div id="reason-section" style="margin-bottom: 15px;">
                        <h4>Rollback reason</h4>
                        <select id="rollback-reason" style="padding: 4px;">
                            <option value="">-- Select reason --</option>
                            <option value="vandalism">Vandalism</option>
                            <option value="spam">Spam</option>
                            <option value="test">Test rollback</option>
                            <option value="teste">Test edit</option>
                            <option value="rules-violation">Violation</option>
                            <option value="other">Other</option>
                        </select>
                        <input type="text" id="custom-reason" placeholder="Enter custom reason" style="display:none; padding: 4px; margin-top: 10px; width: 100%;">
                    </div>
                    
                    <div id="actions-section" style="text-align: center;">
                        <button id="rollback-selected" style="padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px;">Rollback selected</button>
                        <button id="rollback-all" style="padding: 5px 10px; background-color: #ff4136; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Rollback all</button>
                    </div>
                </div>
                
                <div id="filtered-edits-container" style="display:none; border: 1px solid #ccc; padding: 15px; margin-bottom:20px; border-radius: 4px; background: #f9f9f9;">
                    <h4>Filtered edits</h4>
                    <div style="margin-bottom: 10px;">
                        <input type="checkbox" id="select-all"> <label for="select-all">Select All</label>
                    </div>
                    <table id="filtered-edits-table" style="width: 100%; border-collapse: collapse;">
                        <thead>
                            <tr style="background: #e9e9e9;">
                                <th style="padding: 5px; border: 1px solid #ccc;">Revid</th>
                                <th style="padding: 5px; border: 1px solid #ccc;">Title</th>
                                <th style="padding: 5px; border: 1px solid #ccc;">Timestamp</th>
                                <th style="padding: 5px; border: 1px solid #ccc;">Namespace</th>
                                <th style="padding: 5px; border: 1px solid #ccc;">Size</th>
                                <th style="padding: 5px; border: 1px solid #ccc;">Select</th>
                            </tr>
                        </thead>
                        <tbody>
                            <!-- Wstawiane dynamicznie -->
                        </tbody>
                    </table>
                    <div style="text-align: center; margin-top: 10px;">
                        <button id="rollback-selected-filtered" style="padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Rollback selected filtered edits</button>
                    </div>
                </div>
            `);
            
            
            const $toggleButton = $(`
                <button id="mass-rollback-toggle" style="padding: 5px 10px; margin-bottom: 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer;">
                    Show/Hide MassRollback
                </button>
            `);
            
            $toggleButton.on('click', function() {
                $('#mass-rollback-container').slideToggle();
                
                const btn = $(this);
                btn.text(btn.text() === 'Open MassRollback' ? 'Show/Hide MassRollback' : 'Show/Hide MassRollback');
            });
            
            
            $('#mw-content-text').prepend($toggleButton).prepend($container);
        },

        bindEvents: function() {
            
            $('#rollback-reason').on('change', function() {
                $('#custom-reason').toggle($(this).val() === 'other');
            });

            $('#clear-filters').on('click', function() {
                $('#start-date').val('');
                $('#end-date').val('');
                $('#namespace-filter').val(['all']);
                $('#size-filter').val('');
                $('#sort-order').val('desc');
            });

            $('#refresh-stats').on('click', this.fetchUserStats.bind(this));
            $('#show-filtered').on('click', this.displayFilteredEdits.bind(this));
            $('#rollback-selected-filtered').on('click', this.rollbackSelectedFromFiltered.bind(this));
            $('#rollback-selected').on('click', this.rollbackSelected.bind(this));
            $('#rollback-all').on('click', this.rollbackAll.bind(this));

            $(document).on('change', '#select-all', function() {
                const isChecked = $(this).is(':checked');
                $('#filtered-edits-table tbody input[type="checkbox"]').prop('checked', isChecked);
            });

            $('li[data-mw-revid]').each(function() {
                const $li = $(this);
                const revid = $li.data('mw-revid');
                const parentid = $li.data('mw-prev-revid');
                const title = $li.find('.mw-contributions-title').text().trim();
                const $checkbox = $('<input>', {
                    type: 'checkbox',
                    class: 'rollback-checkbox',
                    'data-revid': revid,
                    'data-parentid': parentid,
                    'data-title': title,
                    style: 'margin-right: 5px;'
                });
                $li.prepend($checkbox);
            });
        },

        fetchUserStats: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 'max',
                ucprop: 'timestamp'
            }).done(function(data) {
                const contributions = data.query.usercontribs;
                $('#total-edits').text(contributions.length);
                if (contributions.length > 0) {
                    const timestamps = contributions.map(c => new Date(c.timestamp)).sort((a, b) => a - b);
                    $('#first-edit').text(timestamps[0].toLocaleDateString());
                    $('#last-edit').text(timestamps[timestamps.length - 1].toLocaleDateString());
                }
            });
        },

        getNamespaceName: function(ns) {
            const nsMapping = {
                0: 'Main',
                1: 'Talk',
                2: 'User',
                3: 'User talk',
                4: 'Project',
                5: 'Project talk',
                6: 'File',
                7: 'File talk',
                8: 'MediaWiki',
                9: 'MediaWiki talk',
                10: 'Template',
                11: 'Template talk',
                12: 'Help',
                13: 'Help talk',
                14: 'Category',
                15: 'Category talk',
                100: 'Portal',
                101: 'Portal talk'
            };
            return nsMapping[ns] || ns;
        },

        applyFilters: function(contributions) {
            const startDate = $('#start-date').val() ? new Date($('#start-date').val()) : null;
            const endDate = $('#end-date').val() ? new Date($('#end-date').val()) : null;
            let namespaceFilter = $('#namespace-filter').val() || [];
            const sizeFilter = $('#size-filter').val();
            const sortOrder = $('#sort-order').val();

            if (namespaceFilter.includes('all')) {
                namespaceFilter = [];
            }

            let filtered = contributions.filter(contrib => {
                const contribDate = new Date(contrib.timestamp);
                if (startDate && contribDate < startDate) return false;
                if (endDate && contribDate > endDate) return false;
                if (namespaceFilter.length > 0 && !namespaceFilter.includes(contrib.ns.toString())) return false;
                if (sizeFilter) {
                    const size = contrib.size || 0;
                    switch (sizeFilter) {
                        case 'small': return size < 50;
                        case 'medium': return size >= 50 && size <= 500;
                        case 'large': return size > 500;
                    }
                }
                return true;
            });

            filtered.sort((a, b) => {
                return sortOrder === 'asc'
                    ? new Date(a.timestamp) - new Date(b.timestamp)
                    : new Date(b.timestamp) - new Date(a.timestamp);
            });

            return filtered;
        },

        displayFilteredEdits: function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            api.get({
                action: 'query',
                list: 'usercontribs',
                ucuser: userName,
                uclimit: 'max',
                ucprop: 'ids|title|timestamp|size|ns|parentid'
            }).done((data) => {
                const allContributions = data.query.usercontribs;
                const filtered = this.applyFilters(allContributions);
                const $tbody = $('#filtered-edits-table tbody');
                $tbody.empty();

                if (filtered.length === 0) {
                    $tbody.append('<tr><td colspan="6" style="text-align:center; padding:5px;">No edits match the selected filters.</td></tr>');
                } else {
                    filtered.forEach(contrib => {
                        const row = `
                            <tr>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.revid}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.title}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${new Date(contrib.timestamp).toLocaleString()}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${this.getNamespaceName(contrib.ns)}</td>
                                <td style="padding: 5px; border: 1px solid #ccc;">${contrib.size || 0}</td>
                                <td style="padding: 5px; border: 1px solid #ccc; text-align: center;">
                                    <input type="checkbox" class="filtered-rollback-checkbox" data-revid="${contrib.revid}" data-parentid="${contrib.parentid}" data-title="${contrib.title}">
                                </td>
                            </tr>
                        `;
                        $tbody.append(row);
                    });
                }
                $('#filtered-edits-container').css("display", "block");
                $('#filtered-edits-container').slideDown();
            });
        },

        confirmAction: function(message, count) {
            return new Promise((resolve) => {
                const $modal = $(`
                    <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                         background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
                        <div style="background: #fff; padding: 20px; border-radius: 4px; max-width: 400px; width: 90%;">
                            <h4 style="margin-top:0;">Confirm Rollback</h4>
                            <p>${message}</p>
                            <p>Number of edits: ${count}</p>
                            <div style="text-align: right; margin-top: 15px;">
                                <button id="confirm-action" style="padding: 5px 10px; background-color: #007bff; color: #fff; border: none; border-radius: 3px; cursor: pointer; margin-right: 5px;">Confirm</button>
                                <button id="cancel-action" style="padding: 5px 10px; background-color: #aaa; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Cancel</button>
                            </div>
                        </div>
                    </div>
                `);
                $('body').append($modal);
                $modal.find('#confirm-action').on('click', function() {
                    $modal.remove();
                    resolve(true);
                });
                $modal.find('#cancel-action').on('click', function() {
                    $modal.remove();
                    resolve(false);
                });
            });
        },

        performRollback: function(contributions) {
            const userName = mw.config.get('wgRelevantUserName');
            const reason = $('#rollback-reason').val() === 'other' 
                ? $('#custom-reason').val() 
                : $('#rollback-reason option:selected').text();
            const api = new mw.Api();
            const promises = contributions.map(contrib => {
                const summary = `Reverted edit by [[User:${userName}|${userName}]]: ${reason}`;
                return api.postWithToken('csrf', {
                    action: 'edit',
                    undoafter: contrib.parentid,
                    undo: contrib.revid,
                    title: contrib.title,
                    summary: summary
                });
            });
            return Promise.all(promises);
        },

        rollbackSelected: async function() {
            const selected = $('.rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid')
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No edits selected.', { type: 'warn' });
                return;
            }
            const confirmed = await this.confirmAction('Are you sure you want to rollback the selected edits?', selected.length);
            if (!confirmed) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackSelectedFromFiltered: async function() {
            const selected = $('.filtered-rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid')
                };
            }).get();

            if (selected.length === 0) {
                mw.notify('No filtered edits selected.', { type: 'warn' });
                return;
            }
            const confirmed = await this.confirmAction('Are you sure you want to rollback the selected filtered edits?', selected.length);
            if (!confirmed) return;
            try {
                await this.performRollback(selected);
                mw.notify(`Successfully rolled back ${selected.length} filtered edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        },

        rollbackAll: async function() {
            const userName = mw.config.get('wgRelevantUserName');
            const api = new mw.Api();
            try {
                const data = await api.get({
                    action: 'query',
                    list: 'usercontribs',
                    ucuser: userName,
                    uclimit: 'max',
                    ucprop: 'ids|title|parentid'
                });
                const contributions = data.query.usercontribs;
                if (contributions.length === 0) {
                    mw.notify('No edits to rollback.', { type: 'warn' });
                    return;
                }
                const confirmed = await this.confirmAction('Are you sure you want to rollback ALL edits?', contributions.length);
                if (!confirmed) return;
                await this.performRollback(contributions);
                mw.notify(`Successfully rolled back all ${contributions.length} edits.`, { type: 'success' });
                location.reload();
            } catch (error) {
                mw.notify('Error during rollback.', { type: 'error' });
                console.error(error);
            }
        }
    };

    $(document).ready(function() {
        MassRollback.init();
    });
})(jQuery, mediaWiki);