wikimedia/mediawiki-core

View on GitHub
resources/src/mediawiki.rcfilters/dm/SavedQueriesModel.js

Summary

Maintainability
B
5 hrs
Test Coverage
var SavedQueryItemModel = require( './SavedQueryItemModel.js' ),
    SavedQueriesModel;

/**
 * View model for saved queries.
 *
 * @class mw.rcfilters.dm.SavedQueriesModel
 * @ignore
 * @mixes OO.EventEmitter
 * @mixes OO.EmitterList
 *
 * @param {mw.rcfilters.dm.FiltersViewModel} filtersModel Filters model
 * @param {Object} [config] Configuration options
 * @param {string} [config.default] Default query ID
 */
SavedQueriesModel = function MwRcfiltersDmSavedQueriesModel( filtersModel, config ) {
    config = config || {};

    // Mixin constructor
    OO.EventEmitter.call( this );
    OO.EmitterList.call( this );

    this.default = config.default;
    this.filtersModel = filtersModel;
    this.converted = false;

    // Events
    this.aggregate( { update: 'itemUpdate' } );
};

/* Initialization */

OO.initClass( SavedQueriesModel );
OO.mixinClass( SavedQueriesModel, OO.EventEmitter );
OO.mixinClass( SavedQueriesModel, OO.EmitterList );

/* Events */

/**
 * Model is initialized.
 *
 * @event initialize
 * @ignore
 */

/**
 * An item has changed.
 *
 * @event itemUpdate
 * @param {mw.rcfilters.dm.SavedQueryItemModel} Changed item
 * @ignore
 */

/**
 * The default has changed.
 *
 * @event default
 * @param {string} New default ID
 * @ignore
 */

/* Methods */

/**
 * Initialize the saved queries model by reading it from the user's settings.
 * The structure of the saved queries is:
 * {
 *    version: (string) Version number; if version 2, the query represents
 *             parameters. Otherwise, the older version represented filters
 *             and needs to be readjusted,
 *    default: (string) Query ID
 *    queries:{
 *       query_id_1: {
 *          data:{
 *             filters: (Object) Minimal definition of the filters
 *             highlights: (Object) Definition of the highlights
 *          },
 *          label: (optional) Name of this query
 *       }
 *    }
 * }
 *
 * @param {Object} [savedQueries] An object with the saved queries with
 *  the above structure.
 * @fires initialize
 */
SavedQueriesModel.prototype.initialize = function ( savedQueries ) {
    var model = this;

    savedQueries = savedQueries || {};

    this.clearItems();
    this.default = null;
    this.converted = false;

    if ( savedQueries.version !== '2' ) {
        // Old version dealt with filter names. We need to migrate to the new structure
        // The new structure:
        // {
        //   version: (string) '2',
        //   default: (string) Query ID,
        //   queries: {
        //     query_id: {
        //       label: (string) Name of the query
        //       data: {
        //         params: (object) Representing all the parameter states
        //         highlights: (object) Representing all the filter highlight states
        //     }
        //   }
        // }
        // eslint-disable-next-line no-jquery/no-each-util
        $.each( savedQueries.queries || {}, function ( id, obj ) {
            if ( obj.data && obj.data.filters ) {
                obj.data = model.convertToParameters( obj.data );
            }
        } );

        this.converted = true;
        savedQueries.version = '2';
    }

    // Initialize the query items
    // eslint-disable-next-line no-jquery/no-each-util
    $.each( savedQueries.queries || {}, function ( id, obj ) {
        var normalizedData = obj.data,
            isDefault = String( savedQueries.default ) === String( id );

        if ( normalizedData && normalizedData.params ) {
            // Backwards-compat fix: Remove sticky parameters from
            // the given data, if they exist
            normalizedData.params = model.filtersModel.removeStickyParams( normalizedData.params );

            // Correct the invert state for effective selection
            if ( normalizedData.params.invert && !normalizedData.params.namespace ) {
                delete normalizedData.params.invert;
            }

            model.cleanupHighlights( normalizedData );

            id = String( id );

            // Skip the addNewQuery method because we don't want to unnecessarily manipulate
            // the given saved queries unless we literally intend to (like in backwards compat fixes)
            // And the addNewQuery method also uses a minimization routine that checks for the
            // validity of items and minimizes the query. This isn't necessary for queries loaded
            // from the backend, and has the risk of removing values if they're temporarily
            // invalid (example: if we temporarily removed a cssClass from a filter in the backend)
            model.addItems( [
                new SavedQueryItemModel(
                    id,
                    obj.label,
                    normalizedData,
                    { default: isDefault }
                )
            ] );

            if ( isDefault ) {
                model.default = id;
            }
        }
    } );

    this.emit( 'initialize' );
};

