Jump to content

User:Max/DeletedMetaData.js

From Wikipedia, the free encyclopedia
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>
$(() => {
    const excludedNamespaces = [-1, -2];
    if (excludedNamespaces.includes(mw.config.get('wgNamespaceNumber'))) return;

    const DeletedMetaData = {
        pageTitle: mw.config.get('wgPageName').replace(/_/g, ' '),
        icons: {},
        dialogApp: null,

        init() {
            mw.loader.using(['vue', '@wikimedia/codex', 'mediawiki.api', 'mediawiki.util']).then(async (require) => {
                this.Vue = require('vue');
                this.Codex = require('@wikimedia/codex');
                await this.loadIcons();
                this.checkAndAddPortlet();
            });
        },

        async loadIcons() {
            try {
                const data = await new mw.Api().get({
                    action: 'query',
                    list: 'codexicons',
                    names: 'cdxIconArrowNext|cdxIconArrowPrevious|cdxIconSortVertical|cdxIconArrowUp|cdxIconArrowDown|cdxIconFilter|cdxIconClose',
                    formatversion: 2
                });
                this.icons = (data && data.query && data.query.codexicons) || {};
            } catch (error) {
                this.icons = {};
            }
        },

        async checkAndAddPortlet() {
            try {
                const data = await new mw.Api().get({
                    action: 'query',
                    prop: 'deletedrevisions',
                    titles: this.pageTitle,
                    drvprop: 'user|timestamp|size|tags',
                    drvlimit: 1,
                    formatversion: 2
                });
                const page = data.query && data.query.pages && data.query.pages[0];
                if (page && page.deletedrevisions && page.deletedrevisions.length > 0) {
                    this.addStyle();
                    this.addPortletLink();
                }
            } catch (error) {
                return;
            }
        },

		addStyle() {
			mw.util.addCSS(`
		        .script-dmd-dialog.cdx-dialog {
		            max-width: 900px;
		            width: min(900px, 92vw);
		        }
		        .script-dmd-toolbar {
		            display: flex;
		            align-items: center;
		            justify-content: space-between;
		            margin: 0 0 10px;
		            flex-wrap: wrap;
		            gap: 8px;
		        }
		        .script-dmd-count {
		            font-size: 0.9em;
		            color: var(--color-subtle, #54595d);
		        }
		        .script-dmd-filter-row {
		            display: flex;
		            flex-wrap: wrap;
		            align-items: flex-end;
		            gap: 12px;
		            margin: 0 0 14px;
		            padding-bottom: 14px;
		            border-bottom: 1px solid var(--border-color-subtle, #c8ccd1);
		        }
		        .script-dmd-table-wrapper {
		            overflow-x: auto;
		            margin: 4px 0 12px;
		        }
		        .script-dmd-table {
		            border-collapse: collapse;
		            width: 100%;
		            font-size: 0.9em;
		        }
		        .script-dmd-table th {
		            border-bottom: 2px solid var(--border-color-subtle, #c8ccd1);
		            background: var(--background-color-interactive-subtle, #eaecf0);
		            padding: 6px;
		            text-align: left;
		            white-space: nowrap;
		            cursor: pointer;
		            user-select: none;
		        }
		        .script-dmd-table th .cdx-icon {
		            vertical-align: middle;
		            margin-left: 2px;
		        }
		        .script-dmd-table td {
		            border-bottom: 1px solid var(--border-color-subtle, #eaecf0);
		            padding: 6px;
		        }
		        .script-dmd-pagination {
		            display: flex;
		            align-items: center;
		            gap: 10px;
		            margin-top: 8px;
		        }
		        .script-dmd-page-label {
		            font-size: 0.9em;
		            color: var(--color-subtle, #54595d);
		        }
		    `);
		},

        addPortletLink() {
            const link = mw.util.addPortletLink('p-cactions', '#', 'View deleted revisions', 'ca-view-deleted-revisions');
            if (!link) return;
            $(link).on('click', (event) => {
                event.preventDefault();
                this.openDialog();
            });
        },

        openDialog() {
            if (this.dialogApp) {
                this.dialogApp.open = true;
                return;
            }

            const container = document.createElement('div');
            document.body.appendChild(container);
            const pageTitle = this.pageTitle;
            const previousIcon = this.icons.cdxIconArrowPrevious || null;
            const nextIcon = this.icons.cdxIconArrowNext || null;
            const sortVerticalIcon = this.icons.cdxIconSortVertical || null;
            const sortUpIcon = this.icons.cdxIconArrowUp || null;
            const sortDownIcon = this.icons.cdxIconArrowDown || null;
            const filterIcon = this.icons.cdxIconFilter || null;
            const closeIcon = this.icons.cdxIconClose || null;
            const pageSize = 50;

            const app = this.Vue.createMwApp({
                components: {
                    CdxDialog: this.Codex.CdxDialog,
                    CdxButton: this.Codex.CdxButton,
                    CdxIcon: this.Codex.CdxIcon,
                    CdxProgressBar: this.Codex.CdxProgressBar,
                    CdxMessage: this.Codex.CdxMessage,
                    CdxField: this.Codex.CdxField,
                    CdxTextInput: this.Codex.CdxTextInput
                },
                data() {
                    return {
                        open: true,
                        loading: false,
                        error: '',
                        allRevisions: [],
                        currentPage: 1,
                        pageTitle,
                        previousIcon,
                        nextIcon,
                        sortVerticalIcon,
                        sortUpIcon,
                        sortDownIcon,
                        filterIcon,
                        closeIcon,
                        pageSize,
                        sortColumn: 'timestamp',
                        sortDirection: 'desc',
                        filtersOpen: false,
                        filterUsername: '',
                        filterDateFrom: '',
                        filterDateTo: '',
                        columns: [
                            { key: 'user', label: 'User' },
                            { key: 'timestamp', label: 'Date/Time' },
                            { key: 'size', label: 'Size' },
                            { key: 'tags', label: 'Tags' }
                        ]
                    };
                },
                computed: {
                    hasActiveFilters() {
                        return this.filterUsername.trim() !== '' || this.filterDateFrom !== '' || this.filterDateTo !== '';
                    },
                    filteredRevisions() {
                        const username = this.filterUsername.trim().toLowerCase();
                        const from = this.filterDateFrom ? new Date(this.filterDateFrom + 'T00:00:00Z').getTime() : null;
                        const to = this.filterDateTo ? new Date(this.filterDateTo + 'T23:59:59Z').getTime() : null;
                        return this.allRevisions.filter((revision) => {
                            if (username) {
                                if (revision.userhidden || !revision.user || !revision.user.toLowerCase().includes(username)) return false;
                            }
                            const timestamp = new Date(revision.timestamp).getTime();
                            if (from !== null && timestamp < from) return false;
                            if (to !== null && timestamp > to) return false;
                            return true;
                        });
                    },
                    sortedRevisions() {
                        const column = this.sortColumn;
                        const direction = this.sortDirection;
                        const list = this.filteredRevisions.slice();
                        list.sort((a, b) => {
                            let aVal;
                            let bVal;
                            if (column === 'timestamp') {
                                aVal = new Date(a.timestamp).getTime();
                                bVal = new Date(b.timestamp).getTime();
                            } else if (column === 'size') {
                                aVal = typeof a.size === 'number' ? a.size : -1;
                                bVal = typeof b.size === 'number' ? b.size : -1;
                            } else if (column === 'tags') {
                                aVal = (a.tags || []).join(', ').toLowerCase();
                                bVal = (b.tags || []).join(', ').toLowerCase();
                            } else {
                                aVal = (a.userhidden ? '' : a.user || '').toLowerCase();
                                bVal = (b.userhidden ? '' : b.user || '').toLowerCase();
                            }
                            if (aVal < bVal) return direction === 'asc' ? -1 : 1;
                            if (aVal > bVal) return direction === 'asc' ? 1 : -1;
                            return 0;
                        });
                        return list;
                    },
                    pageCount() {
                        return Math.max(1, Math.ceil(this.sortedRevisions.length / this.pageSize));
                    },
                    pagedRevisions() {
                        const start = (this.currentPage - 1) * this.pageSize;
                        return this.sortedRevisions.slice(start, start + this.pageSize);
                    },
                    pagedRangeStart() {
                        if (this.filteredRevisions.length === 0) return 0;
                        return (this.currentPage - 1) * this.pageSize + 1;
                    },
                    pagedRangeEnd() {
                        return Math.min(this.currentPage * this.pageSize, this.filteredRevisions.length);
                    },
                    hasNext() {
                        return this.currentPage < this.pageCount;
                    },
                    hasPrev() {
                        return this.currentPage > 1;
                    }
                },
                watch: {
                    filterUsername() {
                        this.currentPage = 1;
                    },
                    filterDateFrom() {
                        this.currentPage = 1;
                    },
                    filterDateTo() {
                        this.currentPage = 1;
                    }
                },
                methods: {
                    formatDate(timestamp) {
                        const date = new Date(timestamp);
                        const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
                        let hours = date.getUTCHours();
                        const ampm = hours >= 12 ? 'PM' : 'AM';
                        hours = hours % 12 || 12;
                        const minutes = date.getUTCMinutes().toString().padStart(2, '0');
                        const seconds = date.getUTCSeconds().toString().padStart(2, '0');
                        return `${date.getUTCDate()} ${monthNames[date.getUTCMonth()]} ${date.getUTCFullYear()}, ${hours}:${minutes}:${seconds} ${ampm} (UTC)`;
                    },
                    formatSize(size) {
                        return typeof size === 'number' ? `${size.toLocaleString()} bytes` : '—';
                    },
                    formatTags(tags) {
                        return tags && tags.length > 0 ? tags.join(', ') : '—';
                    },
                    userUrl(user) {
                        return mw.util.getUrl(`User:${user}`);
                    },
                    sortIcon(column) {
                        if (this.sortColumn !== column) return this.sortVerticalIcon;
                        return this.sortDirection === 'asc' ? this.sortUpIcon : this.sortDownIcon;
                    },
                    sortBy(column) {
                        if (this.sortColumn === column) {
                            this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
                        } else {
                            this.sortColumn = column;
                            this.sortDirection = 'asc';
                        }
                        this.currentPage = 1;
                    },
                    toggleFilters() {
                        this.filtersOpen = !this.filtersOpen;
                    },
                    clearFilters() {
                        this.filterUsername = '';
                        this.filterDateFrom = '';
                        this.filterDateTo = '';
                    },
                    async loadAll() {
                        this.loading = true;
                        this.error = '';
                        this.allRevisions = [];
                        let continuation = null;
                        try {
                            do {
                                const params = {
                                    action: 'query',
                                    prop: 'deletedrevisions',
                                    titles: this.pageTitle,
                                    drvprop: 'user|timestamp|size|tags',
                                    drvlimit: 'max',
                                    formatversion: 2
                                };
                                if (continuation) params.drvcontinue = continuation;
                                const data = await new mw.Api().get(params);
                                const page = data.query && data.query.pages && data.query.pages[0];
                                const revisions = (page && page.deletedrevisions) || [];
                                this.allRevisions = this.allRevisions.concat(revisions);
                                continuation = data.continue ? data.continue.drvcontinue : null;
                            } while (continuation);
                        } catch (error) {
                            this.error = 'Error loading deleted revisions.';
                        }
                        this.loading = false;
                    },
                    nextPage() {
                        this.currentPage += 1;
                    },
                    prevPage() {
                        this.currentPage -= 1;
                    }
                },
                mounted() {
                    this.loadAll();
                },
                template: `
                    <cdx-dialog
                        v-model:open="open"
                        title="Deleted revisions"
                        :subtitle="pageTitle"
                        use-close-button
                        class="script-dmd-dialog"
                    >
                        <div class="script-dmd-toolbar">
                            <span v-if="!loading && !error" class="script-dmd-count">
                                Showing {{ pagedRangeStart }}–{{ pagedRangeEnd }} of {{ filteredRevisions.length }}<span v-if="hasActiveFilters"> (filtered from {{ allRevisions.length }})</span> deleted revisions
                            </span>
                            <cdx-button v-if="!loading && !error && allRevisions.length > 0" :weight="hasActiveFilters ? 'primary' : 'normal'" :action="hasActiveFilters ? 'progressive' : 'default'" @click="toggleFilters">
                                <cdx-icon v-if="filterIcon" :icon="filterIcon"></cdx-icon>
                                Filter
                            </cdx-button>
                        </div>

                        <div v-if="filtersOpen" class="script-dmd-filter-row">
                            <cdx-field>
                                <template #label>Username</template>
                                <cdx-text-input v-model="filterUsername" placeholder="Any user"></cdx-text-input>
                            </cdx-field>
                            <cdx-field>
                                <template #label>From date</template>
                                <cdx-text-input v-model="filterDateFrom" input-type="date"></cdx-text-input>
                            </cdx-field>
                            <cdx-field>
                                <template #label>To date</template>
                                <cdx-text-input v-model="filterDateTo" input-type="date"></cdx-text-input>
                            </cdx-field>
                            <cdx-button :disabled="!hasActiveFilters" @click="clearFilters">
                                <cdx-icon v-if="closeIcon" :icon="closeIcon"></cdx-icon>
                                Clear
                            </cdx-button>
                        </div>

                        <cdx-progress-bar v-if="loading" aria-label="Loading deleted revisions"></cdx-progress-bar>
                        <cdx-message v-if="error" type="error" :allow-user-dismiss="false">{{ error }}</cdx-message>
                        <cdx-message v-if="!loading && !error && allRevisions.length === 0" type="notice" :allow-user-dismiss="false">No deleted revisions found.</cdx-message>
                        <cdx-message v-if="!loading && !error && allRevisions.length > 0 && filteredRevisions.length === 0" type="notice" :allow-user-dismiss="false">No deleted revisions match the current filters.</cdx-message>
                        <div v-if="!loading && pagedRevisions.length > 0" class="script-dmd-table-wrapper">
                            <table class="script-dmd-table">
                                <thead>
                                    <tr>
                                        <th v-for="col in columns" :key="col.key" @click="sortBy(col.key)">
                                            {{ col.label }}
                                            <cdx-icon v-if="sortIcon(col.key)" :icon="sortIcon(col.key)" size="small"></cdx-icon>
                                        </th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr v-for="(revision, index) in pagedRevisions" :key="index">
                                        <td>
                                            <a v-if="revision.user && !revision.userhidden" :href="userUrl(revision.user)">{{ revision.user }}</a>
                                            <em v-else>Username removed</em>
                                        </td>
                                        <td>{{ formatDate(revision.timestamp) }}</td>
                                        <td>{{ formatSize(revision.size) }}</td>
                                        <td>{{ formatTags(revision.tags) }}</td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                        <div v-if="!loading && pageCount > 1" class="script-dmd-pagination">
                            <cdx-button :disabled="!hasPrev" @click="prevPage">
                                <cdx-icon v-if="previousIcon" :icon="previousIcon"></cdx-icon>
                                Previous
                            </cdx-button>
                            <span class="script-dmd-page-label">Page {{ currentPage }} of {{ pageCount }}</span>
                            <cdx-button :disabled="!hasNext" @click="nextPage">
                                Next
                                <cdx-icon v-if="nextIcon" :icon="nextIcon"></cdx-icon>
                            </cdx-button>
                        </div>
                    </cdx-dialog>
                `
            });

            this.dialogApp = app.mount(container);
        }
    };

    DeletedMetaData.init();
});
//</nowiki>