Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Commit

Permalink
Product Query: Add order by “best selling” as a preset (#7687)
Browse files Browse the repository at this point in the history
* Add support for “Popular Presets” for PQ block

This commits achieves the following:

* Adds a section in the inspector control called “Popular Presets”,
which contains a dropdown with popular presets.
* Adds support for the first preset: “Best selling products”.
By selecting this, users can sort products by total sales.
* Switches the order of the custom inspector controls and the default
Query Loop inspector controls: our controls will be now on top
as per the latest design spec (see pdnLyh-2By-p2).
* Restricts the allowed Query parameters to the sort orders we want to
allow according to the latest design spec (disabling title and date).
* Removes the core “Order By” dropdown.
* Refactor `setCustomQueryAttribute` to `setQueryAttribute` because
since a few iterations, our custom query attributes are not deeply nested
anymore, and this function can be used for the normal query too.
* Add back-end support for sorting by Best Selling via the Product Query block
* Adds the `popularity` value as an allowed value for `orderby` on
`product` REST API calls.
* Handles the query differently if the `orderby` value is one among the
custom ones.
  • Loading branch information
sunyatasattva authored Nov 21, 2022
1 parent 28fd155 commit 03da591
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 24 deletions.
8 changes: 6 additions & 2 deletions assets/js/blocks/product-query/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ function objectOmit< T, K extends keyof T >( obj: T, key: K ) {

export const QUERY_LOOP_ID = 'core/query';

export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'order', 'taxQuery', 'search' ];
export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'taxQuery', 'search' ];

export const ALL_PRODUCT_QUERY_CONTROLS = [ 'onSale', 'stockStatus' ];
export const ALL_PRODUCT_QUERY_CONTROLS = [
'presets',
'onSale',
'stockStatus',
];

