User:BZPN/MassRollback2.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 14: 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; display: none;">
<div id="mass-rollback-container" style="border: 1px solid #ccc; padding: 15px; margin-bottom: 20px; border-radius: 4px; background: #f9f9f9; display: none;">
Line 87: Line 86:
<h4>Rollback reason</h4>
<h4>Rollback reason</h4>
<select id="rollback-reason" style="padding: 4px;">
<select id="rollback-reason" style="padding: 4px;">
<option value="">-- Select reason --</option>
<option value=""></option>
<option value="vandalism">Vandalism</option>
<option value="vandalism">Vandalism</option>
<option value="spam">Spam</option>
<option value="spam">Spam</option>
Line 129: Line 128:
</div>
</div>
`);
`);
const $toggleButton = $(`
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;">
<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 MassRollback
Show/Hide MassRollback
</button>
</button>
`);
`);
Line 139: Line 137:
$toggleButton.on('click', function() {
$toggleButton.on('click', function() {
$('#mass-rollback-container').slideToggle();
$('#mass-rollback-container').slideToggle();
const btn = $(this);
btn.text(btn.text() === 'Open MassRollback' ? 'Show MassRollback' : 'Hide MassRollback');
});
});
$('#mw-content-text').prepend($toggleButton).prepend($container);
$('#mw-content-text').prepend($toggleButton).prepend($container);
Line 149: Line 143:


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


// Dodaj checkbox przy edycjach w widoku kontrybucji
$('li[data-mw-revid]').each(function() {
$('li[data-mw-revid]').each(function() {
const $li = $(this);
const $li = $(this);
Line 304: Line 298:
});
});
}
}
$('#filtered-edits-container').css("display", "block");
$('#filtered-edits-container').css("display", "block").slideDown();
$('#filtered-edits-container').slideDown();
});
});
},
},
Line 337: Line 330:
},
},


// Nowa wersja: grupujemy edycje wg tytułu i dla każdej grupy wykrywamy ciąg kolejnych edycji.
performRollback: function(contributions) {
performRollback: function(contributions) {
const userName = mw.config.get('wgRelevantUserName');
const userName = mw.config.get('wgRelevantUserName');
Line 342: Line 336:
? $('#custom-reason').val()
? $('#custom-reason').val()
: $('#rollback-reason option:selected').text();
: $('#rollback-reason option:selected').text();
const summary = `Reverted edit(s) by [[User:${userName}|${userName}]]: ${reason}`;
const api = new mw.Api();
const api = new mw.Api();

const promises = contributions.map(contrib => {
// Grupujemy edycje wg tytułu strony
const summary = `Reverted edit by [[User:${userName}|${userName}]]: ${reason}`;
return api.postWithToken('csrf', {
const groups = {};
contributions.forEach(contrib => {
if (!groups[contrib.title]) {
groups[contrib.title] = [];
}
groups[contrib.title].push(contrib);
});

const promises = [];
// Dla każdej strony – sortuj wg daty malejąco i wykryj ciąg edycji
for (let title in groups) {
let group = groups[title].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
let latest = group[0];
// Rozpoczynamy ciąg od najnowszej edycji
let chain = [latest];
let currentParent = latest.parentid;
// Przeglądamy kolejne edycje dla tego samego tytułu
for (let i = 1; i < group.length; i++) {
const contrib = group[i];
// Jeżeli kolejna edycja jest dokładnie poprzednikiem poprzednio znalezionego elementu, dodajemy do ciągu
if (parseInt(contrib.revid, 10) === parseInt(currentParent, 10)) {
chain.push(contrib);
currentParent = contrib.parentid;
} else {
break;
}
}
// Wykonaj rollback tylko, jeśli mamy przynajmniej jedną edycję (zawsze prawda)
promises.push(api.postWithToken('csrf', {
action: 'edit',
action: 'edit',
undoafter: contrib.parentid,
undo: latest.revid,
undo: contrib.revid,
undoafter: currentParent,
title: contrib.title,
title: title,
summary: summary
summary: summary
});
}));
});
}
return Promise.all(promises);
return Promise.all(promises);
},
},


rollbackSelected: async function() {
rollbackSelected: async function() {
// Pobieramy zaznaczone edycje z listy kontrybucji
const selected = $('.rollback-checkbox:checked').map(function() {
const selected = $('.rollback-checkbox:checked').map(function() {
return {
return {
title: $(this).data('title'),
title: $(this).data('title'),
revid: $(this).data('revid'),
revid: $(this).data('revid'),
parentid: $(this).data('parentid')
parentid: $(this).data('parentid'),
// Wartość timestamp może nie być dostępna w tym widoku – można opcjonalnie dodać dodatkowy pobór danych
timestamp: $(this).closest('li').find('.mw-contributions-timestamp').text() || new Date().toISOString()
};
};
}).get();
}).get();
Line 386: Line 412:
title: $(this).data('title'),
title: $(this).data('title'),
revid: $(this).data('revid'),
revid: $(this).data('revid'),
parentid: $(this).data('parentid')
parentid: $(this).data('parentid'),
timestamp: new Date().toISOString() // Jeśli API już zwraca timestamp, można go tutaj użyć
};
};
}).get();
}).get();
Line 410: Line 437:
const api = new mw.Api();
const api = new mw.Api();
try {
try {
// Dodajemy "timestamp" by móc grupować edycje
const data = await api.get({
const data = await api.get({
action: 'query',
action: 'query',
Line 415: Line 443:
ucuser: userName,
ucuser: userName,
uclimit: 'max',
uclimit: 'max',
ucprop: 'ids|title|parentid'
ucprop: 'ids|title|parentid|timestamp'
});
});
const contributions = data.query.usercontribs;
const contributions = data.query.usercontribs;

Latest revision as of 14:51, 25 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=""></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();
            });
            
            $('#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);
            });

            // Dodaj checkbox przy edycjach w widoku kontrybucji
            $('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").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);
                });
            });
        },

        // Nowa wersja: grupujemy edycje wg tytułu i dla każdej grupy wykrywamy ciąg kolejnych edycji.
        performRollback: function(contributions) {
            const userName = mw.config.get('wgRelevantUserName');
            const reason = $('#rollback-reason').val() === 'other' 
                ? $('#custom-reason').val() 
                : $('#rollback-reason option:selected').text();
            const summary = `Reverted edit(s) by [[User:${userName}|${userName}]]: ${reason}`;
            const api = new mw.Api();

            // Grupujemy edycje wg tytułu strony
            const groups = {};
            contributions.forEach(contrib => {
                if (!groups[contrib.title]) {
                    groups[contrib.title] = [];
                }
                groups[contrib.title].push(contrib);
            });

            const promises = [];
            // Dla każdej strony – sortuj wg daty malejąco i wykryj ciąg edycji
            for (let title in groups) {
                let group = groups[title].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
                let latest = group[0];
                // Rozpoczynamy ciąg od najnowszej edycji
                let chain = [latest];
                let currentParent = latest.parentid;
                // Przeglądamy kolejne edycje dla tego samego tytułu
                for (let i = 1; i < group.length; i++) {
                    const contrib = group[i];
                    // Jeżeli kolejna edycja jest dokładnie poprzednikiem poprzednio znalezionego elementu, dodajemy do ciągu
                    if (parseInt(contrib.revid, 10) === parseInt(currentParent, 10)) {
                        chain.push(contrib);
                        currentParent = contrib.parentid;
                    } else {
                        break;
                    }
                }
                // Wykonaj rollback tylko, jeśli mamy przynajmniej jedną edycję (zawsze prawda)
                promises.push(api.postWithToken('csrf', {
                    action: 'edit',
                    undo: latest.revid,
                    undoafter: currentParent,
                    title: title,
                    summary: summary
                }));
            }
            return Promise.all(promises);
        },

        rollbackSelected: async function() {
            // Pobieramy zaznaczone edycje z listy kontrybucji
            const selected = $('.rollback-checkbox:checked').map(function() {
                return {
                    title: $(this).data('title'),
                    revid: $(this).data('revid'),
                    parentid: $(this).data('parentid'),
                    // Wartość timestamp może nie być dostępna w tym widoku – można opcjonalnie dodać dodatkowy pobór danych
                    timestamp: $(this).closest('li').find('.mw-contributions-timestamp').text() || new Date().toISOString()
                };
            }).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'),
                    timestamp: new Date().toISOString() // Jeśli API już zwraca timestamp, można go tutaj użyć
                };
            }).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 {
                // Dodajemy "timestamp" by móc grupować edycje
                const data = await api.get({
                    action: 'query',
                    list: 'usercontribs',
                    ucuser: userName,
                    uclimit: 'max',
                    ucprop: 'ids|title|parentid|timestamp'
                });
                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);