From 16ea028b0378f30d1fe9113503071ae44cc97590 Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Mon, 12 Sep 2022 12:02:04 +0200
Subject: [PATCH 01/11] Product Query: Fix pagination issue

---
 src/BlockTypes/ProductQuery.php | 43 +++++++++------------------------
 1 file changed, 12 insertions(+), 31 deletions(-)

diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index 2b39c231988..8fc72d91657 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -37,27 +37,6 @@ protected function initialize() {
 
 	}
 
-	/**
-	 * Remove the query block filter and parse the custom query
-	 *
-	 * This function is supposed to be called by the `query_loop_block_query_vars`
-	 * filter. It de-registers the filter to make sure it runs only once and doesn't end
-	 * up hi-jacking future Query Loop blocks.
-	 *
-	 * It needs unfortunately to be `public` or otherwise the filter can't call it.
-	 *
-	 * @param WP_Query $query The WordPress Query.
-	 * @return array
-	 */
-	public function get_query_by_attributes_once( $query ) {
-		remove_filter(
-			'query_loop_block_query_vars',
-			array( $this, 'get_query_by_attributes_once' ),
-			10
-		);
-
-		return $this->get_query_by_attributes( $query, $this->parsed_block );
-	}
 
 	/**
 	 * Update the query for the product query block.
@@ -66,28 +45,30 @@ public function get_query_by_attributes_once( $query ) {
 	 * @param array       $parsed_block The block being rendered.
 	 */
 	public function update_query( $pre_render, $parsed_block ) {
-		if ( 'core/query' !== $parsed_block['blockName'] || ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
+		if ( 'core/query' !== $parsed_block['blockName'] ) {
 			return;
 		}
 
 		$this->parsed_block = $parsed_block;
 
-		add_filter(
-			'query_loop_block_query_vars',
-			array( $this, 'get_query_by_attributes_once' ),
-			10,
-			1
-		);
+		if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
+			add_filter(
+				'query_loop_block_query_vars',
+				array( $this, 'get_query_by_attributes' ),
+				10,
+				1
+			);
+		}
 	}
 
 	/**
 	 * Return a custom query based on the attributes.
 	 *
-	 * @param WP_Query $query         The WordPress Query.
-	 * @param WP_Block $parsed_block  The block being rendered.
+	 * @param WP_Query $query The WordPress Query.
 	 * @return array
 	 */
