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

Product Query: Add order by “best selling” as a preset #7687

Merged
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
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