Commit 5e7d832356 for woocommerce
commit 5e7d832356371a7620543bbd299b7556c501c087
Author: Amit Raj <77401999+amitraj2203@users.noreply.github.com>
Date: Mon Jan 19 20:44:06 2026 +0530
Add `Products by Brand` Collection to Product Collection Block (#62817)
* Add BY_BRAND option to CoreCollectionNames enum for product collections
* Add new BY_BRAND collection block for displaying products by brand
* Add BY_BRAND collection to product collections
* Update CollectionSpecificControls to include BY_BRAND in visibility checks
* Refactor useTaxonomyControls to include BY_BRAND filtering logic
* Add handler for BY_BRAND product collection to return empty result if no brand is selected
* Add changelog file
* Add unit test for filtering products by brand in QueryBuilder
* Update const name
* Fix lint error
---------
Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/62368-add-products-by-brand-collection b/plugins/woocommerce/changelog/62368-add-products-by-brand-collection
new file mode 100644
index 0000000000..45fedc71ff
--- /dev/null
+++ b/plugins/woocommerce/changelog/62368-add-products-by-brand-collection
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add "Products by Brand" collection to Product Collection block.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx
new file mode 100644
index 0000000000..2acabdfd5d
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import type {
+ InnerBlockTemplate,
+ BlockVariationScope,
+} from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
+import { Icon, store } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import {
+ INNER_BLOCKS_PRODUCT_TEMPLATE,
+ INNER_BLOCKS_PAGINATION_TEMPLATE,
+} from '../constants';
+import { CoreCollectionNames, CoreFilterNames } from '../types';
+
+const collection = {
+ name: CoreCollectionNames.BY_BRAND,
+ title: __( 'Products by Brand', 'woocommerce' ),
+ icon: <Icon icon={ store } />,
+ description: __( 'Display products from specific brands.', 'woocommerce' ),
+ scope: [ 'inserter', 'block' ] as BlockVariationScope[],
+};
+
+const attributes = {
+ displayLayout: {
+ type: 'flex',
+ columns: 5,
+ shrinkColumns: true,
+ },
+ hideControls: [ CoreFilterNames.HAND_PICKED, CoreFilterNames.FILTERABLE ],
+};
+
+const heading: InnerBlockTemplate = [
+ 'core/heading',
+ {
+ textAlign: 'center',
+ level: 2,
+ content: __( 'Products by Brand', 'woocommerce' ),
+ style: { spacing: { margin: { bottom: '1rem' } } },
+ },
+];
+
+const innerBlocks: InnerBlockTemplate[] = [
+ heading,
+ INNER_BLOCKS_PRODUCT_TEMPLATE,
+ INNER_BLOCKS_PAGINATION_TEMPLATE,
+];
+
+export default {
+ ...collection,
+ attributes,
+ innerBlocks,
+};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/index.tsx
index 12532f81b1..d92ccaebdb 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/index.tsx
@@ -26,6 +26,7 @@ import topRated from './top-rated';
import upsells from './upsells';
import byCategory from './by-category';
import byTag from './by-tag';
+import byBrand from './by-brand';
import cartContents from './cart-contents';
// Order in here is reflected in the Collection Chooser in Editor.
@@ -39,6 +40,7 @@ const collections: BlockVariation[] = [
handPicked,
byCategory,
byTag,
+ byBrand,
related,
upsells,
crossSells,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
index 5fb53c88d1..34ab885334 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
@@ -287,9 +287,10 @@ const CollectionSpecificControls = (
query: props.attributes.query,
};
- const isByCategoryOrTag =
+ const isByTaxonomy =
collection === CoreCollectionNames.BY_CATEGORY ||
- collection === CoreCollectionNames.BY_TAG;
+ collection === CoreCollectionNames.BY_TAG ||
+ collection === CoreCollectionNames.BY_BRAND;
return (
<InspectorControls>
@@ -315,9 +316,9 @@ const CollectionSpecificControls = (
}
{
/**
- * "Category and Tag" collection-specific controls.
+ * "By Taxonomy" collection-specific controls.
*/
- isByCategoryOrTag && (
+ isByTaxonomy && (
<PanelBody>
<TaxonomyControls
{ ...queryControlProps }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/use-taxonomy-controls.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/use-taxonomy-controls.tsx
index de450d3efb..f5454f61ab 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/use-taxonomy-controls.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/taxonomy-controls/use-taxonomy-controls.tsx
@@ -51,6 +51,15 @@ function useTaxonomyControls( {
: taxonomy.slug === 'product_tag'
);
}
+ if ( collection === CoreCollectionNames.BY_BRAND ) {
+ return taxonomies.filter( ( taxonomy ) =>
+ // If it's in filter panel, we want to show everything BUT the brand control.
+ // Otherwise, it's a collection specific filter and we want to show ONLY the brand control.
+ isFiltersPanel
+ ? taxonomy.slug !== 'product_brand'
+ : taxonomy.slug === 'product_brand'
+ );
+ }
return isFiltersPanel ? taxonomies : [];
}, [ taxonomies, collection, isFiltersPanel ] );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
index 0fbdf87f90..655f5e116d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
@@ -190,6 +190,7 @@ export enum CoreCollectionNames {
CROSS_SELLS = 'woocommerce/product-collection/cross-sells',
BY_CATEGORY = 'woocommerce/product-collection/by-category',
BY_TAG = 'woocommerce/product-collection/by-tag',
+ BY_BRAND = 'woocommerce/product-collection/by-brand',
CART_CONTENTS = 'woocommerce/product-collection/cart-contents',
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
index 92dc339aec..d145e60c7f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/HandlerRegistry.php
@@ -86,6 +86,18 @@ class HandlerRegistry {
}
);
+ $this->register_collection_handlers(
+ 'woocommerce/product-collection/by-brand',
+ function ( $collection_args, $common_query_values, $query ) {
+ // For Products by Brand collection, if no brand is selected, we should return an empty result set.
+ if ( empty( $query['taxonomies_query'] ) ) {
+ return array(
+ 'post__in' => array( -1 ),
+ );
+ }
+ }
+ );
+
$this->register_collection_handlers(
'woocommerce/product-collection/related',
function ( $collection_args ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 326dd3b5bc..568470b118 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -919,4 +919,144 @@ class QueryBuilder extends \WP_UnitTestCase {
wp_delete_term( $featured_tag_id, 'product_tag' );
wp_delete_term( $sale_tag_id, 'product_tag' );
}
+
+ /**
+ * Tests that the by-brand collection handler works as expected.
+ */
+ public function test_collection_by_brand() {
+ // Create test brands.
+ $nike_brand = wp_create_term( 'Nike', 'product_brand' );
+ $nike_brand_id = $nike_brand['term_id'];
+
+ $adidas_brand = wp_create_term( 'Adidas', 'product_brand' );
+ $adidas_brand_id = $adidas_brand['term_id'];
+
+ // Create test products.
+ $nike_shoes = WC_Helper_Product::create_simple_product();
+ $nike_shoes->set_name( 'Nike Shoes' );
+ $nike_shoes->save();
+
+ $nike_shirt = WC_Helper_Product::create_simple_product();
+ $nike_shirt->set_name( 'Nike Shirt' );
+ $nike_shirt->save();
+
+ $adidas_shoes = WC_Helper_Product::create_simple_product();
+ $adidas_shoes->set_name( 'Adidas Shoes' );
+ $adidas_shoes->save();
+
+ $unbranded_product = WC_Helper_Product::create_simple_product();
+ $unbranded_product->set_name( 'Unbranded Product' );
+ $unbranded_product->save();
+
+ // Assign products to brands.
+ wp_set_object_terms( $nike_shoes->get_id(), $nike_brand_id, 'product_brand' );
+ wp_set_object_terms( $nike_shirt->get_id(), $nike_brand_id, 'product_brand' );
+ wp_set_object_terms( $adidas_shoes->get_id(), $adidas_brand_id, 'product_brand' );
+ // unbranded_product has no brand.
+
+ // Test filtering by Nike brand - Frontend.
+ $merged_query = Utils::initialize_merged_query(
+ $this->block_instance,
+ null,
+ array(
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'terms' => array( $nike_brand_id ),
+ 'include_children' => false,
+ ),
+ ),
+ )
+ );
+
+ $query = new WP_Query( $merged_query );
+ $found_product_ids = wp_list_pluck( $query->posts, 'ID' );
+
+ // Should return Nike shoes and Nike shirt.
+ $this->assertContains( $nike_shoes->get_id(), $found_product_ids );
+ $this->assertContains( $nike_shirt->get_id(), $found_product_ids );
+ $this->assertNotContains( $adidas_shoes->get_id(), $found_product_ids );
+ $this->assertNotContains( $unbranded_product->get_id(), $found_product_ids );
+
+ // Test filtering by Nike brand - Editor.
+ $args = array(
+ 'posts_per_page' => 10,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'terms' => array( $nike_brand_id ),
+ 'include_children' => false,
+ ),
+ ),
+ );
+ $request = Utils::build_request();
+
+ $updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
+ $editor_query = new WP_Query( $updated_query );
+ $editor_found_ids = wp_list_pluck( $editor_query->posts, 'ID' );
+
+ // Should return Nike shoes and Nike shirt in editor as well.
+ $this->assertContains( $nike_shoes->get_id(), $editor_found_ids );
+ $this->assertContains( $nike_shirt->get_id(), $editor_found_ids );
+ $this->assertNotContains( $adidas_shoes->get_id(), $editor_found_ids );
+ $this->assertNotContains( $unbranded_product->get_id(), $editor_found_ids );
+
+ // Test filtering by Adidas brand - Frontend.
+ $merged_query_adidas = Utils::initialize_merged_query(
+ $this->block_instance,
+ null,
+ array(
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'terms' => array( $adidas_brand_id ),
+ 'include_children' => false,
+ ),
+ ),
+ )
+ );
+
+ $query_adidas = new WP_Query( $merged_query_adidas );
+ $found_adidas_ids = wp_list_pluck( $query_adidas->posts, 'ID' );
+
+ // Should return only Adidas shoes.
+ $this->assertNotContains( $nike_shoes->get_id(), $found_adidas_ids );
+ $this->assertNotContains( $nike_shirt->get_id(), $found_adidas_ids );
+ $this->assertContains( $adidas_shoes->get_id(), $found_adidas_ids );
+ $this->assertNotContains( $unbranded_product->get_id(), $found_adidas_ids );
+
+ // Test filtering by Adidas brand - Editor.
+ $args_adidas = array(
+ 'posts_per_page' => 10,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+ 'tax_query' => array(
+ array(
+ 'taxonomy' => 'product_brand',
+ 'terms' => array( $adidas_brand_id ),
+ 'include_children' => false,
+ ),
+ ),
+ );
+ $request_adidas = Utils::build_request();
+
+ $updated_query_adidas = $this->block_instance->update_rest_query_in_editor( $args_adidas, $request_adidas );
+ $editor_query_adidas = new WP_Query( $updated_query_adidas );
+ $editor_adidas_ids = wp_list_pluck( $editor_query_adidas->posts, 'ID' );
+
+ // Should return only Adidas shoes in editor as well.
+ $this->assertNotContains( $nike_shoes->get_id(), $editor_adidas_ids );
+ $this->assertNotContains( $nike_shirt->get_id(), $editor_adidas_ids );
+ $this->assertContains( $adidas_shoes->get_id(), $editor_adidas_ids );
+ $this->assertNotContains( $unbranded_product->get_id(), $editor_adidas_ids );
+
+ $nike_shoes->delete();
+ $nike_shirt->delete();
+ $adidas_shoes->delete();
+ $unbranded_product->delete();
+ wp_delete_term( $nike_brand_id, 'product_brand' );
+ wp_delete_term( $adidas_brand_id, 'product_brand' );
+ }
}