From d321ad5b9d2bb308ec6ce53d678db6a88e8ab778 Mon Sep 17 00:00:00 2001 From: Chlod Alejandro Date: Tue, 1 Nov 2022 11:52:07 +0800 Subject: [PATCH] edit conflict management, fix save lockup --- i18n/core/en.json | 2 + src/models/ContributionSurveySection.ts | 9 ++++- src/session/DeputyRootSession.ts | 11 ++++++ .../root/DeputyContributionSurveySection.tsx | 37 ++++++++++++++++--- src/wiki/DeputyCasePageWikitext.ts | 21 +++++++++-- src/wiki/util/getSectionHTML.ts | 7 ++-- 6 files changed, 74 insertions(+), 13 deletions(-) diff --git a/i18n/core/en.json b/i18n/core/en.json index bce90285..368670e7 100644 --- a/i18n/core/en.json +++ b/i18n/core/en.json @@ -28,6 +28,8 @@ "deputy.session.section.saved": "Section saved", "deputy.session.section.failed": "Failed to save section", "deputy.session.section.missingSection": "The target section is missing from the case page.", + "deputy.session.section.conflict.title": "Edit conflict", + "deputy.session.section.conflict.help": "Someone else edited the page before you. Deputy will restart to load the new case content. Your changes will be preserved.", "deputy.session.row.status": "Current page status", "deputy.session.row.status.unfinished": "Unfinished", diff --git a/src/models/ContributionSurveySection.ts b/src/models/ContributionSurveySection.ts index 43d46cd9..7ed5c492 100644 --- a/src/models/ContributionSurveySection.ts +++ b/src/models/ContributionSurveySection.ts @@ -31,6 +31,10 @@ export default class ContributionSurveySection { * The original wikitext of this section */ readonly originalWikitext: string; + /** + * The revision ID of the wikitext attached to this section. + */ + readonly revid: number; /** * @param casePage The case page of this section @@ -38,13 +42,15 @@ export default class ContributionSurveySection { * @param closed Whether this section has been closed (wrapped in collapse templates) * @param closingComments Closing comments for this section * @param wikitext The original wikitext of this section + * @param revid The revision ID of the wikitext attached to this section. */ constructor( casePage: DeputyCasePage, name: string, closed: boolean, closingComments: string, - wikitext: string + wikitext: string, + revid: number ) { this.casePage = casePage; this.name = name; @@ -53,6 +59,7 @@ export default class ContributionSurveySection { this.originalWikitext = wikitext; this.originallyClosed = closed; + this.revid = revid; } } diff --git a/src/session/DeputyRootSession.ts b/src/session/DeputyRootSession.ts index 1ac634a7..a66e1b06 100644 --- a/src/session/DeputyRootSession.ts +++ b/src/session/DeputyRootSession.ts @@ -335,6 +335,7 @@ export default class DeputyRootSession { // User is editing, don't load interface. return; } + if ( await window.deputy.session.checkForActiveSessionTabs() ) { // User is on another tab, don't load interface. mw.loader.using( [ 'oojs-ui-core', 'oojs-ui-windows' ], () => { @@ -581,4 +582,14 @@ export default class DeputyRootSession { } } } + + /** + * Restarts the section. This rebuilds *everything* from the ground up, which may + * be required when there's an edit conflict. + */ + async restartSession() { + const casePage = this.casePage; + await this.closeSession(); + await window.deputy.session.DeputyRootSession.continueSession( casePage ); + } } diff --git a/src/ui/root/DeputyContributionSurveySection.tsx b/src/ui/root/DeputyContributionSurveySection.tsx index 590a5ec8..fa1b1f82 100644 --- a/src/ui/root/DeputyContributionSurveySection.tsx +++ b/src/ui/root/DeputyContributionSurveySection.tsx @@ -36,6 +36,10 @@ export default class DeputyContributionSurveySection implements DeputyUIElement headingName: string; sectionElements: HTMLElement[]; originalList: HTMLElement; + /** + * Revision ID of the actively-used wikitext. Used for detecting edit conflicts. + */ + revid: number; // UI elements (no OOUI types, fall back to `any`) container: HTMLElement; @@ -241,18 +245,20 @@ export default class DeputyContributionSurveySection implements DeputyUIElement * * @param wikitext Internal use only. Used to skip section loading using existing wikitext. */ - async getSection( wikitext?: string ): Promise { + async getSection( wikitext?: string & { revid: number } ): Promise { const collapsible = this.sectionElements.find( ( v: HTMLElement ) => v.querySelector( '.mw-collapsible' ) )?.querySelector( '.mw-collapsible' ) ?? null; + const sectionWikitext = await this.casePage.wikitext.getSectionWikitext( this.headingName ); return this._section ?? ( this._section = new ContributionSurveySection( this.casePage, this.headingName, collapsible != null, collapsible?.querySelector( 'th > div' ).innerText, - wikitext ?? await this.casePage.wikitext.getSectionWikitext( this.headingName ) + wikitext ?? sectionWikitext, + sectionWikitext.revid ) ); } @@ -287,7 +293,9 @@ export default class DeputyContributionSurveySection implements DeputyUIElement } } - const sectionWikitext = ( await this.getSection() ).originalWikitext; + const section = await this.getSection(); + const sectionWikitext = section.originalWikitext; + this.revid = section.revid; const wikitextLines = sectionWikitext.split( '\n' ); this.rows = []; @@ -385,10 +393,25 @@ export default class DeputyContributionSurveySection implements DeputyUIElement pageid: this.casePage.pageId, section: sectionId, text: this.wikitext, + baserevid: this.revid, summary: decorateEditSummary( this.editSummary ) } ).then( function ( data ) { return data; - }, function ( code, data ) { + }, ( code, data ) => { + if ( code === 'editconflict' ) { + // Wipe cache. + this.casePage.wikitext.resetCachedWikitext(); + OO.ui.alert( + mw.msg( 'deputy.session.section.conflict.help' ), + { + title: mw.msg( 'deputy.session.section.conflict.title' ) + } + ).then( () => { + window.deputy.session.rootSession.restartSession(); + } ); + return false; + } + mw.notify( { + async getSectionWikitext( section: string | number ): Promise { if ( typeof section === 'number' ) { return getPageContent( this.casePage.pageId, { rvsection: section } - ).then( ( v ) => v.toString() ); + ).then( ( v ) => { + return Object.assign( v.toString(), { + revid: v.revid + } ); + } ); } else { const wikitext = await this.getWikitext(); const wikitextLines = wikitext.split( '\n' ); @@ -70,7 +81,11 @@ export default class DeputyCasePageWikitext { } } - return sectionLines.join( '\n' ); + return Object.assign( + sectionLines.join( '\n' ), { + revid: wikitext.revid + } + ); } } diff --git a/src/wiki/util/getSectionHTML.ts b/src/wiki/util/getSectionHTML.ts index e61ae758..4c4c7f96 100644 --- a/src/wiki/util/getSectionHTML.ts +++ b/src/wiki/util/getSectionHTML.ts @@ -14,14 +14,14 @@ export default async function ( page: mw.Title | string, section: number | string, extraOptions: Record = {} -): Promise<{ element: HTMLDivElement, wikitext: string }> { +): Promise<{ element: HTMLDivElement, wikitext: string, revid: number }> { if ( typeof section === 'string' ) { section = await getSectionId( page, section ); } return MwApi.action.get( { action: 'parse', - prop: 'text|wikitext', + prop: 'text|wikitext|revid', page: normalizeTitle( page ).getPrefixedText(), section: section, disablelimitreport: true, @@ -32,7 +32,8 @@ export default async function ( return { element: temp.children[ 0 ] as HTMLDivElement, - wikitext: data.parse.wikitext + wikitext: data.parse.wikitext, + revid: data.parse.revid }; } ); }