/**
 * Clean up highlight parameters.
 * 'highlight' used to be stored, it's not inferred based on the presence of absence of
 * filter colors.
 *
 * @param {Object} data Saved query data
 */
SavedQueriesModel.prototype.cleanupHighlights = function ( data ) {
    if (
        data.params.highlight === '0' &&
        data.highlights && Object.keys( data.highlights ).length
    ) {
        data.highlights = {};
    }
    delete data.params.highlight;
};

/**
 * Convert from representation of filters to representation of parameters
 *
 * @param {Object} data Query data
 * @return {Object} New converted query data
 */
SavedQueriesModel.prototype.convertToParameters = function ( data ) {
    var newData = {},
        defaultFilters = this.filtersModel.getFiltersFromParameters( this.filtersModel.getDefaultParams() ),
        fullFilterRepresentation = $.extend( true, {}, defaultFilters, data.filters ),
        highlightEnabled = data.highlights.highlight;

    delete data.highlights.highlight;

    // Filters
    newData.params = this.filtersModel.getMinimizedParamRepresentation(
        this.filtersModel.getParametersFromFilters( fullFilterRepresentation )
    );

    // Highlights: appending _color to keys
    newData.highlights = {};
    // eslint-disable-next-line no-jquery/no-each-util
    $.each( data.highlights, function ( highlightedFilterName, value ) {
        if ( value ) {
            newData.highlights[ highlightedFilterName + '_color' ] = data.highlights[ highlightedFilterName ];
        }
    } );

    // Add highlight
    newData.params.highlight = String( Number( highlightEnabled || 0 ) );

    return newData;
};

/**
 * Add a query item
 *
 * @param {string} label Label for the new query
 * @param {Object} fulldata Full data representation for the new query, combining highlights and filters
 * @param {boolean} isDefault Item is default
 * @param {string} [id] Query ID, if exists. If this isn't given, a random
 *  new ID will be created.
 * @return {string} ID of the newly added query
 */
SavedQueriesModel.prototype.addNewQuery = function ( label, fulldata, isDefault, id ) {
    var normalizedData = { params: {}, highlights: {} },
        highlightParamNames = Object.keys( this.filtersModel.getEmptyHighlightParameters() ),
        randomID = String( id || Date.now() ),
        data = this.filtersModel.getMinimizedParamRepresentation( fulldata );

    // Split highlight/params
    // eslint-disable-next-line no-jquery/no-each-util
    $.each( data, function ( param, value ) {
        if ( param !== 'highlight' && highlightParamNames.indexOf( param ) > -1 ) {
            normalizedData.highlights[ param ] = value;
        } else {
            normalizedData.params[ param ] = value;
        }
    } );

    // Correct the invert state for effective selection
    if ( normalizedData.params.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
        delete normalizedData.params.invert;
    }
    // Correct the inverttags state for effective selection
    if ( normalizedData.params.inverttags && !this.filtersModel.areTagsEffectivelyInverted() ) {
        delete normalizedData.params.inverttags;
    }

    // Add item
    this.addItems( [
        new SavedQueryItemModel(
            randomID,
            label,
            normalizedData,
            { default: isDefault }
        )
    ] );

    if ( isDefault ) {
        this.setDefault( randomID );
    }

    return randomID;
};

/**
 * Remove query from model
 *
 * @param {string} queryID Query ID
 */
