Edit Recovery: add new special page to list unsaved changes

Add Special:EditRecovery to dispaly a basic list of all pages
with locally-saved edit recovery data. This change just sets up
the initial work for this, and the actual design and improved
UX will come in subsequent changes.

Bug: T347673
Change-Id: I8edbfd21258fcb2e4fc9f3e4ded9876d6635d752
This commit is contained in:
Sam Wilson 2023-11-09 14:45:31 +08:00
parent 86d135301a
commit fb2f0d003c
11 changed files with 157 additions and 0 deletions

View file

@ -2053,6 +2053,7 @@ $wgAutoloadLocalClasses = [
'MediaWiki\\Specials\\SpecialDiff' => __DIR__ . '/includes/specials/SpecialDiff.php',
'MediaWiki\\Specials\\SpecialDoubleRedirects' => __DIR__ . '/includes/specials/SpecialDoubleRedirects.php',
'MediaWiki\\Specials\\SpecialEditPage' => __DIR__ . '/includes/specials/SpecialEditPage.php',
'MediaWiki\\Specials\\SpecialEditRecovery' => __DIR__ . '/includes/specials/SpecialEditRecovery.php',
'MediaWiki\\Specials\\SpecialEditTags' => __DIR__ . '/includes/specials/SpecialEditTags.php',
'MediaWiki\\Specials\\SpecialEditWatchlist' => __DIR__ . '/includes/specials/SpecialEditWatchlist.php',
'MediaWiki\\Specials\\SpecialEmailInvalidate' => __DIR__ . '/includes/specials/SpecialEmailInvalidate.php',

View file

@ -70,6 +70,7 @@ use MediaWiki\Specials\SpecialDeletePage;
use MediaWiki\Specials\SpecialDiff;
use MediaWiki\Specials\SpecialDoubleRedirects;
use MediaWiki\Specials\SpecialEditPage;
use MediaWiki\Specials\SpecialEditRecovery;
use MediaWiki\Specials\SpecialEditTags;
use MediaWiki\Specials\SpecialEditWatchlist;
use MediaWiki\Specials\SpecialEmailInvalidate;
@ -1232,6 +1233,7 @@ class SpecialPageFactory {
MainConfigNames::EnableEmail,
MainConfigNames::EnableJavaScriptTest,
MainConfigNames::EnableSpecialMute,
MainConfigNames::EnableEditRecovery,
MainConfigNames::PageLanguageUseDB,
MainConfigNames::SpecialPages,
];
@ -1356,6 +1358,12 @@ class SpecialPageFactory {
];
}
if ( $this->options->get( MainConfigNames::EnableEditRecovery ) ) {
$this->list['EditRecovery'] = [
'class' => SpecialEditRecovery::class,
];
}
// Add extension special pages
$this->list = array_merge( $this->list,
$this->options->get( MainConfigNames::SpecialPages ) );

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* @ingroup SpecialPage
*/
namespace MediaWiki\Specials;
use MediaWiki\Html\Html;
use MediaWiki\SpecialPage\SpecialPage;
/**
* @ingroup SpecialPage
*/
class SpecialEditRecovery extends SpecialPage {
public function __construct() {
parent::__construct( 'EditRecovery' );
}
protected function getGroupName() {
return 'changes';
}
/**
* @param string|null $subPage
*/
public function execute( $subPage ) {
parent::execute( $subPage );
$this->addHelpLink( 'Help:Edit_Recovery' );
$this->getOutput()->addModuleStyles( 'mediawiki.special.editrecovery.styles' );
$this->getOutput()->addModules( 'mediawiki.special.editrecovery' );
$noJs = Html::element(
'span',
[ 'class' => 'error mw-EditRecovery-special-nojs-notice' ],
$this->msg( 'edit-recovery-nojs-placeholder' )
);
$placeholder = Html::rawElement( 'div', [ 'class' => 'mw-EditRecovery-special' ], $noJs );
$this->getOutput()->addHTML( $placeholder );
}
}

View file

@ -650,6 +650,10 @@
"resettokens-done": "Tokens reset.",
"resettokens-resetbutton": "Reset selected tokens",
"sig-text": "--$1",
"editrecovery": "Edit Recovery",
"edit-recovery-nojs-placeholder": "JavaScript is required for the Edit Recovery feature.",
"edit-recovery-special-intro": "You have unsaved changes to the following {{PLURAL:$1|page or section|pages and/or sections}}:",
"edit-recovery-special-intro-empty": "You have no unsaved changes.",
"edit-recovery-loaded-title": "Changes recovered",
"edit-recovery-loaded-message": "Your unsaved changes have been automatically recovered.",
"edit-recovery-loaded-show": "Show changes",

View file

