Skip to content

Commit

Permalink
Merge pull request #10014 from Automattic/add/theme-query-manager-rec…
Browse files Browse the repository at this point in the history
…eive-method

ThemeQueryManager: Implement receive()
  • Loading branch information
ockham authored Dec 13, 2016
2 parents a8171b2 + ebc44e4 commit fe7736e
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 328 deletions.
116 changes: 114 additions & 2 deletions client/lib/query-manager/theme/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,128 @@
/**
* External dependencies
*/
import { cloneDeep, get, isEqual, keyBy, range } from 'lodash';

/**
* Internal dependencies
*/
import PaginatedQueryManager from '../paginated';
import ThemeQueryKey from './key';
import { isThemeMatchingQuery } from './util';
import { DEFAULT_THEME_QUERY } from './constants';

/**
* ThemeQueryManager manages themes which can be queried
*/
export default class ThemeQueryManager extends PaginatedQueryManager {
matches = isThemeMatchingQuery;
/**
* Signal that an item(s) has been received for tracking. Optionally
* specify that items received are intended for patch application, or that
* they are associated with a query. This function does not mutate the
* instance state. Instead, it returns a new instance of ThemeQueryManager if
* the tracked items have been modified, or the current instance otherwise.
*
* Note that we implement our own receive() method instead of just relying on
* that of the base class. We choose to override the base class's receive()
* so the results are kept in the order they are received from the endpoint.
* The themes query REST API endpoint uses ElasticSearch to sort results by
* relevancy, which we cannot easily mimick on the client side.
*
* @param {(Array|Object)} items Item(s) to be received
* @param {Object} options Options for receive
* @param {Boolean} options.patch Apply changes as partial
* @param {Object} options.query Query set to set or replace
* @param {Boolean} options.mergeQuery Add to existing query set
* @param {Number} options.found Total found items for query
* @return {QueryManager} New instance if changed, or
* same instance otherwise
*/
receive( items, options = {} ) {
// Create the updated manager based on this instance, appending the newly received items
const nextManager = new this.constructor(
{
...this.data,
items: {
...this.data.items,
...keyBy( items, this.options.itemKey )
},
queries: this.data.queries
},
this.options
);

// If manager is the same instance, assume no changes have been made
if ( this === nextManager ) {
return nextManager;
}

// If no query was passed, return the QueryManager with only the new items appended
if ( ! options.query ) {
return nextManager;
}

// If we're already storing the query and associated items, return this instance.
if ( isEqual( super.getItems( options.query ), items ) ) {
return this;
}

const queryKey = this.constructor.QueryKey.stringify( options.query );
const page = options.query.page || this.constructor.DEFAULT_QUERY.page;
const perPage = options.query.number || this.constructor.DEFAULT_QUERY.number;
const startOffset = ( page - 1 ) * perPage;
const nextQuery = get( this.data.queries, queryKey, { itemKeys: [], found: options.found } );

// Coerce received single item to array
if ( ! Array.isArray( items ) ) {
items = [ items ];
}

// If the item set for the queried page is identical, there are no
// updates to be made
const pageItemKeys = items.map( ( item ) => item[ this.options.itemKey ] );

// If we've reached this point, we know that we've received a paged
// set of data where our assumed item set is incorrect.
const modifiedNextQuery = cloneDeep( nextQuery );

// Found count is not always reliable, usually in consideration of user
// capabilities. If we receive a set of items with a count not matching
// the expected number for the query, we recalculate the found value to
// reflect that this is the last set we can expect to receive. Found is
// correct only if the count of items matches expected query number.
if ( modifiedNextQuery.hasOwnProperty( 'found' ) && perPage !== items.length ) {
// Otherwise, found count should be corrected to equal the number
// of items received added to the summed per page total. Note that
// we can reach this point if receiving the last page of items, but
// the updated value should still be correct given this logic.
modifiedNextQuery.found = ( ( page - 1 ) * perPage ) + items.length;
}

// If found is known from options, ensure that we fill the end of the
// array with undefined entries until found count
if ( modifiedNextQuery.hasOwnProperty( 'found' ) ) {
modifiedNextQuery.itemKeys = range( 0, modifiedNextQuery.found ).map( ( index ) => {
return modifiedNextQuery.itemKeys[ index ];
} );
}

// Splice results into their proper place
modifiedNextQuery.itemKeys.splice( startOffset, perPage, ...pageItemKeys );

return new this.constructor(
{
...this.data,
items: {
...this.data.items,
...keyBy( items, this.options.itemKey )
},
queries: {
...this.data.queries,
[ queryKey ]: modifiedNextQuery
}
},
this.options
);
}
}