SavedQueriesModel.prototype.removeQuery = function ( queryID ) {
    var query = this.getItemByID( queryID );

    if ( query ) {
        // Check if this item was the default
        if ( String( this.getDefault() ) === String( queryID ) ) {
            // Nulify the default
            this.setDefault( null );
        }

        this.removeItems( [ query ] );
    }
};

/**
 * Get an item that matches the requested query
 *
 * @param {Object} fullQueryComparison Object representing all filters and highlights to compare
 * @return {mw.rcfilters.dm.SavedQueryItemModel} Matching item model
 * @ignore
 */
SavedQueriesModel.prototype.findMatchingQuery = function ( fullQueryComparison ) {
    // Minimize before comparison
    fullQueryComparison = this.filtersModel.getMinimizedParamRepresentation( fullQueryComparison );

    // Correct the invert state for effective selection
    if ( fullQueryComparison.invert && !this.filtersModel.areNamespacesEffectivelyInverted() ) {
        delete fullQueryComparison.invert;
    }

    return this.getItems().filter( function ( item ) {
        return OO.compare(
            item.getCombinedData(),
            fullQueryComparison
        );
    } )[ 0 ];
};

/**
 * Get query by its identifier
 *
 * @ignore
 * @param {string} queryID Query identifier
 * @return {mw.rcfilters.dm.SavedQueryItemModel|undefined} Item matching
 *  the search. Undefined if not found.
 */
SavedQueriesModel.prototype.getItemByID = function ( queryID ) {
    return this.getItems().filter( function ( item ) {
        return item.getID() === queryID;
    } )[ 0 ];
};

/**
 * Get the full data representation of the default query, if it exists
 *
 * @return {Object|null} Representation of the default params if exists.
 *  Null if default doesn't exist or if the user is not logged in.
 */
SavedQueriesModel.prototype.getDefaultParams = function () {
    return ( !mw.user.isAnon() && this.getItemParams( this.getDefault() ) ) || {};
};

/**
 * Get a full parameter representation of an item data
 *
 * @param  {Object} queryID Query ID
 * @return {Object} Parameter representation
 */
SavedQueriesModel.prototype.getItemParams = function ( queryID ) {
    var item = this.getItemByID( queryID ),
        data = item ? item.getData() : {};

    return !$.isEmptyObject( data ) ? this.buildParamsFromData( data ) : {};
};

/**
 * Build a full parameter representation given item data and model sticky values state
 *
 * @param  {Object} data Item data
 * @return {Object} Full param representation
 */
SavedQueriesModel.prototype.buildParamsFromData = function ( data ) {
    data = data || {};
    // Return parameter representation
    return this.filtersModel.getMinimizedParamRepresentation( $.extend( true, {},
        data.params,
        data.highlights
    ) );
};

/**
 * Get the object representing the state of the entire model and items
 *
 * @return {Object} Object representing the state of the model and items
 */
SavedQueriesModel.prototype.getState = function () {
    var obj = { queries: {}, version: '2' };

    // Translate the items to the saved object
    this.getItems().forEach( function ( item ) {
        obj.queries[ item.getID() ] = item.getState();
    } );

    if ( this.getDefault() ) {
        obj.default = this.getDefault();
    }

    return obj;
};

/**
 * Set a default query. Null to unset default.
 *
 * @param {string} itemID Query identifier
 * @fires default
 */
SavedQueriesModel.prototype.setDefault = function ( itemID ) {
    if ( this.default !== itemID ) {
        this.default = itemID;

        // Set for individual itens
        this.getItems().forEach( function ( item ) {
            item.toggleDefault( item.getID() === itemID );
        } );

        this.emit( 'default', itemID );
    }
};

/**
 * Get the default query ID
 *
 * @return {string} Default query identifier
 */
SavedQueriesModel.prototype.getDefault = function () {
    return this.default;
};

/**
 * Check if the saved queries were converted
 *
 * @return {boolean} Saved queries were converted from the previous
 *  version to the new version
 */
SavedQueriesModel.prototype.isConverted = function () {
    return this.converted;
};

module.exports = SavedQueriesModel;