export const DEFAULT_ALLOWED_CONTROLS = [
...DEFAULT_CORE_ALLOWED_CONTROLS,
Expand Down
23 changes: 12 additions & 11 deletions assets/js/blocks/product-query/inspector-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import {
} from './types';
import {
isWooQueryBlockVariation,
setCustomQueryAttribute,
setQueryAttribute,
useAllowedControls,
} from './utils';
import {
ALL_PRODUCT_QUERY_CONTROLS,
QUERY_LOOP_ID,
STOCK_STATUS_OPTIONS,
} from './constants';
import { PopularPresets } from './inspector-controls/popular-presets';

const NAMESPACED_CONTROLS = ALL_PRODUCT_QUERY_CONTROLS.map(
( id ) =>
Expand Down Expand Up @@ -82,7 +83,7 @@ function getStockStatusIdByLabel( statusLabel: FormTokenField.Value ) {
)?.[ 0 ];
}

export const INSPECTOR_CONTROLS = {
export const TOOLS_PANEL_CONTROLS = {
onSale: ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

Expand All @@ -98,7 +99,7 @@ export const INSPECTOR_CONTROLS = {
) }
checked={ query.__woocommerceOnSale || false }
onChange={ ( __woocommerceOnSale ) => {
setCustomQueryAttribute( props, {
setQueryAttribute( props, {
__woocommerceOnSale,
} );
} }
Expand All @@ -124,7 +125,7 @@ export const INSPECTOR_CONTROLS = {
.map( getStockStatusIdByLabel )
.filter( Boolean ) as string[];

setCustomQueryAttribute( props, {
setQueryAttribute( props, {
__woocommerceStockStatus,
} );
} }
Expand Down Expand Up @@ -154,29 +155,29 @@ export const withProductQueryControls =

return isWooQueryBlockVariation( props ) ? (
<>
<BlockEdit { ...props } />
<InspectorControls>
{ allowedControls?.includes( 'presets' ) && (
<PopularPresets { ...props } />
) }
<ToolsPanel
class="woocommerce-product-query-toolspanel"
label={ __(
'Product filters',
'Advanced Filters',
'woo-gutenberg-products-block'
) }
resetAll={ () => {
setCustomQueryAttribute(
props,
defaultWooQueryParams
);
setQueryAttribute( props, defaultWooQueryParams );
} }
>
{ Object.entries( INSPECTOR_CONTROLS ).map(
{ Object.entries( TOOLS_PANEL_CONTROLS ).map(
( [ key, Control ] ) =>
allowedControls?.includes( key ) ? (
<Control { ...props } />
) : null
) }
</ToolsPanel>
</InspectorControls>
<BlockEdit { ...props } />
</>
) : (
<BlockEdit { ...props } />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* External dependencies
*/
import { CustomSelectControl, PanelBody } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { ProductQueryBlock, ProductQueryBlockQuery } from '../types';
import { setQueryAttribute } from '../utils';

const PRESETS = [
{ key: 'date/desc', name: __( 'Newest', 'woo-gutenberg-products-block' ) },
{
key: 'popularity/desc',
name: __( 'Best Selling', 'woo-gutenberg-products-block' ),
},
];

export function PopularPresets( props: ProductQueryBlock ) {
const { query } = props.attributes;

return (
<PanelBody
className="woocommerce-product-query-panel__sort"
title={ __( 'Popular Filters', 'woo-gutenberg-products-block' ) }
initialOpen={ true }
>
<p>
{ __(
'Arrange products by popular pre-sets.',
'woo-gutenberg-products-block'
) }
</p>
<CustomSelectControl
hideLabelFromVision={ true }
label={ __(
'Choose among these pre-sets',
'woo-gutenberg-products-block'
) }
onChange={ ( option ) => {
if ( ! option.selectedItem?.key ) return;

const [ orderBy, order ] = option.selectedItem?.key?.split(
'/'
) as [
ProductQueryBlockQuery[ 'orderBy' ],
ProductQueryBlockQuery[ 'order' ]
];

setQueryAttribute( props, { order, orderBy } );
} }
options={ PRESETS }
value={ PRESETS.find(
( option ) =>
option.key === `${ query.orderBy }/${ query.order }`
) }
/>
</PanelBody>
);
}
15 changes: 13 additions & 2 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import type { EditorBlock } from '@woocommerce/types';
// will help signify our intentions.
/* eslint-disable @typescript-eslint/naming-convention */
export interface ProductQueryArguments {
/**
* Available sorting options specific to the Product Query block
*
* Other sorting options may be possible, but we are restricting
* the choice to those.
*/
orderBy: 'date' | 'popularity';
/**
* Display only products on sale.
*
Expand Down Expand Up @@ -52,7 +59,11 @@ export interface ProductQueryArguments {

export type ProductQueryBlock = EditorBlock< QueryBlockAttributes >;

export type ProductQueryBlockQuery = QueryBlockQuery & ProductQueryArguments;
export type ProductQueryBlockQuery = Omit<
QueryBlockQuery,
keyof ProductQueryArguments
> &
ProductQueryArguments;

export interface QueryBlockAttributes {
allowedControls?: string[];
Expand Down Expand Up @@ -81,7 +92,7 @@ export interface QueryBlockQuery {
}

export interface ProductQueryContext {
query?: QueryBlockQuery & ProductQueryArguments;
query?: ProductQueryBlockQuery;
queryId?: number;
}

Expand Down
11 changes: 4 additions & 7 deletions assets/js/blocks/product-query/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { store as WP_BLOCKS_STORE } from '@wordpress/blocks';
*/
import { QUERY_LOOP_ID } from './constants';
import {
ProductQueryArguments,
ProductQueryBlock,
ProductQueryBlockQuery,
QueryVariation,
} from './types';

Expand Down Expand Up @@ -40,14 +40,11 @@ export function isWooQueryBlockVariation( block: ProductQueryBlock ) {
/**
* Sets the new query arguments of a Product Query block
*
* Because we add a new set of deeply nested attributes to the query
* block, this utility function makes it easier to change just the
* options relating to our custom query, while keeping the code
* clean.
* Shorthand for setting new nested query parameters.
*/
export function setCustomQueryAttribute(
export function setQueryAttribute(
block: ProductQueryBlock,
queryParams: Partial< ProductQueryArguments >
queryParams: Partial< ProductQueryBlockQuery >
) {
const { query } = block.attributes;

Expand Down
57 changes: 55 additions & 2 deletions src/BlockTypes/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key

/**
* ProductQuery class.
Expand All @@ -22,6 +23,13 @@ class ProductQuery extends AbstractBlock {
*/
protected $parsed_block;

/**
* Orderby options not natively supported by WordPress REST API
*
* @var array
*/
protected $custom_order_opts = array( 'popularity' );

/**
* All the query args related to the filter by attributes block.
*
Expand All @@ -46,6 +54,7 @@ protected function initialize() {
2
);
add_filter( 'rest_product_query', array( $this, 'update_rest_query' ), 10, 2 );
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
}

/**
Expand Down Expand Up @@ -94,8 +103,9 @@ public function update_query( $pre_render, $parsed_block ) {
*/
public function update_rest_query( $args, $request ) {
$on_sale_query = $request->get_param( '__woocommerceOnSale' ) !== 'true' ? array() : $this->get_on_sale_products_query();
$orderby_query = $this->get_custom_orderby_query( $request->get_param( 'orderby' ) );

return array_merge( $args, $on_sale_query );
return array_merge( $args, $on_sale_query, $orderby_query );
}

/**
Expand Down Expand Up @@ -124,6 +134,12 @@ public function build_query( $query ) {

$queries_by_attributes = $this->get_queries_by_attributes( $parsed_block );
$queries_by_filters = $this->get_queries_by_applied_filters();
$orderby_query = $this->get_custom_orderby_query( $query['orderby'] );

$base_query = array_merge(
$common_query_values,
$orderby_query
);

return array_reduce(
array_merge(
Expand All @@ -133,7 +149,7 @@ public function build_query( $query ) {
function( $acc, $query ) {
return $this->merge_queries( $acc, $query );
},
$common_query_values
$base_query
);
}

Expand Down Expand Up @@ -182,6 +198,21 @@ private function merge_queries( $a, $b ) {
return $a;
}

/**
* Extends allowed `collection_params` for the REST API
*
* By itself, the REST API doesn't accept custom `orderby` values,
* even if they are supported by a custom post type.
*
* @param array $params A list of allowed `orderby` values.
*
* @return array
*/
public function extend_rest_query_allowed_params( $params ) {
$params['orderby']['enum'] = array_merge( $params['orderby']['enum'], $this->custom_order_opts );
return $params;
}

/**
* Return a query for on sale products.
*
Expand All @@ -193,6 +224,28 @@ private function get_on_sale_products_query() {
);
}

/**
* Return query params to support custom sort values
*
* @param string $orderby Sort order option.
*
* @return array
*/
private function get_custom_orderby_query( $orderby ) {
if ( ! in_array( $orderby, $this->custom_order_opts, true ) ) {
return array( 'orderby' => $orderby );
}

$meta_keys = array(
'popularity' => 'total_sales',
);

return array(
'meta_key' => $meta_keys[ $orderby ],
'orderby' => 'meta_value_num',
);
}

/**
* Return a query for products depending on their stock status.
*
Expand Down

0 comments on commit 03da591

Please sign in to comment.