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-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';
|
||||||
|
|
|
@ -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,
|
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,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue