Source: ext.templateData.templateDiscovery/FavoritesStore.js

const templateDiscoveryConfig = require( './config.json' );
const mwConfig = require( './mwConfig.json' );
const USER_PREFERENCE_NAME = 'templatedata-favorite-templates';

/**
 * @class
 * @mixes OO.EventEmitter
 *
 * @constructor
 */
function FavoritesStore() {
	// Mixin constructors
	OO.EventEmitter.call( this );

	this.favoritesArray = JSON.parse( mw.user.options.get( USER_PREFERENCE_NAME ) );
	this.maxFavorites = templateDiscoveryConfig.maxFavorites;
}

/* Setup */
OO.mixinClass( FavoritesStore, OO.EventEmitter );

/* Events */
/**
 * When favorite is removed
 *
 * @event removed
 * @param {number} pageId
 */
/**
 * When favorite is added
 *
 * @event added
 * @param {number} pageId
 */

/**
 * @return {Promise}
 */
FavoritesStore.prototype.getAllFavoritesDetails = function () {
	return this.getFavoritesDetails( this.favoritesArray );
};

/**
 * Get the details of some favorites by page ID.
 *
 * @param {Array<number|string>} pageIds
 * @return {Promise}
 */
FavoritesStore.prototype.getFavoritesDetails = function ( pageIds ) {
	return new mw.Api().get( {
		action: 'templatedata',
		includeMissingTitles: 1,
		pageids: pageIds.join( '|' ),
		lang: mw.config.get( 'wgUserLanguage' ),
		redirects: 1,
		formatversion: 2
	} ).then( ( data ) => {
		const redirectedFrom = {};
		if ( data.redirects ) {
			data.redirects.forEach( ( redirect ) => {
				redirectedFrom[ redirect.to ] = redirect.from;
			} );
		}
		const favorites = [];
		Object.keys( data.pages ).forEach( ( k ) => {
			const favorite = data.pages[ k ];
			// Skip if the page is missing, or in an invalid namespace
			if ( favorite.missing ||
				!mwConfig.TemplateDataEditorNamespaces.includes( favorite.ns )
			) {
				this.favoritesArray.splice( this.favoritesArray.indexOf( parseInt( k ) ), 1 );
				return;
			}
			favorite.pageId = k;
			if ( favorite.title in redirectedFrom ) {
				favorite.redirecttitle = redirectedFrom[ favorite.title ];
			}
			favorites.push( favorite );
		} );
		// Return favorites sorted by page id in this.favoritesArray
		return favorites.sort( ( p1, p2 ) => {
			const index1 = this.favoritesArray.indexOf( parseInt( p1.pageId ) );
			const index2 = this.favoritesArray.indexOf( parseInt( p2.pageId ) );
			return index1 === index2 ? 0 : ( index1 < index2 ? -1 : 1 );
		} );
	} );
};

/**
 * Parse a page ID to a number, or throw an error
 *
 * @param {string} pageId
 * @return {number} The parsed page ID
 * @throws {Error} If the pageId is not a number
 */
function parsePageId( pageId ) {
	const parsedPageId = parseInt( pageId );
	if ( isNaN( parsedPageId ) ) {
		throw new Error( 'Invalid pageId: ' + pageId );
	}
	return parsedPageId;
}

/**
 * Add a page ID to the favorites array
 *
 * @param {string} pageId
 * @return {Promise} Resolves when the page ID is added (or is not able to be).
 */
FavoritesStore.prototype.addFavorite = function ( pageId ) {
	if ( this.favoritesArray.length < this.maxFavorites ) {
		const newFavorites = [ ...this.favoritesArray ];
		newFavorites.push( parsePageId( pageId ) );
		return this.saveFavoritesArray( newFavorites ).then( () => {
			this.emit( 'added', pageId );
			mw.notify(
				mw.msg( 'templatedata-favorite-added' ),
				{
					type: 'info',
					tag: 'templatedata-favorite-added'
				}
			);
		} );
	} else {
		mw.notify(
			mw.msg( 'templatedata-favorite-maximum-reached', this.maxFavorites ),
			{
				type: 'error',
				tag: 'templatedata-favorite-maximum-reached'
			}
		);
		return Promise.reject();
	}
};

/**
 * Remove a page ID from the favorites array
 *
 * @param {string} pageId
 * @return {Promise} Resolves when the page ID is removed (or is not able to be).
 */
FavoritesStore.prototype.removeFavorite = function ( pageId ) {
	const index = this.favoritesArray.indexOf( parsePageId( pageId ) );
	if ( index === -1 ) {
		return Promise.resolve();
	}
	const newFavorites = [ ...this.favoritesArray ];
	newFavorites.splice( index, 1 );
	return this.saveFavoritesArray( newFavorites ).then( () => {
		this.emit( 'removed', pageId );
		mw.notify(
			mw.msg( 'templatedata-favorite-removed' ),
			{
				type: 'info',
				tag: 'templatedata-favorite-removed'
			}
		);
		return;
	} );
};

/**
 * Check if a page ID is in the favorites array
 *
 * @param {string} pageId
 * @return {boolean} Whether the page ID is in the favorites array
 */
FavoritesStore.prototype.isFavorite = function ( pageId ) {
	return this.favoritesArray.includes( parsePageId( pageId ) );
};

/**
 * Save the favorites array to the user options
 *
 * @param {Array} favoritesArray
 * @return {Promise}
 */
FavoritesStore.prototype.saveFavoritesArray = function ( favoritesArray ) {
	// Update favoritesArray early, so that subsequent calls to this method use the new value,
	// but keep the old value available and roll back to it on error.
	const oldFavoritesArray = [ ...this.favoritesArray ];
	this.favoritesArray = [ ...favoritesArray ];
	const json = JSON.stringify( favoritesArray );
	return new mw.Api().saveOption( USER_PREFERENCE_NAME, json, { errorsuselocal: 1, errorformat: 'html' } )
		.then( () => {
			mw.user.options.set( USER_PREFERENCE_NAME, json );
		},
		( code, response ) => {
			// Restore previous value if the new one was not saved.
			this.favoritesArray = [ ...oldFavoritesArray ];
			// The 'notloggedin' error is a special case in mw.Api.saveOptions()
			if ( code === 'notloggedin' ) {
				mw.notify( mw.msg( 'notloggedin' ), {
					type: 'error',
					title: mw.msg( 'templatedata-favorite-error' )
				} );
			} else {
				for ( const error of response.errors ) {
					mw.notify( error.html, {
						type: 'error',
						title: mw.msg( 'templatedata-favorite-error' )
					} );
				}
			}
			throw code;
		} );
};

module.exports = FavoritesStore;