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

Commit

Permalink
Implement Hand-Picked Products block (#7925)
Browse files Browse the repository at this point in the history
Implements the ProductSelector advanced filter within the “Products (Beta)” block.
The filter allows the merchant to narrow down the exact products to which all
subsequent filters will be applied, mirroring the functionality of the existing
“Hand-picked Products” plus all the other functionalities available
from the “Products (Beta)” block.
  • Loading branch information
sunyatasattva authored May 8, 2023
1 parent 1832630 commit da19120
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 15 deletions.
11 changes: 2 additions & 9 deletions assets/js/blocks/product-query/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import { objectOmit } from '@woocommerce/utils';
import type { InnerBlockTemplate } from '@wordpress/blocks';

/**
Expand All @@ -12,15 +13,6 @@ import { VARIATION_NAME as PRODUCT_TITLE_ID } from './variations/elements/produc
import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product-template';
import { ImageSizing } from '../../atomic/blocks/product-elements/image/types';

/**
* Returns an object without a key.
*/
function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
const { [ key ]: omit, ...rest } = obj;

return rest;
}

export const EDIT_ATTRIBUTES_URL =
'/wp-admin/edit.php?post_type=product&page=product_attributes';

Expand All @@ -31,6 +23,7 @@ export const DEFAULT_CORE_ALLOWED_CONTROLS = [ 'taxQuery', 'search' ];
export const ALL_PRODUCT_QUERY_CONTROLS = [
'attributes',
'presets',
'productSelector',
'onSale',
'stockStatus',
'wooInherit',
Expand Down
4 changes: 3 additions & 1 deletion assets/js/blocks/product-query/inspector-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ import {
QUERY_LOOP_ID,
STOCK_STATUS_OPTIONS,
} from './constants';
import { PopularPresets } from './inspector-controls/popular-presets';
import { AttributesFilter } from './inspector-controls/attributes-filter';
import { PopularPresets } from './inspector-controls/popular-presets';
import { ProductSelector } from './inspector-controls/product-selector';

import './editor.scss';

Expand Down Expand Up @@ -168,6 +169,7 @@ export const TOOLS_PANEL_CONTROLS = {
</ToolsPanelItem>
);
},
productSelector: ProductSelector,
stockStatus: ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

Expand Down
108 changes: 108 additions & 0 deletions assets/js/blocks/product-query/inspector-controls/product-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { getProducts } from '@woocommerce/editor-components/utils';
import { ProductResponseItem } from '@woocommerce/types';
import { objectOmit } from '@woocommerce/utils';
import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
FormTokenField,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';

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

function useProductsList() {
const [ productsList, setProductsList ] = useState< ProductResponseItem[] >(
[]
);

useEffect( () => {
getProducts( { selected: [] } ).then( ( results ) => {
setProductsList( results as ProductResponseItem[] );
} );
}, [] );

return productsList;
}

export const ProductSelector = ( props: ProductQueryBlock ) => {
const { query } = props.attributes;

const productsList = useProductsList();

const onTokenChange = ( values: FormTokenField.Value[] ) => {
const ids = values
.map(
( nameOrId ) =>
productsList.find(
( product ) =>
product.name === nameOrId ||
product.id === Number( nameOrId )
)?.id
)
.filter( Boolean )
.map( String );

if ( ! ids.length && props.attributes.query.include ) {
const prunedQuery = objectOmit( props.attributes.query, 'include' );

setQueryAttribute(
{
...props,
attributes: {
...props.attributes,
query: prunedQuery,
},
},
{}
);
} else {
setQueryAttribute( props, {
include: ids,
} );
}
};

return (
<ToolsPanelItem
label={ __(
'Hand-picked Products',
'woo-gutenberg-products-block'
) }
hasValue={ () => query.include?.length }
>
<FormTokenField
disabled={ ! productsList.length }
displayTransform={ ( token: string ) =>
Number.isNaN( Number( token ) )
? token
: productsList.find(
( product ) => product.id === Number( token )
)?.name || ''
}
label={ __(
'Pick some products',
'woo-gutenberg-products-block'
) }
onChange={ onTokenChange }
suggestions={ productsList.map( ( product ) => product.name ) }
validateInput={ ( value: string ) =>
productsList.find( ( product ) => product.name === value )
}
value={
! productsList.length
? [ __( 'Loading…', 'woo-gutenberg-products-block' ) ]
: query?.include || []
}
__experimentalExpandOnFocus={ true }
/>
</ToolsPanelItem>
);
};
1 change: 1 addition & 0 deletions assets/js/blocks/product-query/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface QueryBlockAttributes {
export interface QueryBlockQuery {
author?: string;
exclude?: string[];
include?: string[];
inherit: boolean;
offset?: number;
order: 'asc' | 'desc';
Expand Down
1 change: 1 addition & 0 deletions assets/js/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './attributes-query';
export * from './attributes';
export * from './filters';
export * from './notices';
export * from './object-operations';
export * from './products';
export * from './shared-attributes';
export * from './sanitize-html';
8 changes: 8 additions & 0 deletions assets/js/utils/object-operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Returns an object without a key.
*/
export function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
const { [ key ]: omit, ...rest } = obj;

return rest;
}
32 changes: 27 additions & 5 deletions src/BlockTypes/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,21 @@ public function build_query( $query ) {
}

$common_query_values = array(
'post_type' => 'product',
'post__in' => array(),
'post_status' => 'publish',
'meta_query' => array(),
'posts_per_page' => $query['posts_per_page'],
'orderby' => $query['orderby'],
'order' => $query['order'],
'offset' => $query['offset'],
'meta_query' => array(),
'post__in' => array(),
'post_status' => 'publish',
'post_type' => 'product',
'tax_query' => array(),
);

return $this->merge_queries(
$handpicked_products = isset( $parsed_block['attrs']['query']['include'] ) ?
$parsed_block['attrs']['query']['include'] : $common_query_values['post__in'];

$merged_query = $this->merge_queries(
$common_query_values,
$this->get_global_query( $parsed_block ),
$this->get_custom_orderby_query( $query['orderby'] ),
Expand All @@ -185,6 +188,8 @@ public function build_query( $query ) {
$this->get_filter_by_taxonomies_query( $query ),
$this->get_filter_by_keyword_query( $query )
);

return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
}

/**
Expand Down Expand Up @@ -307,6 +312,23 @@ private function get_custom_orderby_query( $orderby ) {
);
}

/**
* Apply the query only to a subset of products
*
* @param array $query The query.
* @param array $ids Array of selected product ids.
*
* @return array
*/
private function filter_query_to_only_include_ids( $query, $ids ) {
if ( ! empty( $ids ) ) {
$query['post__in'] = empty( $query['post__in'] ) ?
$ids : array_intersect( $ids, $query['post__in'] );
}

return $query;
}

/**
* Return the `tax_query` for the requested attributes
*
Expand Down

0 comments on commit da19120

Please sign in to comment.