Prevent multiple collection data requests using debounce (https://github.com/woocommerce/woocommerce-blocks/pull/1233)

* Add context for collection data query

* Introduce useCollectionData hook

* Implement hook in filter blocks

* Update API to handle nuances of counts instead of client side

* Clone requests so original is untouched

* Prevent dupe requests is working

* Cleanup

* Update assets/js/base/hooks/use-collection-data.js

Co-Authored-By: Albert Juhé Lluveras <aljullu@gmail.com>

* Update assets/js/base/hooks/use-collection-data.js

Co-Authored-By: Albert Juhé Lluveras <aljullu@gmail.com>

* Feedback

* Remove context
This commit is contained in:
Mike Jolley 2019-11-23 17:04:30 +00:00 committed by GitHub
parent c595444b7f
commit c55a387657
8 changed files with 226 additions and 62 deletions

View File

@ -3,4 +3,5 @@ export * from './use-shallow-equal';
export * from './use-store-products'; export * from './use-store-products';
export * from './use-collection'; export * from './use-collection';
export * from './use-collection-header'; export * from './use-collection-header';
export * from './use-collection-data';
export * from './use-previous'; export * from './use-previous';

View File

@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { useState, useEffect, useMemo } from '@wordpress/element';
import {
useQueryStateByContext,
useQueryStateByKey,
useCollection,
} from '@woocommerce/base-hooks';
import { useQueryStateContext } from '@woocommerce/base-context/query-state-context';
import { useDebounce } from 'use-debounce';
import { sortBy } from 'lodash';
/**
* Internal dependencies
*/
import { useShallowEqual } from './use-shallow-equal';
const buildCollectionDataQuery = ( collectionDataQueryState ) => {
const query = collectionDataQueryState;
if ( collectionDataQueryState.calculate_attribute_counts ) {
query.calculate_attribute_counts = sortBy(
collectionDataQueryState.calculate_attribute_counts.map(
( { taxonomy, queryType } ) => {
return {
taxonomy,
query_type: queryType,
};
}
),
[ 'taxonomy', 'query_type' ]
);
}
return query;
};
export const useCollectionData = ( {
queryAttribute,
queryPrices,
queryState,
} ) => {
let context = useQueryStateContext();
context = `${ context }-collection-data`;
const [ collectionDataQueryState ] = useQueryStateByContext( context );
const [
calculateAttributesQueryState,
setCalculateAttributesQueryState,
] = useQueryStateByKey( 'calculate_attribute_counts', [], context );
const [
calculatePriceRangeQueryState,
setCalculatePriceRangeQueryState,
] = useQueryStateByKey( 'calculate_price_range', null, context );
const currentQueryAttribute = useShallowEqual( queryAttribute || {} );
const currentQueryPrices = useShallowEqual( queryPrices );
useEffect( () => {
if (
typeof currentQueryAttribute === 'object' &&
Object.keys( currentQueryAttribute ).length
) {
const foundAttribute = calculateAttributesQueryState.find(
( attribute ) => {
return (
attribute.taxonomy === currentQueryAttribute.taxonomy
);
}
);
if ( ! foundAttribute ) {
setCalculateAttributesQueryState( [
...calculateAttributesQueryState,
currentQueryAttribute,
] );
}
}
}, [
currentQueryAttribute,
calculateAttributesQueryState,
setCalculateAttributesQueryState,
] );
useEffect( () => {
if (
calculatePriceRangeQueryState !== currentQueryPrices &&
currentQueryPrices !== undefined
) {
setCalculatePriceRangeQueryState( currentQueryPrices );
}
}, [
currentQueryPrices,
setCalculatePriceRangeQueryState,
calculatePriceRangeQueryState,
] );
// Defer the select query so all collection-data query vars can be gathered.
const [ shouldSelect, setShouldSelect ] = useState( false );
const [ debouncedShouldSelect ] = useDebounce( shouldSelect, 200 );
if ( ! shouldSelect ) {
setShouldSelect( true );
}
const collectionDataQueryVars = useMemo( () => {
return buildCollectionDataQuery( collectionDataQueryState );
}, [ collectionDataQueryState ] );
return useCollection( {
namespace: '/wc/store',
resourceName: 'products/collection-data',
query: {
...queryState,
page: undefined,
per_page: undefined,
orderby: undefined,
order: undefined,
...collectionDataQueryVars,
},
shouldSelect: debouncedShouldSelect,
} );
};

View File

@ -5,6 +5,7 @@ import {
useCollection, useCollection,
useQueryStateByKey, useQueryStateByKey,
useQueryStateByContext, useQueryStateByContext,
useCollectionData,
} from '@woocommerce/base-hooks'; } from '@woocommerce/base-hooks';
import { import {
useCallback, useCallback,
@ -91,32 +92,6 @@ const AttributeFilterBlock = ( {
.flatMap( ( attribute ) => attribute.slug ); .flatMap( ( attribute ) => attribute.slug );
}, [ productAttributesQuery, attributeObject ] ); }, [ productAttributesQuery, attributeObject ] );
const filteredCountsQueryState = useMemo( () => {
// If doing an "AND" query, we need to remove current taxonomy query so counts are not affected.
const modifiedQueryState =
blockAttributes.queryType === 'or'
? productAttributesQuery.filter(
( item ) => item.attribute !== attributeObject.taxonomy
)
: productAttributesQuery;
// Take current query and remove paging args.
return {
...queryState,
orderby: undefined,
order: undefined,
per_page: undefined,
page: undefined,
attributes: modifiedQueryState,
calculate_attribute_counts: [ attributeObject.taxonomy ],
};
}, [
queryState,
attributeObject,
blockAttributes,
productAttributesQuery,
] );
const { const {
results: attributeTerms, results: attributeTerms,
isLoading: attributeTermsLoading, isLoading: attributeTermsLoading,
@ -130,11 +105,12 @@ const AttributeFilterBlock = ( {
const { const {
results: filteredCounts, results: filteredCounts,
isLoading: filteredCountsLoading, isLoading: filteredCountsLoading,
} = useCollection( { } = useCollectionData( {
namespace: '/wc/store', queryAttribute: {
resourceName: 'products/collection-data', taxonomy: attributeObject.taxonomy,
query: filteredCountsQueryState, queryType: blockAttributes.queryType,
shouldSelect: blockAttributes.attributeId > 0, },
queryState,
} ); } );
/** /**

View File

@ -2,9 +2,9 @@
* External dependencies * External dependencies
*/ */
import { import {
useCollection,
useQueryStateByKey, useQueryStateByKey,
useQueryStateByContext, useQueryStateByContext,
useCollectionData,
} from '@woocommerce/base-hooks'; } from '@woocommerce/base-hooks';
import { Fragment, useCallback, useState, useEffect } from '@wordpress/element'; import { Fragment, useCallback, useState, useEffect } from '@wordpress/element';
import PriceSlider from '@woocommerce/base-components/price-slider'; import PriceSlider from '@woocommerce/base-components/price-slider';
@ -22,19 +22,9 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
'max_price' 'max_price'
); );
const [ queryState ] = useQueryStateByContext(); const [ queryState ] = useQueryStateByContext();
const { results, isLoading } = useCollection( { const { results, isLoading } = useCollectionData( {
namespace: '/wc/store', queryPrices: true,
resourceName: 'products/collection-data', queryState,
query: {
...queryState,
min_price: undefined,
max_price: undefined,
orderby: undefined,
order: undefined,
per_page: undefined,
page: undefined,
calculate_price_range: true,
},
} ); } );
const [ minPrice, setMinPrice ] = useState(); const [ minPrice, setMinPrice ] = useState();

View File

@ -143,27 +143,71 @@ class ProductCollectionData extends RestContoller {
$filters = new ProductQueryFilters(); $filters = new ProductQueryFilters();
if ( ! empty( $request['calculate_price_range'] ) ) { if ( ! empty( $request['calculate_price_range'] ) ) {
$price_results = $filters->get_filtered_price( $request ); $filter_request = clone $request;
$filter_request->set_param( 'min_price', null );
$filter_request->set_param( 'max_price', null );
$price_results = $filters->get_filtered_price( $filter_request );
$return['min_price'] = $price_results->min_price; $return['min_price'] = $price_results->min_price;
$return['max_price'] = $price_results->max_price; $return['max_price'] = $price_results->max_price;
} }
if ( ! empty( $request['calculate_attribute_counts'] ) ) { if ( ! empty( $request['calculate_attribute_counts'] ) ) {
$return['attribute_counts'] = []; $taxonomy__or_queries = [];
$counts = $filters->get_attribute_counts( $request, $request['calculate_attribute_counts'] ); $taxonomy__and_queries = [];
foreach ( $counts as $key => $value ) { foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) {
$return['attribute_counts'][] = [ if ( 'or' === $attributes_to_count['query_type'] ) {
'term' => $key, $taxonomy__or_queries[] = $attributes_to_count['taxonomy'];
'count' => $value, } else {
]; $taxonomy__and_queries[] = $attributes_to_count['taxonomy'];
}
}
$return['attribute_counts'] = [];
// Or type queries need special handling because the attribute, if set, needs removing from the query first otherwise counts would not be correct.
if ( $taxonomy__or_queries ) {
foreach ( $taxonomy__or_queries as $taxonomy ) {
$filter_request = clone $request;
$filter_attributes = $filter_request->get_param( 'attributes' );
if ( ! empty( $filter_attributes ) ) {
$filter_attributes = array_filter(
$filter_attributes,
function( $query ) {
return $query['attribute'] !== $taxonomy;
}
);
}
$filter_request->set_param( 'attributes', $filter_attributes );
$counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] );
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
}
}
}
if ( $taxonomy__and_queries ) {
$counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries );
foreach ( $counts as $key => $value ) {
$return['attribute_counts'][] = [
'term' => $key,
'count' => $value,
];
}
} }
} }
if ( ! empty( $request['calculate_rating_counts'] ) ) { if ( ! empty( $request['calculate_rating_counts'] ) ) {
$return['rating_counts'] = [];
$counts = $filters->get_rating_counts( $request ); $counts = $filters->get_rating_counts( $request );
$return['rating_counts'] = [];
foreach ( $counts as $key => $value ) { foreach ( $counts as $key => $value ) {
$return['rating_counts'][] = [ $return['rating_counts'][] = [
@ -194,7 +238,22 @@ class ProductCollectionData extends RestContoller {
'description' => __( 'If requested, calculates attribute term counts for products in the collection.', 'woo-gutenberg-products-block' ), 'description' => __( 'If requested, calculates attribute term counts for products in the collection.', 'woo-gutenberg-products-block' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
'type' => 'string', 'type' => 'object',
'properties' => array(
'taxonomy' => array(
'description' => __( 'Taxonomy name.', 'woo-gutenberg-products-block' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'query_type' => array(
'description' => __( 'Query type being performed which may affect counts. Valid values include "and" and "or".', 'woo-gutenberg-products-block' ),
'type' => 'string',
'enum' => array( 'and', 'or' ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
), ),
'default' => array(), 'default' => array(),
); );

View File

@ -100,15 +100,15 @@ This endpoint allows you to get aggregate data from a collection of products, fo
```http ```http
GET /products/collection-data GET /products/collection-data
GET /products/collection-data?calculate_price_range=true GET /products/collection-data?calculate_price_range=true
GET /products/collection-data?calculate_attribute_counts=pa_size,pa_color GET /products/collection-data?calculate_attribute_counts[0][query_type]=or&calculate_attribute_counts[0][taxonomy]=pa_color
GET /products/collection-data?calculate_rating_counts=true GET /products/collection-data?calculate_rating_counts=true
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| :--------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------- | | :--------------------------- | :----- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. | | `calculate_price_range` | bool | No | Returns the min and max price for the product collection. If false, only `null` will be returned. |
| `calculate_attribute_counts` | string | No | Returns attribute counts for a list of attribute (taxonomy) names you pass in via the parameter. If empty, only `null` will be returned. | | `calculate_attribute_counts` | object | No | Returns attribute counts for a list of attribute taxonomies you pass in via this parameter. Each should be provided as an object with keys "taxonomy" and "query_type". If empty, `null` will be returned. |
| `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. | | `calculate_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. |
**In addition to the above attributes**, all product list attributes are supported. This allows you to get data for a certain subset of products. See [the products API list products section](#list-products) for the full list. **In addition to the above attributes**, all product list attributes are supported. This allows you to get data for a certain subset of products. See [the products API list products section](#list-products) for the full list.

View File

@ -64,6 +64,12 @@ class ProductQueryFilters {
public function get_attribute_counts( $request, $attributes = [] ) { public function get_attribute_counts( $request, $attributes = [] ) {
global $wpdb; global $wpdb;
// Remove paging and sorting params from the request.
$request->set_param( 'page', null );
$request->set_param( 'per_page', null );
$request->set_param( 'order', null );
$request->set_param( 'orderby', null );
// Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products. // Grab the request from the WP Query object, and remove SQL_CALC_FOUND_ROWS and Limits so we get a list of all products.
$product_query = new ProductQuery(); $product_query = new ProductQuery();

View File

@ -107,7 +107,15 @@ class ProductCollectionData extends TestCase {
ProductHelper::create_variation_product(); ProductHelper::create_variation_product();
$request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' ); $request = new WP_REST_Request( 'GET', '/wc/store/products/collection-data' );
$request->set_param( 'calculate_attribute_counts', 'pa_size' ); $request->set_param(
'calculate_attribute_counts',
[
[
'taxonomy' => 'pa_size',
'query_type' => 'and',
]
]
);
$response = $this->server->dispatch( $request ); $response = $this->server->dispatch( $request );
$data = $response->get_data(); $data = $response->get_data();