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

Implement Hand-Picked Products block #7925

Merged
merged 10 commits into from
May 8, 2023
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;
}

Comment on lines -15 to -23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thanks for moving this to utils 🙌🏻

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[] );
} );
}, [] );
Comment on lines +22 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed here but just wanted to share my thoughts. Maybe we can create a custom hook to fetch and manage the productsList state to separate concerns and make the component more modular:

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

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

  return productsList;
};

And use it in the component like this:

const productsList = useProductsList();


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