Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ThemeQueryManager: Implement receive() #10014

Merged
merged 4 commits into from
Dec 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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