diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js index 29997853322..5a00d60534e 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/index.js @@ -15,3 +15,4 @@ import './product-elements/stock-indicator'; import './product-elements/add-to-cart'; import './product-elements/product-image-gallery'; import './product-elements/product-details'; +import './product-elements/related-products'; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json new file mode 100644 index 00000000000..e6e3a2a9aaf --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/block.json @@ -0,0 +1,17 @@ +{ + "name": "woocommerce/related-products", + "version": "1.0.0", + "title": "Related Products", + "icon": "product", + "description": "Display related products.", + "category": "woocommerce", + "supports": { + "align": true, + "reusable": false + }, + "keywords": [ "WooCommerce" ], + "usesContext": [ "postId", "postType", "queryId" ], + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx new file mode 100644 index 00000000000..36778c17b1f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/edit.tsx @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { + BLOCK_ATTRIBUTES, + INNER_BLOCKS_TEMPLATE, +} from '@woocommerce/blocks/product-query/variations'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; +import { InnerBlockTemplate } from '@wordpress/blocks'; +import { Disabled, Notice } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './editor.scss'; + +const Edit = () => { + const TEMPLATE: InnerBlockTemplate[] = [ + [ 'core/query', BLOCK_ATTRIBUTES, INNER_BLOCKS_TEMPLATE ], + ]; + const blockProps = useBlockProps(); + + return ( +
+ + +

+ { __( + 'These products will vary depending on the main product in the page', + 'woo-gutenberg-products-block' + ) } +

+
+
+ +
+ ); +}; + +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss new file mode 100644 index 00000000000..b3872daea3a --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/editor.scss @@ -0,0 +1,4 @@ +.wc-block-editor-related-products__notice { + margin: 10px auto; + max-width: max-content; +} diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx new file mode 100644 index 00000000000..1db84adc949 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/index.tsx @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { box as icon } from '@wordpress/icons'; +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; +import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import save from './save'; +import metadata from './block.json'; + +registerBlockSingleProductTemplate( { + registerBlockFn: () => { + // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core. + registerBlockType( metadata, { + icon, + edit, + save, + } ); + }, + unregisterBlockFn: () => { + unregisterBlockType( metadata.name ); + }, + blockName: metadata.name, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx new file mode 100644 index 00000000000..0feb6d8f950 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/related-products/save.tsx @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; + +const Save = () => { + const blockProps = useBlockProps.save(); + + return ( +
+ { /* @ts-expect-error: `InnerBlocks.Content` is a component that is typed in WordPress core*/ } + +
+ ); +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx index 312ce2af40d..04425a80c2e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/index.tsx @@ -14,6 +14,7 @@ import { CORE_NAME as PRODUCT_TEMPLATE_ID } from './variations/elements/product- import './inspector-controls'; import './style.scss'; import './variations/product-query'; +import './variations/related-products'; const EXTENDED_CORE_ELEMENTS = [ PRODUCT_SUMMARY_ID, diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts index e8bc6128918..cbee9c48cc8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/types.ts @@ -112,4 +112,5 @@ export interface ProductQueryContext { export enum QueryVariation { /** The main, fully customizable, Product Query block */ PRODUCT_QUERY = 'woocommerce/product-query', + RELATED_PRODUCTS = 'woocommerce/related-products', } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts new file mode 100644 index 00000000000..9dcdfe4481e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/index.ts @@ -0,0 +1 @@ +export * from './related-products'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx new file mode 100644 index 00000000000..4286baeef79 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-query/variations/related-products.tsx @@ -0,0 +1,133 @@ +/** + * External dependencies + */ +import { + InnerBlockTemplate, + registerBlockVariation, + unregisterBlockVariation, +} from '@wordpress/blocks'; +import { Icon } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { stacks } from '@woocommerce/icons'; +import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils'; + +/** + * Internal dependencies + */ +import { QUERY_LOOP_ID } from '../constants'; + +import { VARIATION_NAME as PRODUCT_TEMPLATE_ID } from './elements/product-template'; +import { VARIATION_NAME as PRODUCT_TITLE_ID } from './elements/product-title'; + +const VARIATION_NAME = 'woocommerce/related-products'; + +export const BLOCK_ATTRIBUTES = { + namespace: VARIATION_NAME, + allowedControls: [], + displayLayout: { + type: 'flex', + columns: 5, + }, + query: { + perPage: 5, + pages: 0, + offset: 0, + postType: 'product', + order: 'asc', + orderBy: 'title', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: false, + }, + lock: { + remove: true, + move: true, + }, +}; + +export const INNER_BLOCKS_TEMPLATE: InnerBlockTemplate[] = [ + [ + 'core/post-template', + { __woocommerceNamespace: PRODUCT_TEMPLATE_ID }, + [ + [ + 'woocommerce/product-image', + { + productId: 0, + }, + ], + [ + 'core/post-title', + { + textAlign: 'center', + level: 3, + fontSize: 'medium', + __woocommerceNamespace: PRODUCT_TITLE_ID, + }, + [], + ], + [ + 'woocommerce/product-price', + { + textAlign: 'center', + fontSize: 'small', + style: { + spacing: { + margin: { bottom: '1rem' }, + }, + }, + }, + [], + ], + [ + 'woocommerce/product-button', + { + textAlign: 'center', + fontSize: 'small', + style: { + spacing: { + margin: { bottom: '1rem' }, + }, + }, + }, + [], + ], + ], + ], +]; + +registerBlockSingleProductTemplate( { + registerBlockFn: () => + registerBlockVariation( QUERY_LOOP_ID, { + description: __( + 'Display related products.', + 'woo-gutenberg-products-block' + ), + name: 'Related Products Controls', + title: __( + 'Related Products Controls', + 'woo-gutenberg-products-block' + ), + isActive: ( blockAttributes ) => + blockAttributes.namespace === VARIATION_NAME, + icon: ( + + ), + attributes: BLOCK_ATTRIBUTES, + // Gutenberg doesn't support this type yet, discussion here: + // https://github.com/WordPress/gutenberg/pull/43632 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + allowedControls: [], + innerBlocks: INNER_BLOCKS_TEMPLATE, + scope: [ 'block' ], + } ), + unregisterBlockFn: () => + unregisterBlockVariation( QUERY_LOOP_ID, 'Related Products' ), + blockName: VARIATION_NAME, +} ); diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php index 85805715cfd..2eae4a8cdf5 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductQuery.php @@ -81,7 +81,7 @@ class ProductQuery extends AbstractBlock { * @param array $parsed_block The block being rendered. * @return boolean */ - private function is_woocommerce_variation( $parsed_block ) { + public static function is_woocommerce_variation( $parsed_block ) { return isset( $parsed_block['attrs']['namespace'] ) && substr( $parsed_block['attrs']['namespace'], 0, 11 ) === 'woocommerce'; } @@ -99,7 +99,7 @@ class ProductQuery extends AbstractBlock { $this->parsed_block = $parsed_block; - if ( $this->is_woocommerce_variation( $parsed_block ) ) { + if ( self::is_woocommerce_variation( $parsed_block ) ) { // Set this so that our product filters can detect if it's a PHP template. $this->asset_data_registry->add( 'has_filterable_products', true, true ); $this->asset_data_registry->add( 'is_rendering_php_template', true, true ); diff --git a/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php b/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php new file mode 100644 index 00000000000..e77416edf92 --- /dev/null +++ b/plugins/woocommerce-blocks/src/BlockTypes/RelatedProducts.php @@ -0,0 +1,89 @@ +parsed_block = $parsed_block; + + if ( ProductQuery::is_woocommerce_variation( $parsed_block ) && 'woocommerce/related-products' === $parsed_block['attrs']['namespace'] ) { + // Set this so that our product filters can detect if it's a PHP template. + add_filter( + 'query_loop_block_query_vars', + array( $this, 'build_query' ), + 10, + 1 + ); + } + } + + + + /** + * Return a custom query based on attributes, filters and global WP_Query. + * + * @param WP_Query $query The WordPress Query. + * @return array + */ + public function build_query( $query ) { + $parsed_block = $this->parsed_block; + if ( ! ProductQuery::is_woocommerce_variation( $parsed_block ) && 'woocommerce/related-products' !== $parsed_block['attrs']['namespace'] ) { + return $query; + } + $product = wc_get_product(); + $related_products = wc_get_related_products( $product->get_id() ); + + return array( + 'post_type' => 'product', + 'post__in' => $related_products, + 'post_status' => 'publish', + ); + } + + +} diff --git a/plugins/woocommerce-blocks/src/BlockTypesController.php b/plugins/woocommerce-blocks/src/BlockTypesController.php index 2b46c98831e..5ad32e5558a 100644 --- a/plugins/woocommerce-blocks/src/BlockTypesController.php +++ b/plugins/woocommerce-blocks/src/BlockTypesController.php @@ -212,6 +212,7 @@ final class BlockTypesController { 'RatingFilter', 'ReviewsByCategory', 'ReviewsByProduct', + 'RelatedProducts', 'ProductDetails', 'StockFilter', ];