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:
parent
c595444b7f
commit
c55a387657
|
@ -3,4 +3,5 @@ export * from './use-shallow-equal';
|
|||
export * from './use-store-products';
|
||||
export * from './use-collection';
|
||||
export * from './use-collection-header';
|
||||
export * from './use-collection-data';
|
||||
export * from './use-previous';
|
||||
|
|
|
@ -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,
|
||||
} );
|
||||
};
|
|
@ -5,6 +5,7 @@ import {
|
|||
useCollection,
|
||||
useQueryStateByKey,
|
||||
useQueryStateByContext,
|
||||
useCollectionData,
|
||||
} from '@woocommerce/base-hooks';
|
||||
import {
|
||||
useCallback,
|
||||
|
@ -91,32 +92,6 @@ const AttributeFilterBlock = ( {
|
|||
.flatMap( ( attribute ) => attribute.slug );
|
||||
}, [ 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 {
|
||||
results: attributeTerms,
|
||||
isLoading: attributeTermsLoading,
|
||||
|
@ -130,11 +105,12 @@ const AttributeFilterBlock = ( {
|
|||
const {
|
||||
results: filteredCounts,
|
||||
isLoading: filteredCountsLoading,
|
||||
} = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products/collection-data',
|
||||
query: filteredCountsQueryState,
|
||||
shouldSelect: blockAttributes.attributeId > 0,
|
||||
} = useCollectionData( {
|
||||
queryAttribute: {
|
||||
taxonomy: attributeObject.taxonomy,
|
||||
queryType: blockAttributes.queryType,
|
||||
},
|
||||
queryState,
|
||||
} );
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
useCollection,
|
||||
useQueryStateByKey,
|
||||
useQueryStateByContext,
|
||||
useCollectionData,
|
||||
} from '@woocommerce/base-hooks';
|
||||
import { Fragment, useCallback, useState, useEffect } from '@wordpress/element';
|
||||
import PriceSlider from '@woocommerce/base-components/price-slider';
|
||||
|
@ -22,19 +22,9 @@ const PriceFilterBlock = ( { attributes, isPreview = false } ) => {
|
|||
'max_price'
|
||||
);
|
||||
const [ queryState ] = useQueryStateByContext();
|
||||
const { results, isLoading } = useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products/collection-data',
|
||||
query: {
|
||||
...queryState,
|
||||
min_price: undefined,
|
||||
max_price: undefined,
|
||||
orderby: undefined,
|
||||
order: undefined,
|
||||
per_page: undefined,
|
||||
page: undefined,
|
||||
calculate_price_range: true,
|
||||
},
|
||||
const { results, isLoading } = useCollectionData( {
|
||||
queryPrices: true,
|
||||
queryState,
|
||||
} );
|
||||
|
||||
const [ minPrice, setMinPrice ] = useState();
|
||||
|
|
|
@ -143,27 +143,71 @@ class ProductCollectionData extends RestContoller {
|
|||
$filters = new ProductQueryFilters();
|
||||
|
||||
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['max_price'] = $price_results->max_price;
|
||||
}
|
||||
|
||||
if ( ! empty( $request['calculate_attribute_counts'] ) ) {
|
||||
$return['attribute_counts'] = [];
|
||||
$counts = $filters->get_attribute_counts( $request, $request['calculate_attribute_counts'] );
|
||||
$taxonomy__or_queries = [];
|
||||
$taxonomy__and_queries = [];
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$return['attribute_counts'][] = [
|
||||
'term' => $key,
|
||||
'count' => $value,
|
||||
];
|
||||
foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) {
|
||||
if ( 'or' === $attributes_to_count['query_type'] ) {
|
||||
$taxonomy__or_queries[] = $attributes_to_count['taxonomy'];
|
||||
} 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'] ) ) {
|
||||
$return['rating_counts'] = [];
|
||||
$counts = $filters->get_rating_counts( $request );
|
||||
$return['rating_counts'] = [];
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$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' ),
|
||||
'type' => '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(),
|
||||
);
|
||||
|
|
|
@ -100,15 +100,15 @@ This endpoint allows you to get aggregate data from a collection of products, fo
|
|||
```http
|
||||
GET /products/collection-data
|
||||
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
|
||||
```
|
||||
|
||||
| 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_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_rating_counts` | bool | No | Returns the counts of products with a certain average rating, 1-5. If false, only `null` will be returned. |
|
||||
| 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_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. |
|
||||
|
||||
**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.
|
||||
|
||||
|
|
|
@ -64,6 +64,12 @@ class ProductQueryFilters {
|
|||
public function get_attribute_counts( $request, $attributes = [] ) {
|
||||
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.
|
||||
$product_query = new ProductQuery();
|
||||
|
||||
|
|
|
@ -107,7 +107,15 @@ class ProductCollectionData extends TestCase {
|
|||
ProductHelper::create_variation_product();
|
||||
|
||||
$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 );
|
||||
$data = $response->get_data();
|
||||
|
||||
|
|
Loading…
Reference in New Issue