ThemeQueryManager.QueryKey = ThemeQueryKey;
Expand Down
211 changes: 3 additions & 208 deletions client/lib/query-manager/theme/test/index.js
Original file line number Diff line number Diff line change
@@ -1,222 +1,17 @@
/**
* External dependencies
*/
import { expect } from 'chai';
//import { expect } from 'chai';

/**
* Internal dependencies
*/
import ThemeQueryManager from '../';
//import ThemeQueryManager from '../';

/**
* Constants
*/
const DEFAULT_THEME = {
name: 'Twenty Something',
author: 'the WordPress team',
screenshot: 'https://i0.wp.com/theme.wordpress.com/wp-content/themes/pub/twentysomething/screenshot.png',
screenshots: [ 'https://i0.wp.com/theme.files.wordpress.com/2015/12/twentysomething-featured-image.jpg?ssl=1' ],
stylesheet: 'pub/twentysomething',
taxonomies: {
theme_subject: [
{
name: 'Blog',
slug: 'blog',
term_id: '273'
},
{
name: 'Lifestream',
slug: 'lifestream',
term_id: '652270'
},
{
name: 'Journal',
slug: 'journal',
term_id: '96'
}
],
theme_color: [
{
name: 'Black',
slug: 'black',
term_id: '59007'
},
{
name: 'Blue',
slug: 'blue',
term_id: '9150'
},
{
name: 'Gray',
slug: 'gray',
term_id: '147520'
}
]
},
demo_uri: 'https://twentysomethingdemo.wordpress.com/',
descriptionLong: 'The annual WordPress theme for this year is a modern take on an ever-popular layout. ' +
'The horizontal header area with an optional right sidebar works perfectly for both blogs <em>and</em> websites.',
description: 'This is a modernized take on an ever-popular WordPress layout' +
' — the horizontal masthead with an optional right sidebar that works perfectly for blogs and websites.'
};

describe( 'ThemeQueryManager', () => {
let manager;
beforeEach( () => {
manager = new ThemeQueryManager();
} );

describe( '#matches()', () => {
context( 'query.search', () => {
it( 'should return false for a non-matching search', () => {
const isMatch = manager.matches( {
search: 'nonexisting'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );

it( 'should return true for a matching title search', () => {
const isMatch = manager.matches( {
search: 'Twenty'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return true for a falsey title search', () => {
const isMatch = manager.matches( {
search: null
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return true for a matching content search', () => {
const isMatch = manager.matches( {
search: 'modern'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return true for a matching author search', () => {
const isMatch = manager.matches( {
search: 'team'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return true for a matching filter search', () => {
const isMatch = manager.matches( {
search: 'journal'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should search case-insensitive', () => {
const isMatch = manager.matches( {
search: 'Sidebar'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should separately test title and content fields', () => {
const isMatch = manager.matches( {
search: 'TwentyThe'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );
} );

context( 'query.filters', () => {
it( 'should return false if theme does not include filter', () => {
const isMatch = manager.matches( {
filters: 'nosuchfilter'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );

it( 'should return false on a partial match', () => {
const isMatch = manager.matches( {
filters: 'ourna'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );

it( 'should return true if theme includes filter', () => {
const isMatch = manager.matches( {
filters: 'journal'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

context( 'with multiple filters from a single taxonomy', () => {
it( 'should return false if theme doesn\'t match all filters', () => {
const isMatch = manager.matches( {
filters: 'journal,business'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );
it( 'should return true if theme matches all filters', () => {
const isMatch = manager.matches( {
filters: 'journal,blog'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );
} );

context( 'with multiple filters from different taxonomies', () => {
it( 'should return false if theme doesn\'t match all filters', () => {
const isMatch = manager.matches( {
filters: 'journal,green'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );
it( 'should return true if theme matches all filters', () => {
const isMatch = manager.matches( {
filters: 'journal,black'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );
} );
} );

context( 'query.tier', () => {
it( 'should return true for a free theme when querying for all themes', () => {
const isMatch = manager.matches( {
tier: ''
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return true for a free theme when querying for free themes', () => {
const isMatch = manager.matches( {
tier: 'free'
}, DEFAULT_THEME );

expect( isMatch ).to.be.true;
} );

it( 'should return false for a free theme when querying for premium themes', () => {
const isMatch = manager.matches( {
tier: 'premium'
}, DEFAULT_THEME );

expect( isMatch ).to.be.false;
} );
} );
} );
// TODO
} );
41 changes: 0 additions & 41 deletions client/lib/query-manager/theme/test/util.js

This file was deleted.

Loading

0 comments on commit fe7736e

Please sign in to comment.