-	public function get_query_by_attributes( $query, $parsed_block ) {
+	public function get_query_by_attributes( $query ) {
+		$parsed_block = $this->parsed_block;
 		if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
 			return $query;
 		}

From 6e93dd02c77f91dfe99f6459cc66825b7782550f Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Thu, 15 Sep 2022 14:10:47 +0200
Subject: [PATCH 02/11] Product Query - Add support for the Filter By Price
 Block #6790

Product Query - Add support for the Filter By Price Block
---
 src/BlockTypes/PriceFilter.php  |   2 +
 src/BlockTypes/ProductQuery.php | 109 ++++++++++++++++++++++++++++----
 2 files changed, 99 insertions(+), 12 deletions(-)

diff --git a/src/BlockTypes/PriceFilter.php b/src/BlockTypes/PriceFilter.php
index 0c9ab501c8e..6274d326375 100644
--- a/src/BlockTypes/PriceFilter.php
+++ b/src/BlockTypes/PriceFilter.php
@@ -12,4 +12,6 @@ class PriceFilter extends AbstractBlock {
 	 * @var string
 	 */
 	protected $block_name = 'price-filter';
+	const MIN_PRICE_QUERY_VAR = 'min_price';
+	const MAX_PRICE_QUERY_VAR = 'max_price';
 }
diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index 8fc72d91657..b03b4ec347a 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -27,6 +27,10 @@ class ProductQuery extends AbstractBlock {
 	 * - Hook into pre_render_block to update the query.
 	 */
 	protected function initialize() {
+		add_filter( 'query_vars', array( $this, 'set_query_vars' ) );
+		// 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 );
 		parent::initialize();
 		add_filter(
 			'pre_render_block',
@@ -37,7 +41,6 @@ protected function initialize() {
 
 	}
 
-
 	/**
 	 * Update the query for the product query block.
 	 *
@@ -54,7 +57,7 @@ public function update_query( $pre_render, $parsed_block ) {
 		if ( isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
 			add_filter(
 				'query_loop_block_query_vars',
-				array( $this, 'get_query_by_attributes' ),
+				array( $this, 'build_query' ),
 				10,
 				1
 			);
@@ -62,12 +65,12 @@ public function update_query( $pre_render, $parsed_block ) {
 	}
 
 	/**
-	 * Return a custom query based on the attributes.
+	 * Return a custom query based on attributes, filters and global WP_Query.
 	 *
 	 * @param WP_Query $query The WordPress Query.
 	 * @return array
 	 */
-	public function get_query_by_attributes( $query ) {
+	public function build_query( $query ) {
 		$parsed_block = $this->parsed_block;
 		if ( ! isset( $parsed_block['attrs']['__woocommerceVariationProps'] ) ) {
 			return $query;
@@ -79,24 +82,39 @@ public function get_query_by_attributes( $query ) {
 			'post_status'    => 'publish',
 			'posts_per_page' => $query['posts_per_page'],
 			'orderby'        => $query['orderby'],
+			'orderby'        => $query['orderby'],
 			'order'          => $query['order'],
+			// Ignoring the warning of not using meta queries.
+			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+			'meta_query'     => array(),
+		);
+
+		$queries_attributes = $this->get_queries_by_attributes( $variation_props );
+		$queries_filters    = $this->get_queries_by_applied_filters();
+
+		return array_reduce(
+			array_merge(
+				$queries_attributes,
+				$queries_filters
+			),
+			function( $acc, $query ) {
+						// Ignoring the warning of not using meta queries.
+						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+						$acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query'];
+						return $acc;
+			},
+			$common_query_values
 		);
-		$on_sale_query       = $this->get_on_sale_products_query( $variation_props );
 
-		return array_merge( $query, $common_query_values, $on_sale_query );
 	}
 
+
 	/**
 	 * Return a query for on sale products.
 	 *
-	 * @param array $variation_props Dedicated attributes for the variation.
 	 * @return array
 	 */
-	private function get_on_sale_products_query( $variation_props ) {
-		if ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) {
-			return array();
-		}
-
+	private function get_on_sale_products_query() {
 		return array(
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -117,4 +135,71 @@ private function get_on_sale_products_query( $variation_props ) {
 			),
 		);
 	}
+
+	/**
+	 * Set the query vars that are used by filter blocks.
+	 *
+	 * @param array $qvars Public query vars.
+	 * @return array
+	 */
+	public function set_query_vars( $qvars ) {
+		$filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR );
+		return array_merge( $qvars, $filter_query_args );
+	}
+
+	/**
+	 * Return queries that are generated by query args
+	 *
+	 * @return array
+	 */
+	private function get_queries_by_applied_filters() {
+		return array( 'price_filter' => $this->get_filter_by_price_query() );
+	}
+
+	/**
+	 * Return queries that are generated by attributes
+	 *
+	 * @param array $variation_props Dedicated attributes for the variation.
+	 * @return array
+	 */
+	private function get_queries_by_attributes( $variation_props ) {
+		return array(
+			'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(),
+		);
+	}
+
+	/**
+	 * Return a query that filters products by price.
+	 *
+	 * @return array
+	 */
+	private function get_filter_by_price_query() {
+		$min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR );
+		$max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR );
+
+		$max_price_query = empty( $max_price ) ? array() : [
+			'key'     => '_price',
+			'value'   => $max_price,
+			'compare' => '<=',
+			'type'    => 'numeric',
+		];
+
+		$min_price_query = empty( $min_price ) ? array() : [
+			'key'     => '_price',
+			'value'   => $min_price,
+			'compare' => '>=',
+			'type'    => 'numeric',
+		];
+
+		return array(
+			// Ignoring the warning of not using meta queries.
+			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+			'meta_query' => array(
+				'relation' => 'OR',
+				$max_price_query,
+				$min_price_query,
+			),
+		);
+	}
+
 }

From b243528f2dc703917063a352e1bfe4cb8c8a2545 Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Fri, 16 Sep 2022 17:49:18 +0200
Subject: [PATCH 03/11] fix query relation

---
 src/BlockTypes/ProductQuery.php | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index b03b4ec347a..ac89c03a6da 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -82,7 +82,6 @@ public function build_query( $query ) {
 			'post_status'    => 'publish',
 			'posts_per_page' => $query['posts_per_page'],
 			'orderby'        => $query['orderby'],
-			'orderby'        => $query['orderby'],
 			'order'          => $query['order'],
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -163,8 +162,9 @@ private function get_queries_by_applied_filters() {
 	 * @return array
 	 */
 	private function get_queries_by_attributes( $variation_props ) {
+		$on_sale_enabled = isset( $variation_props['attributes']['query']['onSale'] ) && true === $variation_props['attributes']['query']['onSale'];
 		return array(
-			'on_sale' => ( ! isset( $variation_props['attributes']['query']['onSale'] ) || true !== $variation_props['attributes']['query']['onSale'] ) ? array() : $this->get_on_sale_products_query(),
+			'on_sale' => ( $on_sale_enabled ? $this->get_on_sale_products_query() : array() ),
 		);
 	}
 
@@ -195,7 +195,7 @@ private function get_filter_by_price_query() {
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 			'meta_query' => array(
-				'relation' => 'OR',
+				'relation' => 'AND',
 				$max_price_query,
 				$min_price_query,
 			),

From e55a41dcf2f3bd366edb7837b1d8d8eca0c39c1b Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Fri, 16 Sep 2022 18:52:33 +0200
Subject: [PATCH 04/11] fix on sale query

---
 src/BlockTypes/ProductQuery.php | 18 +-----------------
 1 file changed, 1 insertion(+), 17 deletions(-)

diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index ac89c03a6da..cdf4e15b78c 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -115,23 +115,7 @@ function( $acc, $query ) {
 	 */
 	private function get_on_sale_products_query() {
 		return array(
-			// Ignoring the warning of not using meta queries.
-			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
-			'meta_query' => array(
-				'relation' => 'OR',
-				array(
-					'key'     => '_sale_price',
-					'value'   => 0,
-					'compare' => '>',
-					'type'    => 'numeric',
-				),
-				array(
-					'key'     => '_min_variation_sale_price',
-					'value'   => 0,
-					'compare' => '>',
-					'type'    => 'numeric',
-				),
-			),
+			'post_in' => wc_get_product_ids_on_sale(),
 		);
 	}
 

From 6d0de4dfeed961c077f77db25333c9c9342ca41d Mon Sep 17 00:00:00 2001
From: Luis Herranz <luisherranz@gmail.com>
Date: Tue, 20 Sep 2022 12:18:06 +0200
Subject: [PATCH 05/11] Initial client-side navigation

---
 assets/js/blocks/product-query/directives.js |  39 +++++++
 assets/js/blocks/product-query/frontend.js   |  70 +++++++++++++
 assets/js/blocks/product-query/router.js     | 104 +++++++++++++++++++
 assets/js/blocks/product-query/style.scss    |  21 ++++
 assets/js/blocks/product-query/vdom.js       |  43 ++++++++
 package-lock.json                            |  15 +++
 package.json                                 |   1 +
 src/BlockTypes/ProductQuery.php              |  16 +++
 8 files changed, 309 insertions(+)
 create mode 100644 assets/js/blocks/product-query/directives.js
 create mode 100644 assets/js/blocks/product-query/frontend.js
 create mode 100644 assets/js/blocks/product-query/router.js
 create mode 100644 assets/js/blocks/product-query/style.scss
 create mode 100644 assets/js/blocks/product-query/vdom.js

diff --git a/assets/js/blocks/product-query/directives.js b/assets/js/blocks/product-query/directives.js
new file mode 100644
index 00000000000..72a6fcb64c9
--- /dev/null
+++ b/assets/js/blocks/product-query/directives.js
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { h, options } from 'preact';
+
+// WordPress Directives.
+const directives = {};
+
+// Expose function to add directives.
+export const directive = ( name, cb ) => {
+	directives[ name ] = cb;
+};
+
+const WpDirective = ( props ) => {
+	for ( const d in props.wp ) {
+		directives[ d ]?.( props );
+	}
+	props._wrapped = true;
+	const { wp, tag, children, ...rest } = props;
+	return h( tag, rest, children );
+};
+
+const old = options.vnode;
+
+options.vnode = ( vnode ) => {
+	const wp = vnode.props.class;
+	const wrapped = vnode.props._wrapped;
+
+	if ( wp ) {
+		if ( ! wrapped ) {
+			vnode.props.tag = vnode.type;
+			vnode.type = WpDirective;
+		}
+	} else if ( wrapped ) {
+		delete vnode.props._wrapped;
+	}
+
+	if ( old ) old( vnode );
+};
diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js
new file mode 100644
index 00000000000..5716a62232c
--- /dev/null
+++ b/assets/js/blocks/product-query/frontend.js
@@ -0,0 +1,70 @@
+/**
+ * External dependencies
+ */
+import { options } from 'preact';
+import { useEffect } from 'preact/hooks';
+
+/**
+ * Internal dependencies
+ */
+import { prefetch, navigate } from './router';
+import { directive } from './directives';
+
+// The `wp-client-navigation` directive.
+directive( 'clientNavigation', ( props ) => {
+	const {
+		wp: { clientNavigation },
+		href,
+	} = props;
+
+	const url = href.startsWith( '/' ) ? href : window.location.pathname + href;
+
+	useEffect( () => {
+		// Prefetch the page if it is in the directive options.
+		if ( clientNavigation?.prefetch ) {
+			prefetch( url );
+		}
+	} );
+
+	// Don't do anything if it's falsy.
+	if ( clientNavigation !== false ) {
+		props.onclick = async ( event ) => {
+			event.preventDefault();
+
+			// Fetch the page (or return it from cache).
+			await navigate( url );
+
+			// Update the scroll, depending on the option. True by default.
+			if ( clientNavigation?.scroll === 'smooth' ) {
+				window.scrollTo( { top: 0, left: 0, behavior: 'smooth' } );
+			} else if ( clientNavigation?.scroll !== false ) {
+				window.scrollTo( 0, 0 );
+			}
+		};
+	}
+} );
+
+// Manually add the `wp-client-navigation` directive to the virtual nodes.
+// TODO: Move this to the HTML.
+const clientNavigationClassNames = [
+	'wp-block-query-pagination-next',
+	'wp-block-query-pagination-previous',
+	'page-numbers',
+];
+const old = options.vnode;
+options.vnode = ( vnode ) => {
+	if ( vnode.type === 'a' ) {
+		clientNavigationClassNames.forEach( ( className ) => {
+			if ( vnode.props.class?.includes( className ) ) {
+				vnode.props.wp = {
+					clientNavigation: {
+						prefetch:
+							className === 'page-numbers' ? false : 'eager',
+						scroll: false,
+					},
+				};
+			}
+		} );
+	}
+	if ( old ) old( vnode );
+};
diff --git a/assets/js/blocks/product-query/router.js b/assets/js/blocks/product-query/router.js
new file mode 100644
index 00000000000..b70b8fb894b
--- /dev/null
+++ b/assets/js/blocks/product-query/router.js
@@ -0,0 +1,104 @@
+/**
+ * External dependencies
+ */
+import { hydrate, render } from 'preact';
+
+/**
+ * Internal dependencies
+ */
+import { toVdom } from './vdom';
+
+// The root to render the vdom (document.body).
+let rootFragment;
+
+// The cache of visited and prefetched pages.
+const pages = new Map();
+
+// For wrapperless hydration of document.body.
+// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
+const createRootFragment = ( parent, replaceNode ) => {
+	replaceNode = [].concat( replaceNode );
+	const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
+	function insert( c, r ) {
+		parent.insertBefore( c, r || s );
+	}
+	return ( parent.__k = {
+		nodeType: 1,
+		parentNode: parent,
+		firstChild: replaceNode[ 0 ],
+		childNodes: replaceNode,
+		insertBefore: insert,
+		appendChild: insert,
+		removeChild( c ) {
+			parent.removeChild( c );
+		},
+	} );
+};
+
+// Helper function to await until the CPU is idle.
+const idle = () =>
+	new Promise( ( resolve ) => window.requestIdleCallback( resolve ) );
+
+// Helper to remove domain and hash from the URL. We are only interesting in
+// caching the path and the query.
+const cleanUrl = ( url ) => {
+	const u = new URL( url, 'http://a.bc' );
+	return u.pathname + u.search;
+};
+
+// Fetch a new page and convert it to a static virtual DOM.
+const fetchPage = async ( url ) => {
+	const html = await window.fetch( url ).then( ( res ) => res.text() );
+	await idle(); // Wait until CPU is idle to do the parsing and vdom.
+	const dom = new window.DOMParser().parseFromString( html, 'text/html' );
+	return toVdom( dom.body );
+};
+
+// Prefetch a page. We store the promise to avoid triggering a second fetch for
+// a page if a fetching has already started.
+export const prefetch = ( url ) => {
+	url = cleanUrl( url );
+	if ( ! pages.has( url ) ) {
+		pages.set( url, fetchPage( url ) );
+	}
+};
+
+window.prefetch = prefetch;
+
+// Navigate to a new page.
+export const navigate = async ( href ) => {
+	const url = cleanUrl( href );
+	prefetch( url );
+	const vdom = await pages.get( url );
+	render( vdom, rootFragment );
+	window.history.pushState( {}, '', href );
+};
+
+window.navigate = navigate;
+
+// Listen to the back and forward buttons and restore the page if it's in
+// the cache.
+window.addEventListener( 'popstate', async () => {
+	const previousUrl = cleanUrl( window.location ); // Remove hash.
+	if ( pages.has( previousUrl ) ) {
+		const vdom = await pages.get( previousUrl );
+		render( vdom, rootFragment );
+	} else {
+		window.location.reload();
+	}
+} );
+
+// Initialize the router with the initial DOM.
+document.addEventListener( 'DOMContentLoaded', async () => {
+	// Create the root fragment to hydrate everything.
+	rootFragment = createRootFragment(
+		document.documentElement,
+		document.body
+	);
+	const url = cleanUrl( window.location ); // Remove hash.
+
+	await idle(); // Wait until the CPU is idle to do the hydration.
+	const vdom = toVdom( document.body );
+	pages.set( url, Promise.resolve( vdom ) );
+	hydrate( vdom, rootFragment );
+} );
diff --git a/assets/js/blocks/product-query/style.scss b/assets/js/blocks/product-query/style.scss
new file mode 100644
index 00000000000..d8757619ada
--- /dev/null
+++ b/assets/js/blocks/product-query/style.scss
@@ -0,0 +1,21 @@
+p.animation {
+	width: 20px;
+	height: 20px;
+	background: #f00;
+	position: relative;
+	animation: animate 3s infinite;
+	animation-direction: alternate;
+}
+
+@keyframes animate {
+	0% {
+		background: #f00;
+		left: 0;
+		top: 0;
+	}
+	100% {
+		background: #ff0;
+		left: 300px;
+		top: 0;
+	}
+}
\ No newline at end of file
diff --git a/assets/js/blocks/product-query/vdom.js b/assets/js/blocks/product-query/vdom.js
new file mode 100644
index 00000000000..452bc34c33a
--- /dev/null
+++ b/assets/js/blocks/product-query/vdom.js
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { h } from 'preact';
+
+// Convert DOM nodes to static virtual DOM nodes.
+export const toVdom = ( node ) => {
+	if ( node.nodeType === 3 ) return node.data;
+	if ( node.nodeType === 8 ) return null;
+	if ( node.localName === 'script' ) return h( 'script', null );
+
+	const props = {},
+		a = node.attributes;
+
+	for ( let i = 0; i < a.length; i++ ) {
+		if ( a[ i ].name.startsWith( 'wp-' ) ) {
+			props.wp = props.wp || {};
+			let value = a[ i ].value;
+			try {
+				value = JSON.parse( value );
+			} catch ( e ) {}
+			props.wp[ renameDirective( a[ i ].name ) ] = value;
+		} else {
+			props[ a[ i ].name ] = a[ i ].value;
+		}
+	}
+
+	return h(
+		node.localName,
+		props,
+		[].map.call( node.childNodes, toVdom ).filter( exists )
+	);
+};
+
+// Rename WordPress Directives from `wp-some-directive` to `someDirective`.
+const renameDirective = ( s ) =>
+	s
+		.toLowerCase()
+		.replace( /^wp-/, '' )
+		.replace( /-(.)/g, ( _, chr ) => chr.toUpperCase() );
+
+// Filter the truthy.
+const exists = ( i ) => i;
diff --git a/package-lock.json b/package-lock.json
index 97c6a4cc201..36ee42de712 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
 				"dinero.js": "1.9.1",
 				"downshift": "6.1.7",
 				"html-react-parser": "0.14.3",
+				"preact": "^10.11.0",
 				"react-number-format": "4.9.3",
 				"reakit": "1.3.11",
 				"snakecase-keys": "5.4.2",
@@ -44169,6 +44170,15 @@
 			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
 			"dev": true
 		},
+		"node_modules/preact": {
+			"version": "10.11.0",
+			"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.0.tgz",
+			"integrity": "sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w==",
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/preact"
+			}
+		},
 		"node_modules/prelude-ls": {
 			"version": "1.2.1",
 			"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -88343,6 +88353,11 @@
 			"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
 			"dev": true
 		},
+		"preact": {
+			"version": "10.11.0",
+			"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.0.tgz",
+			"integrity": "sha512-Fk6+vB2kb6mSJfDgODq0YDhMfl0HNtK5+Uc9QqECO4nlyPAQwCI+BKyWO//idA7ikV7o+0Fm6LQmNuQi1wXI1w=="
+		},
 		"prelude-ls": {
 			"version": "1.2.1",
 			"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
diff --git a/package.json b/package.json
index c3174880ba6..59ec30dfc35 100644
--- a/package.json
+++ b/package.json
@@ -232,6 +232,7 @@
 		"dinero.js": "1.9.1",
 		"downshift": "6.1.7",
 		"html-react-parser": "0.14.3",
+		"preact": "^10.11.0",
 		"react-number-format": "4.9.3",
 		"reakit": "1.3.11",
 		"snakecase-keys": "5.4.2",
diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index 8fc72d91657..b24edd6de99 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -35,6 +35,22 @@ protected function initialize() {
 			2
 		);
 
+		add_filter(
+			'render_block_core/query',
+			array( $this, 'enqueue_script' ),
+			10,
+			3
+		);
+
+	}
+
+	public function enqueue_script( $block_content, $block, $instance ) {
+		if (
+			isset($block['attrs']['__woocommerceVariationProps']['name']) && 
+			$block['attrs']['__woocommerceVariationProps']['name'] === 'product-query') {
+			$this->enqueue_scripts();
+		} 
+		return $block_content;
 	}
 
 

From 1508400f13ec7b2839ed05d75bf4af2611cd35dd Mon Sep 17 00:00:00 2001
From: Luis Herranz <luisherranz@gmail.com>
Date: Tue, 20 Sep 2022 13:49:13 +0200
Subject: [PATCH 06/11] Add query-id class

---
 src/BlockTypes/ProductQuery.php | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index b24edd6de99..bfb4d8f7c08 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -48,8 +48,19 @@ public function enqueue_script( $block_content, $block, $instance ) {
 		if (
 			isset($block['attrs']['__woocommerceVariationProps']['name']) && 
 			$block['attrs']['__woocommerceVariationProps']['name'] === 'product-query') {
+
+			// Enqueue the full vdom scripts.
 			$this->enqueue_scripts();
+
+			// Add "query-id-${query_id}" class.
+			// TODO: Replace with WP_HTML_Walker when available.
+			$block_content = preg_replace(
+				'/([ "])wp-block-query([ "])/', 
+				'$1wp-block-query query-id-' . $block['attrs']['queryId'] . '$2', 
+				$block_content
+			);
 		} 
+
 		return $block_content;
 	}
 

From 49ac769b0a6819696c0d65b0080cba9fb3c47823 Mon Sep 17 00:00:00 2001
From: Luis Herranz <luisherranz@gmail.com>
Date: Tue, 20 Sep 2022 14:14:04 +0200
Subject: [PATCH 07/11] Move full-vdom files to its own folder

---
 assets/js/blocks/product-query/frontend.js                   | 5 ++---
 assets/js/blocks/product-query/{ => full-vdom}/directives.js | 0
 assets/js/blocks/product-query/full-vdom/index.js            | 2 ++
 assets/js/blocks/product-query/{ => full-vdom}/router.js     | 0
 assets/js/blocks/product-query/{ => full-vdom}/vdom.js       | 0
 5 files changed, 4 insertions(+), 3 deletions(-)
 rename assets/js/blocks/product-query/{ => full-vdom}/directives.js (100%)
 create mode 100644 assets/js/blocks/product-query/full-vdom/index.js
 rename assets/js/blocks/product-query/{ => full-vdom}/router.js (100%)
 rename assets/js/blocks/product-query/{ => full-vdom}/vdom.js (100%)

diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js
index 5716a62232c..f1e4665adc8 100644
--- a/assets/js/blocks/product-query/frontend.js
+++ b/assets/js/blocks/product-query/frontend.js
@@ -7,8 +7,7 @@ import { useEffect } from 'preact/hooks';
 /**
  * Internal dependencies
  */
-import { prefetch, navigate } from './router';
-import { directive } from './directives';
+import { prefetch, navigate, directive } from './full-vdom';
 
 // The `wp-client-navigation` directive.
 directive( 'clientNavigation', ( props ) => {
@@ -45,7 +44,7 @@ directive( 'clientNavigation', ( props ) => {
 } );
 
 // Manually add the `wp-client-navigation` directive to the virtual nodes.
-// TODO: Move this to the HTML.
+// TODO: Move this to the HTML once WP_HTML_Walker is available.
 const clientNavigationClassNames = [
 	'wp-block-query-pagination-next',
 	'wp-block-query-pagination-previous',
diff --git a/assets/js/blocks/product-query/directives.js b/assets/js/blocks/product-query/full-vdom/directives.js
similarity index 100%
rename from assets/js/blocks/product-query/directives.js
rename to assets/js/blocks/product-query/full-vdom/directives.js
diff --git a/assets/js/blocks/product-query/full-vdom/index.js b/assets/js/blocks/product-query/full-vdom/index.js
new file mode 100644
index 00000000000..7ca826040be
--- /dev/null
+++ b/assets/js/blocks/product-query/full-vdom/index.js
@@ -0,0 +1,2 @@
+export { navigate, prefetch } from './router';
+export { directive } from './directives';
diff --git a/assets/js/blocks/product-query/router.js b/assets/js/blocks/product-query/full-vdom/router.js
similarity index 100%
rename from assets/js/blocks/product-query/router.js
rename to assets/js/blocks/product-query/full-vdom/router.js
diff --git a/assets/js/blocks/product-query/vdom.js b/assets/js/blocks/product-query/full-vdom/vdom.js
similarity index 100%
rename from assets/js/blocks/product-query/vdom.js
rename to assets/js/blocks/product-query/full-vdom/vdom.js

From 4fc131456fa19b475dc9eb5d9b49b913e45f94da Mon Sep 17 00:00:00 2001
From: Luis Herranz <luisherranz@gmail.com>
Date: Tue, 20 Sep 2022 16:33:16 +0200
Subject: [PATCH 08/11] Hydrate only the Product Query blocks

---
 assets/js/blocks/product-query/frontend.js    |  16 +--
 .../product-query/full-vdom/router-helpers.js |  94 +++++++++++++++
 .../blocks/product-query/full-vdom/router.js  | 108 ++++++------------
 src/BlockTypes/ProductQuery.php               |   6 +-
 4 files changed, 137 insertions(+), 87 deletions(-)
 create mode 100644 assets/js/blocks/product-query/full-vdom/router-helpers.js

diff --git a/assets/js/blocks/product-query/frontend.js b/assets/js/blocks/product-query/frontend.js
index f1e4665adc8..5bbf2a9c4e3 100644
--- a/assets/js/blocks/product-query/frontend.js
+++ b/assets/js/blocks/product-query/frontend.js
@@ -15,7 +15,6 @@ directive( 'clientNavigation', ( props ) => {
 		wp: { clientNavigation },
 		href,
 	} = props;
-
 	const url = href.startsWith( '/' ) ? href : window.location.pathname + href;
 
 	useEffect( () => {
@@ -23,22 +22,15 @@ directive( 'clientNavigation', ( props ) => {
 		if ( clientNavigation?.prefetch ) {
 			prefetch( url );
 		}
-	} );
+	}, [ url ] );
 
 	// Don't do anything if it's falsy.
 	if ( clientNavigation !== false ) {
 		props.onclick = async ( event ) => {
+			// Stop server-side navigation.
 			event.preventDefault();
-
-			// Fetch the page (or return it from cache).
-			await navigate( url );
-
-			// Update the scroll, depending on the option. True by default.
-			if ( clientNavigation?.scroll === 'smooth' ) {
-				window.scrollTo( { top: 0, left: 0, behavior: 'smooth' } );
-			} else if ( clientNavigation?.scroll !== false ) {
-				window.scrollTo( 0, 0 );
-			}
+			// Start client-side navigation.
+			await navigate( url, { scroll: clientNavigation?.scroll } );
 		};
 	}
 } );
diff --git a/assets/js/blocks/product-query/full-vdom/router-helpers.js b/assets/js/blocks/product-query/full-vdom/router-helpers.js
new file mode 100644
index 00000000000..34ce563e841
--- /dev/null
+++ b/assets/js/blocks/product-query/full-vdom/router-helpers.js
@@ -0,0 +1,94 @@
+/**
+ * External dependencies
+ */
+import { hydrate, render } from 'preact';
+
+/**
+ * Internal dependencies
+ */
+import { toVdom } from './vdom';
+
+// Remove domain and hash from the URL. We are only interesting in the path and
+// the query.
+export const cleanUrl = ( url ) => {
+	const u = new URL( url, 'http://a.bc' );
+	return u.pathname + u.search;
+};
+
+// Helper to await until the CPU is idle.
+export const idle = () =>
+	new Promise( ( resolve ) => window.requestIdleCallback( resolve ) );
+
+// Get the id class from a Product Query element.
+const getQueryId = ( query ) =>
+	Array.from( query.classList.values() ).find( ( className ) =>
+		className.startsWith( 'query-id-' )
+	);
+
+// Root fragments where we will render the Product Query blocks.
+const rootFragments = new Map();
+
+// Create root fragments for each Product Query block.
+export const createRootFragments = () => {
+	document.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => {
+		rootFragments.set(
+			getQueryId( query ),
+			createRootFragment( query.parentElement, query )
+		);
+	} );
+};
+
+// Fetch a URL and return the HTML string.
+const fetchUrl = async ( url ) => {
+	return await window.fetch( url ).then( ( res ) => res.text() );
+};
+
+// Parse a DOM from an HTML string.
+const parseDom = ( html ) => {
+	return new window.DOMParser().parseFromString( html, 'text/html' );
+};
+
+// Fetch a page and return the virtual DOM of each Product Query block.
+export const fetchPage = async ( url ) => {
+	const html = await fetchUrl( url );
+	const dom = parseDom( html );
+	return toVdoms( dom );
+};
+
+// Build a virtual DOM for each Product Query block found in a document.
+export const toVdoms = ( dom ) => {
+	const vdoms = new Map();
+	dom.querySelectorAll( '.woo-product-query' ).forEach( ( query ) => {
+		vdoms.set( getQueryId( query ), toVdom( query ) );
+	} );
+	return vdoms;
+};
+
+// Render the virtual DOM of each Product Query block.
+export const renderVdoms = ( vdoms, initial = false ) => {
+	const r = initial ? hydrate : render;
+	vdoms.forEach( ( vdom, id ) => {
+		r( vdom, rootFragments.get( id ) );
+	} );
+};
+
+// We use this for wrapperless hydration.
+// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
+const createRootFragment = ( parent, replaceNode ) => {
+	replaceNode = [].concat( replaceNode );
+	const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
+	function insert( c, r ) {
+		parent.insertBefore( c, r || s );
+	}
+	return ( parent.__k = {
+		nodeType: 1,
+		parentNode: parent,
+		firstChild: replaceNode[ 0 ],
+		childNodes: replaceNode,
+		insertBefore: insert,
+		appendChild: insert,
+		removeChild( c ) {
+			parent.removeChild( c );
+		},
+	} );
+};
diff --git a/assets/js/blocks/product-query/full-vdom/router.js b/assets/js/blocks/product-query/full-vdom/router.js
index b70b8fb894b..69d1d65bcf4 100644
--- a/assets/js/blocks/product-query/full-vdom/router.js
+++ b/assets/js/blocks/product-query/full-vdom/router.js
@@ -1,59 +1,17 @@
-/**
- * External dependencies
- */
-import { hydrate, render } from 'preact';
-
 /**
  * Internal dependencies
  */
-import { toVdom } from './vdom';
+import {
+	createRootFragments,
+	cleanUrl,
+	fetchPage,
+	toVdoms,
+	renderVdoms,
+} from './router-helpers';
 
-// The root to render the vdom (document.body).
-let rootFragment;
-
-// The cache of visited and prefetched pages.
+// The cache of all the visited or prefetched pages.
 const pages = new Map();
 
-// For wrapperless hydration of document.body.
-// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
-const createRootFragment = ( parent, replaceNode ) => {
-	replaceNode = [].concat( replaceNode );
-	const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
-	function insert( c, r ) {
-		parent.insertBefore( c, r || s );
-	}
-	return ( parent.__k = {
-		nodeType: 1,
-		parentNode: parent,
-		firstChild: replaceNode[ 0 ],
-		childNodes: replaceNode,
-		insertBefore: insert,
-		appendChild: insert,
-		removeChild( c ) {
-			parent.removeChild( c );
-		},
-	} );
-};
-
-// Helper function to await until the CPU is idle.
-const idle = () =>
-	new Promise( ( resolve ) => window.requestIdleCallback( resolve ) );
-
-// Helper to remove domain and hash from the URL. We are only interesting in
-// caching the path and the query.
-const cleanUrl = ( url ) => {
-	const u = new URL( url, 'http://a.bc' );
-	return u.pathname + u.search;
-};
-
-// Fetch a new page and convert it to a static virtual DOM.
-const fetchPage = async ( url ) => {
-	const html = await window.fetch( url ).then( ( res ) => res.text() );
-	await idle(); // Wait until CPU is idle to do the parsing and vdom.
-	const dom = new window.DOMParser().parseFromString( html, 'text/html' );
-	return toVdom( dom.body );
-};
-
 // Prefetch a page. We store the promise to avoid triggering a second fetch for
 // a page if a fetching has already started.
 export const prefetch = ( url ) => {
@@ -63,42 +21,48 @@ export const prefetch = ( url ) => {
 	}
 };
 
-window.prefetch = prefetch;
-
 // Navigate to a new page.
-export const navigate = async ( href ) => {
-	const url = cleanUrl( href );
+export const navigate = async ( href, { scroll } = { scroll: false } ) => {
+	const url = cleanUrl( href ); // Remove hash.
+
+	// Get the new page and render each Product Query block.
 	prefetch( url );
-	const vdom = await pages.get( url );
-	render( vdom, rootFragment );
+	const vdoms = await pages.get( url );
+	renderVdoms( vdoms );
+
+	// Change the history.
 	window.history.pushState( {}, '', href );
-};
 
-window.navigate = navigate;
+	// Update the scroll, depending on the option. True by default.
+	if ( scroll === 'smooth' ) {
+		window.scrollTo( { top: 0, left: 0, behavior: 'smooth' } );
+	} else if ( scroll !== false ) {
+		window.scrollTo( 0, 0 );
+	}
+};
 
 // Listen to the back and forward buttons and restore the page if it's in
-// the cache.
+// the cache. If not, refresh the page.
 window.addEventListener( 'popstate', async () => {
 	const previousUrl = cleanUrl( window.location ); // Remove hash.
 	if ( pages.has( previousUrl ) ) {
-		const vdom = await pages.get( previousUrl );
-		render( vdom, rootFragment );
+		const vdoms = await pages.get( previousUrl );
+		renderVdoms( vdoms );
 	} else {
 		window.location.reload();
 	}
 } );
 
-// Initialize the router with the initial DOM.
+// Initialize the router.
 document.addEventListener( 'DOMContentLoaded', async () => {
-	// Create the root fragment to hydrate everything.
-	rootFragment = createRootFragment(
-		document.documentElement,
-		document.body
-	);
-	const url = cleanUrl( window.location ); // Remove hash.
+	// Create root fragments for each Product Query block.
+	createRootFragments();
+
+	// Get the virtual DOM of each Product Query block and store them in the
+	// cache.
+	const vdoms = toVdoms( document.body );
+	pages.set( cleanUrl( window.location ), Promise.resolve( vdoms ) );
 
-	await idle(); // Wait until the CPU is idle to do the hydration.
-	const vdom = toVdom( document.body );
-	pages.set( url, Promise.resolve( vdom ) );
-	hydrate( vdom, rootFragment );
+	// Render the virtual DOM of each Product Query block.
+	renderVdoms( vdoms, true );
 } );
diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index bfb4d8f7c08..a0a693ff19d 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -52,11 +52,11 @@ public function enqueue_script( $block_content, $block, $instance ) {
 			// Enqueue the full vdom scripts.
 			$this->enqueue_scripts();
 
-			// Add "query-id-${query_id}" class.
-			// TODO: Replace with WP_HTML_Walker when available.
+			// Add the "woo-product-query" and "query-id-${query_id}" class names.
+			// TODO: Replace regular expression with WP_HTML_Walker when available.
 			$block_content = preg_replace(
 				'/([ "])wp-block-query([ "])/', 
-				'$1wp-block-query query-id-' . $block['attrs']['queryId'] . '$2', 
+				'$1wp-block-query woo-product-query query-id-' . $block['attrs']['queryId'] . '$2', 
 				$block_content
 			);
 		} 

From 926f62c56f92778780eb49313307769d9c6f73d8 Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Tue, 20 Sep 2022 15:30:28 +0200
Subject: [PATCH 09/11] Product Query - Add support for the Filter By
 Attributes block #6790

Product Query - Add support for the Filter By Attributes block
---
 src/BlockTypes/AttributeFilter.php |   4 +-
 src/BlockTypes/ProductQuery.php    | 112 +++++++++++++++++++++++++++--
 2 files changed, 110 insertions(+), 6 deletions(-)

diff --git a/src/BlockTypes/AttributeFilter.php b/src/BlockTypes/AttributeFilter.php
index ec2d0bf3441..4b9d06ee7e9 100644
--- a/src/BlockTypes/AttributeFilter.php
+++ b/src/BlockTypes/AttributeFilter.php
@@ -10,7 +10,9 @@ class AttributeFilter extends AbstractBlock {
 	 *
 	 * @var string
 	 */
-	protected $block_name = 'attribute-filter';
+	protected $block_name      = 'attribute-filter';
+	const FILTER_QUERY_VAR     = 'filter_';
+	const QUERY_TYPE_QUERY_VAR = 'query_type_';
 
 	/**
 	 * Extra data passed through from server to client for block.
diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index cdf4e15b78c..a34d8045765 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -19,6 +19,13 @@ class ProductQuery extends AbstractBlock {
 	 */
 	protected $parsed_block;
 
+	/**
+	 * All the query args related to the filter by attributes block.
+	 *
+	 * @var array
+	 */
+	protected $attributes_filter_query_args = array();
+
 	/**
 	 * Initialize this block type.
 	 *
@@ -86,6 +93,8 @@ public function build_query( $query ) {
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 			'meta_query'     => array(),
+			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+			'tax_query'      => array(),
 		);
 
 		$queries_attributes = $this->get_queries_by_attributes( $variation_props );
@@ -100,6 +109,9 @@ function( $acc, $query ) {
 						// Ignoring the warning of not using meta queries.
 						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 						$acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query'];
+						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+						$acc['tax_query'] = isset( $query['tax_query'] ) ? array_merge( $acc['tax_query'], array( $query['tax_query'] ) ) : $acc['tax_query'];
+
 						return $acc;
 			},
 			$common_query_values
@@ -126,21 +138,58 @@ private function get_on_sale_products_query() {
 	 * @return array
 	 */
 	public function set_query_vars( $qvars ) {
-		$filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR );
-		return array_merge( $qvars, $filter_query_args );
+		$attributes_filter_query_args = array_reduce(
+			array_values( $this->get_filter_by_attributes_query_vars() ),
+			function( $acc, $array ) {
+				return array_merge( array_values( $array ), $acc );
+			},
+			array()
+		);
+
+		$price_filter_query_args = array( PriceFilter::MIN_PRICE_QUERY_VAR, PriceFilter::MAX_PRICE_QUERY_VAR );
+		return array_merge( $qvars, $price_filter_query_args, $attributes_filter_query_args );
+	}
+
+	/**
+	 * Get all the query args related to the filter by attributes block.
+	 *
+	 * @return array
+	 */
+	private function get_filter_by_attributes_query_vars() {
+
+		if ( ! empty( $this->attributes_filter_query_args ) ) {
+			return $this->attributes_filter_query_args;
+		}
+
+		$this->attributes_filter_query_args = array_reduce(
+			wc_get_attribute_taxonomies(),
+			function( $acc, $attribute ) {
+				$acc[ $attribute->attribute_name ] = array(
+					'filter'     => AttributeFilter::FILTER_QUERY_VAR . $attribute->attribute_name,
+					'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR . $attribute->attribute_name,
+				);
+				return $acc;
+			},
+			array()
+		);
+
+		return $this->attributes_filter_query_args;
 	}
 
 	/**
-	 * Return queries that are generated by query args
+	 * Return queries that are generated by query args.
 	 *
 	 * @return array
 	 */
 	private function get_queries_by_applied_filters() {
-		return array( 'price_filter' => $this->get_filter_by_price_query() );
+		return array(
+			'price_filter'      => $this->get_filter_by_price_query(),
+			'attributes_filter' => $this->get_filter_by_attributes_query(),
+		);
 	}
 
 	/**
-	 * Return queries that are generated by attributes
+	 * Return queries that are generated by block attributes.
 	 *
 	 * @param array $variation_props Dedicated attributes for the variation.
 	 * @return array
@@ -175,6 +224,10 @@ private function get_filter_by_price_query() {
 			'type'    => 'numeric',
 		];
 
+		if ( empty( $min_price_query ) && empty( $max_price_query ) ) {
+			return array();
+		}
+
 		return array(
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -186,4 +239,53 @@ private function get_filter_by_price_query() {
 		);
 	}
 
+	/**
+	 * Return a query that filters products by attributes.
+	 *
+	 * @return array
+	 */
+	private function get_filter_by_attributes_query() {
+		$attributes_filter_query_args = $this->get_filter_by_attributes_query_vars();
+
+		$queries = array_reduce(
+			$attributes_filter_query_args,
+			function( $acc, $query_args ) {
+				$attribute_name       = $query_args['filter'];
+				$attribute_query_type = $query_args['query_type'];
+
+				$attribute_name_value       = get_query_var( $attribute_name );
+				$attribute_query_type_value = get_query_var( $attribute_query_type );
+
+				if ( empty( $attribute_name_value ) ) {
+					return $acc;
+				}
+
+				$attribute_name_value = explode( ',', $attribute_name_value );
+
+				$acc[] = array(
+					'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR, 'pa_', $attribute_name ),
+					'field'    => 'slug',
+					'terms'    => $attribute_name_value,
+					'operator' => 'and' === $attribute_query_type_value ? 'AND' : 'IN',
+				);
+
+				return $acc;
+			},
+			array()
+		);
+
+		if ( empty( $filters ) ) {
+			return array();
+		}
+
+		return array(
+			// Ignoring the warning of not using meta queries.
+			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
+			'tax_query' => array(
+				'relation' => 'AND',
+				$queries,
+			),
+		);
+	}
+
 }

From 1d56a2e90d0ef49330eef6b0133f117237d70e22 Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Wed, 21 Sep 2022 18:56:49 +0200
Subject: [PATCH 10/11] fix bugged pagination and on-sale filter after refactor

---
 src/BlockTypes/ProductQuery.php | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/src/BlockTypes/ProductQuery.php b/src/BlockTypes/ProductQuery.php
index cdf4e15b78c..1ba8a4ad281 100644
--- a/src/BlockTypes/ProductQuery.php
+++ b/src/BlockTypes/ProductQuery.php
@@ -83,6 +83,7 @@ public function build_query( $query ) {
 			'posts_per_page' => $query['posts_per_page'],
 			'orderby'        => $query['orderby'],
 			'order'          => $query['order'],
+			'offset'         => $query['offset'],
 			// Ignoring the warning of not using meta queries.
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 			'meta_query'     => array(),
@@ -97,10 +98,13 @@ public function build_query( $query ) {
 				$queries_filters
 			),
 			function( $acc, $query ) {
-						// Ignoring the warning of not using meta queries.
-						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
-						$acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query'];
-						return $acc;
+				if ( isset( $query['post__in'] ) ) {
+					$acc['post__in'] = isset( $acc['post__in'] ) ? array_merge( $acc['post__in'], $query['post__in'] ) : $query['post__in'];
+				}
+				// Ignoring the warning of not using meta queries.
+				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+				$acc['meta_query'] = isset( $query['meta_query'] ) ? array_merge( $acc['meta_query'], array( $query['meta_query'] ) ) : $acc['meta_query'];
+				return $acc;
 			},
 			$common_query_values
 		);
@@ -115,7 +119,7 @@ function( $acc, $query ) {
 	 */
 	private function get_on_sale_products_query() {
 		return array(
-			'post_in' => wc_get_product_ids_on_sale(),
+			'post__in' => wc_get_product_ids_on_sale(),
 		);
 	}
 

From 1eea9681e797f12dad0976afe8e9eb28087a9142 Mon Sep 17 00:00:00 2001
From: Luigi <gigitux@gmail.com>
Date: Thu, 22 Sep 2022 11:43:44 +0200
Subject: [PATCH 11/11] add compatibility with filters

---
 assets/js/blocks/attribute-filter/block.tsx | 80 +++++++++++----------
 assets/js/blocks/price-filter/block.tsx     | 46 ++++++------
 assets/js/utils/filters.ts                  |  7 +-
 3 files changed, 72 insertions(+), 61 deletions(-)

diff --git a/assets/js/blocks/attribute-filter/block.tsx b/assets/js/blocks/attribute-filter/block.tsx
index a7b2a13d266..43253f0c4fe 100644
--- a/assets/js/blocks/attribute-filter/block.tsx
+++ b/assets/js/blocks/attribute-filter/block.tsx
@@ -275,48 +275,54 @@ const AttributeFilterBlock = ( {
 	 * @param {boolean} allFiltersRemoved If there are active filters or not.
 	 */
 	const updateFilterUrl = useCallback(
-		( query, allFiltersRemoved = false ) => {
-			if ( allFiltersRemoved ) {
-				if ( ! attributeObject?.taxonomy ) {
-					return;
-				}
-				const currentQueryArgKeys = Object.keys(
-					getQueryArgs( window.location.href )
-				);
-
-				const parsedTaxonomy = parseTaxonomyToGenerateURL(
-					attributeObject.taxonomy
-				);
-
-				const url = currentQueryArgKeys.reduce(
-					( currentUrl, queryArg ) =>
-						queryArg.includes(
-							PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy
-						) ||
-						queryArg.includes(
-							PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy
-						)
-							? removeQueryArgs( currentUrl, queryArg )
-							: currentUrl,
-					window.location.href
-				);
-
-				const newUrl = formatParams( url, query );
-				changeUrl( newUrl );
-			} else {
-				const newUrl = formatParams( pageUrl, query );
-				const currentQueryArgs = getQueryArgs( window.location.href );
-				const newUrlQueryArgs = getQueryArgs( newUrl );
-
-				if ( ! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs ) ) {
+		async ( query, allFiltersRemoved = false ) => {
+			return await new Promise( () => {
+				if ( allFiltersRemoved ) {
+					if ( ! attributeObject?.taxonomy ) {
+						return;
+					}
+					const currentQueryArgKeys = Object.keys(
+						getQueryArgs( window.location.href )
+					);
+
+					const parsedTaxonomy = parseTaxonomyToGenerateURL(
+						attributeObject.taxonomy
+					);
+
+					const url = currentQueryArgKeys.reduce(
+						( currentUrl, queryArg ) =>
+							queryArg.includes(
+								PREFIX_QUERY_ARG_QUERY_TYPE + parsedTaxonomy
+							) ||
+							queryArg.includes(
+								PREFIX_QUERY_ARG_FILTER_TYPE + parsedTaxonomy
+							)
+								? removeQueryArgs( currentUrl, queryArg )
+								: currentUrl,
+						window.location.href
+					);
+
+					const newUrl = formatParams( url, query );
 					changeUrl( newUrl );
+				} else {
+					const newUrl = formatParams( pageUrl, query );
+					const currentQueryArgs = getQueryArgs(
+						window.location.href
+					);
+					const newUrlQueryArgs = getQueryArgs( newUrl );
+
+					if (
+						! isQueryArgsEqual( currentQueryArgs, newUrlQueryArgs )
+					) {
+						changeUrl( newUrl );
+					}
 				}
-			}
+			} );
 		},
 		[ pageUrl, attributeObject?.taxonomy ]
 	);
 
-	const onSubmit = ( checkedFilters: string[] ) => {
+	const onSubmit = async ( checkedFilters: string[] ) => {
 		const query = updateAttributeFilter(
 			productAttributesQuery,
 			setProductAttributesQuery,
@@ -325,7 +331,7 @@ const AttributeFilterBlock = ( {
 			blockAttributes.queryType === 'or' ? 'in' : 'and'
 		);
 
-		updateFilterUrl( query, checkedFilters.length === 0 );
+		await updateFilterUrl( query, checkedFilters.length === 0 );
 	};
 
 	const updateCheckedFilters = useCallback(
diff --git a/assets/js/blocks/price-filter/block.tsx b/assets/js/blocks/price-filter/block.tsx
index 4e191b55658..2762665e03e 100644
--- a/assets/js/blocks/price-filter/block.tsx
+++ b/assets/js/blocks/price-filter/block.tsx
@@ -169,30 +169,32 @@ const PriceFilterBlock = ( {
 
 	// Updates the query based on slider values.
 	const onSubmit = useCallback(
-		( newMinPrice, newMaxPrice ) => {
-			const finalMaxPrice =
-				newMaxPrice >= Number( maxConstraint )
-					? undefined
-					: newMaxPrice;
-			const finalMinPrice =
-				newMinPrice <= Number( minConstraint )
-					? undefined
-					: newMinPrice;
-
-			if ( window ) {
-				const newUrl = formatParams( window.location.href, {
-					min_price: finalMinPrice / 10 ** currency.minorUnit,
-					max_price: finalMaxPrice / 10 ** currency.minorUnit,
-				} );
-
-				// If the params have changed, lets update the filter URL.
-				if ( window.location.href !== newUrl ) {
-					changeUrl( newUrl );
+		async ( newMinPrice, newMaxPrice ) => {
+			return await new Promise( () => {
+				const finalMaxPrice =
+					newMaxPrice >= Number( maxConstraint )
+						? undefined
+						: newMaxPrice;
+				const finalMinPrice =
+					newMinPrice <= Number( minConstraint )
+						? undefined
+						: newMinPrice;
+
+				if ( window ) {
+					const newUrl = formatParams( window.location.href, {
+						min_price: finalMinPrice / 10 ** currency.minorUnit,
+						max_price: finalMaxPrice / 10 ** currency.minorUnit,
+					} );
+
+					// If the params have changed, lets update the filter URL.
+					if ( window.location.href !== newUrl ) {
+						changeUrl( newUrl );
+					}
 				}
-			}
 
-			setMinPriceQuery( finalMinPrice );
-			setMaxPriceQuery( finalMaxPrice );
+				setMinPriceQuery( finalMinPrice );
+				setMaxPriceQuery( finalMaxPrice );
+			} );
 		},
 		[
 			minConstraint,
diff --git a/assets/js/utils/filters.ts b/assets/js/utils/filters.ts
index 98548288fba..5180860e127 100644
--- a/assets/js/utils/filters.ts
+++ b/assets/js/utils/filters.ts
@@ -5,6 +5,8 @@ import { getQueryArg } from '@wordpress/url';
 import { getSettingWithCoercion } from '@woocommerce/settings';
 import { isBoolean } from '@woocommerce/types';
 
+import { navigate } from '../blocks/product-query/full-vdom';
+
 const filteringForPhpTemplate = getSettingWithCoercion(
 	'is_rendering_php_template',
 	false,
@@ -32,10 +34,11 @@ export function getUrlParameter( name: string ) {
  *
  * @param {string} newUrl New URL to be set.
  */
-export function changeUrl( newUrl: string ) {
-	if ( filteringForPhpTemplate ) {
+export async function changeUrl( newUrl: string ) {
+	if ( false ) {
 		window.location.href = newUrl;
 	} else {
 		window.history.replaceState( {}, '', newUrl );
+		await navigate( newUrl );
 	}
 }