@ -904,6 +904,10 @@
"resettokens-done": "Message shown on [[Special:ResetTokens]] after the tokens have been reset successfully.",
"resettokens-resetbutton": "Form submit button on [[Special:ResetTokens]].",
"sig-text": "{{notranslate}} This is the text that appears when you click on the signature button (second button from the right) on the edit toolbar. $1 will be replaced with four tildes (which cannot be included directly in the message for technical reasons).",
"editrecovery": "{{doc-special|EditRecovery}}",
"edit-recovery-nojs-placeholder": "Error message shown when JavaScript is disabled.",
"edit-recovery-special-intro": "Message shown above a list of linked page titles.\n\nParameters:\n\n* $1 — Integer number of list items.",
"edit-recovery-special-intro-empty": "Message shown instead of the list of pages when the list is empty.",
"edit-recovery-loaded-title": "Title for a notification toast popup shown when an in-progress edit is recovered.",
"edit-recovery-loaded-message": "Message shown in a notification toast popup when an in-progress edit is recovered.",
"edit-recovery-loaded-show": "Button text in a notification toast popup shown when an in-progress edit is recovered, used to show changes.",

View file

@ -430,6 +430,7 @@ $specialPageAliases = [
'Diff' => [ 'Diff' ],
'DoubleRedirects' => [ 'DoubleRedirects' ],
'EditPage' => [ 'EditPage', 'Edit' ],
'EditRecovery' => [ 'EditRecovery' ],
'EditTags' => [ 'EditTags' ],
'EditWatchlist' => [ 'EditWatchlist' ],
'Emailuser' => [ 'EmailUser', 'Email' ],

View file

@ -2289,6 +2289,25 @@ return [
'mediawiki.special.preferences.styles.ooui' => [
'styles' => 'resources/src/mediawiki.special.preferences.styles.ooui.less',
],
'mediawiki.special.editrecovery.styles' => [
'styles' => 'resources/src/mediawiki.special.editrecovery/styles.less',
],
'mediawiki.special.editrecovery' => [
'packageFiles' => [
'resources/src/mediawiki.special.editrecovery/init.js',
'resources/src/mediawiki.special.editrecovery/SpecialEditRecovery.vue',
'resources/src/mediawiki.editRecovery/storage.js',
],
'dependencies' => [
'vue',
],
'messages' => [
'editlink',
'parentheses',
'edit-recovery-special-intro',
'edit-recovery-special-intro-empty',
],
],
'mediawiki.special.search' => [
'scripts' => 'resources/src/mediawiki.special.search/search.js',
'dependencies' => 'mediawiki.widgets.SearchInputWidget',

View file

@ -85,6 +85,21 @@ function loadData( pageName, section ) {
} );
}
function loadAllData() {
return new Promise( function ( resolve, reject ) {
if ( !db ) {
reject( 'DB not opened' );
}
const transaction = db.transaction( objectStoreName, 'readonly' );
const requestAll = transaction
.objectStore( objectStoreName )
.getAll();
requestAll.addEventListener( 'success', function () {
resolve( requestAll.result );
} );
} );
}
/**
* Save data for a specific page and section
*
@ -213,6 +228,7 @@ module.exports = {
openDatabase: openDatabaseLocal,
closeDatabase: closeDatabase,
loadData: loadData,
loadAllData: loadAllData,
saveData: saveData,
deleteData: deleteData,
deleteExpiredData: deleteExpiredData

View file

@ -0,0 +1,50 @@
<template>
<p v-if="pages.length">
{{ $i18n( 'edit-recovery-special-intro', pages.length ) }}
</p>
<p v-else>
{{ $i18n( 'edit-recovery-special-intro-empty' ) }}
</p>
<ul>
<li v-for="page in pages" :key="page">
<a :href="page.url">{{ page.title }}</a>
<span v-if="page.section"> &ndash; {{ page.section }}</span>
<span>
<a :href="page.editUrl">{{ $i18n( 'parentheses', $i18n( 'editlink' ) ) }}</a>
</span>
</li>
</ul>
</template>
<script>
const { ref } = require( 'vue' );
// @vue/component
module.exports = {
setup() {
const pages = ref( [] );
const storage = require( '../mediawiki.editRecovery/storage.js' );
storage.openDatabase().then( () => {
storage.loadAllData().then( ( allData ) => {
allData.forEach( ( d ) => {
const title = new mw.Title( d.pageName );
const editParams = { action: 'edit' };
if ( d.section ) {
editParams.section = d.section;
}
pages.value.push( {
title: title.getPrefixedText(),
url: title.getUrl(),
editUrl: title.getUrl( editParams ),
section: d.section,
sectionLabel: d.section ? mw.msg( 'parentheses', mw.msg( 'search-section', d.section ) ) : ''
} );
} );
} );
} );
return {
pages
};
}
};
// eslint-disable-next-line vue/dot-location
</script>

View file

@ -0,0 +1,10 @@
( function () {
'use strict';
const outer = document.querySelector( '.mw-EditRecovery-special' );
if ( !outer ) {
return;
}
const Vue = require( 'vue' );
const App = require( './SpecialEditRecovery.vue' );
Vue.createMwApp( App ).mount( outer );
}() );

View file

@ -0,0 +1,3 @@
.client-js .mw-EditRecovery-special-nojs-notice {
display: none;
}