plugin updates
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* ContentProcessor class.
|
||||
*
|
||||
* Process images for content
|
||||
*/
|
||||
class ContentProcessor {
|
||||
|
||||
/**
|
||||
* Summarize the business description to ensure better results are returned by AI.
|
||||
*
|
||||
* @param string $business_description The business description.
|
||||
* @param Connection $ai_connection The AI connection.
|
||||
* @param string $token The JWT token.
|
||||
* @param integer $character_limit The character limit for the business description.
|
||||
*
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
public static function summarize_business_description( $business_description, $ai_connection, $token, $character_limit = 150 ) {
|
||||
if ( empty( $business_description ) ) {
|
||||
return new WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( strlen( $business_description ) > $character_limit ) {
|
||||
$prompt = sprintf( 'You are a professional writer. Read the following business description and write a text with less than %s characters to summarize the products the business is selling: "%s". Make sure you do not add double quotes in your response. Do not add any explanations in the response', $character_limit, $business_description );
|
||||
|
||||
$response = $ai_connection->fetch_ai_response( $token, $prompt, 30 );
|
||||
|
||||
$business_description = $response['completion'] ?? $business_description;
|
||||
}
|
||||
|
||||
return $business_description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that images are provided for assignment to products and patterns.
|
||||
*
|
||||
* @param array|WP_Error $images The array of images.
|
||||
* @param Connection $ai_connection The AI connection.
|
||||
* @param string $token The JWT token.
|
||||
* @param string $business_description The business description.
|
||||
*
|
||||
* @return array|int|mixed|string|WP_Error
|
||||
*/
|
||||
public static function verify_images( $images, $ai_connection, $token, $business_description ) {
|
||||
if ( ! is_wp_error( $images ) && ! empty( $images['images'] ) && ! empty( $images['search_term'] ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
|
||||
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( empty( $images['images'] ) || empty( $images['search_term'] ) ) {
|
||||
return new WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the size of images for optimal performance on products and patterns.
|
||||
*
|
||||
* @param string $image_url The image URL.
|
||||
* @param string $usage_type The usage type of the image. Either 'products' or 'patterns'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function adjust_image_size( $image_url, $usage_type ) {
|
||||
$parsed_url = wp_parse_url( $image_url );
|
||||
|
||||
if ( ! isset( $parsed_url['query'] ) ) {
|
||||
return $image_url;
|
||||
}
|
||||
|
||||
$width = 'products' === $usage_type ? 400 : 500;
|
||||
|
||||
parse_str( $parsed_url['query'], $query_params );
|
||||
|
||||
unset( $query_params['h'], $query_params['w'] );
|
||||
$query_params['w'] = $width;
|
||||
$url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
|
||||
|
||||
return add_query_arg( $query_params, $url );
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Pattern Images Helper class.
|
||||
* Patterns Helper class.
|
||||
*/
|
||||
class PatternsHelper {
|
||||
/**
|
||||
* Returns the image for the given pattern.
|
||||
* Fetches the AI-selected image for the pattern or returns the default image.
|
||||
*
|
||||
* @param array $images The array of images.
|
||||
* @param int $index The index of the image to return.
|
||||
@@ -17,7 +17,7 @@ class PatternsHelper {
|
||||
*
|
||||
* @return string The image.
|
||||
*/
|
||||
public static function get_image_url( array $images, int $index, string $default_image ): string {
|
||||
public static function get_image_url( $images, $index, $default_image ) {
|
||||
$image = filter_var( $default_image, FILTER_VALIDATE_URL )
|
||||
? $default_image
|
||||
: plugins_url( $default_image, dirname( __DIR__, 2 ) );
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use WP_Error;
|
||||
@@ -8,7 +8,7 @@ use WP_Error;
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*/
|
||||
class PatternUpdater {
|
||||
class UpdatePatterns {
|
||||
|
||||
/**
|
||||
* All patterns that are actively in use in the Assembler.
|
||||
@@ -36,40 +36,12 @@ class PatternUpdater {
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function generate_content( $ai_connection, $token, $images, $business_description ) {
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
if ( ! isset( $images['images'] ) ) {
|
||||
return new \WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 === count( $images['images'] ) ) {
|
||||
$images = get_transient( 'woocommerce_ai_managed_images' );
|
||||
}
|
||||
|
||||
if ( empty( $images['images'] ) ) {
|
||||
return new WP_Error( 'no_images_found', __( 'No images found.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
// This is required in case something interrupts the execution of the script and the endpoint is called again on retry.
|
||||
set_transient( 'woocommerce_ai_managed_images', $images, 60 );
|
||||
|
||||
$patterns_dictionary = self::get_patterns_dictionary();
|
||||
$images = ContentProcessor::verify_images( $images, $ai_connection, $token, $business_description );
|
||||
$patterns_dictionary = PatternsHelper::get_patterns_dictionary();
|
||||
|
||||
if ( is_wp_error( $patterns_dictionary ) ) {
|
||||
return $patterns_dictionary;
|
||||
@@ -336,7 +308,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['titles'][ $j ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['titles'][ $j ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -348,7 +320,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['descriptions'][ $k ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['descriptions'][ $k ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -360,7 +332,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['buttons'][ $l ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['buttons'][ $l ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -372,6 +344,18 @@ class PatternUpdater {
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the string from the AI generated content. It removes double quotes that can cause issues when
|
||||
* decoding the patterns JSON.
|
||||
*
|
||||
* @param string $string The string to be sanitized.
|
||||
*
|
||||
* @return string The sanitized string.
|
||||
*/
|
||||
private function sanitize_string( $string ) {
|
||||
return str_replace( '"', '', $string );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign selected images to patterns.
|
||||
*
|
||||
@@ -469,7 +453,9 @@ class PatternUpdater {
|
||||
continue;
|
||||
}
|
||||
|
||||
$images[] = $selected_image['URL'];
|
||||
$selected_image_url = ContentProcessor::adjust_image_size( $selected_image['URL'], 'patterns' );
|
||||
|
||||
$images[] = $selected_image_url;
|
||||
$alts[] = $selected_image['title'];
|
||||
}
|
||||
|
||||
@@ -477,7 +463,7 @@ class PatternUpdater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected image format. Defaults to landscape.
|
||||
* Returns the selected image format. Defaults to portrait.
|
||||
*
|
||||
* @param array $selected_image The selected image to be assigned to the pattern.
|
||||
*
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use WP_Error;
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*/
|
||||
class ProductUpdater {
|
||||
class UpdateProducts {
|
||||
|
||||
/**
|
||||
* The dummy products.
|
||||
@@ -62,41 +62,20 @@ class ProductUpdater {
|
||||
* @return array|WP_Error The generated content for the products. An error if the content could not be generated.
|
||||
*/
|
||||
public function generate_content( $ai_connection, $token, $images, $business_description ) {
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
if ( ! isset( $images['images'] ) || ! isset( $images['search_term'] ) ) {
|
||||
$images = get_transient( 'woocommerce_ai_managed_images' );
|
||||
}
|
||||
$images = ContentProcessor::verify_images( $images, $ai_connection, $token, $business_description );
|
||||
|
||||
if ( ! isset( $images['images'] ) || ! isset( $images['search_term'] ) ) {
|
||||
return new \WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
// This is required in case something interrupts the execution of the script and the endpoint is called again on retry.
|
||||
set_transient( 'woocommerce_ai_managed_images', $images, 60 );
|
||||
|
||||
if ( empty( $business_description ) ) {
|
||||
return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
|
||||
return array(
|
||||
'product_content' => array(),
|
||||
);
|
||||
} else {
|
||||
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$dummy_products_to_update = $this->fetch_dummy_products_to_update();
|
||||
|
||||
if ( is_wp_error( $dummy_products_to_update ) ) {
|
||||
@@ -296,7 +275,7 @@ class ProductUpdater {
|
||||
*
|
||||
* @param array $ai_generated_product_content The AI-generated product content.
|
||||
*
|
||||
* @return string|void
|
||||
* @return void|WP_Error
|
||||
*/
|
||||
public function update_product_content( $ai_generated_product_content ) {
|
||||
if ( ! isset( $ai_generated_product_content['product_id'] ) ) {
|
||||
@@ -316,7 +295,7 @@ class ProductUpdater {
|
||||
$product_image_id = $this->product_image_upload( $product->get_id(), $ai_generated_product_content['image']['src'], $ai_generated_product_content['image']['alt'] );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return $product_image_id->get_error_message();
|
||||
return new \WP_Error( 'error_uploading_image', $product_image_id->get_error_message() );
|
||||
}
|
||||
|
||||
$this->product_update( $product, $product_image_id, $ai_generated_product_content['title'], $ai_generated_product_content['description'], $ai_generated_product_content['price'] );
|
||||
@@ -342,37 +321,7 @@ class ProductUpdater {
|
||||
set_time_limit( 150 );
|
||||
wp_raise_memory_limit( 'image' );
|
||||
|
||||
$product_image_id = media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return $product_image_id->get_error_message();
|
||||
}
|
||||
|
||||
return $product_image_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the size of the image for the product to improve performance and
|
||||
* avoid memory exhaustion errors when uploading them to the media library.
|
||||
*
|
||||
* @param string $image_url The image URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function adjust_image_size_for_products( $image_url ) {
|
||||
$parsed_url = wp_parse_url( $image_url );
|
||||
|
||||
if ( ! isset( $parsed_url['query'] ) ) {
|
||||
return $image_url;
|
||||
}
|
||||
|
||||
parse_str( $parsed_url['query'], $query_params );
|
||||
|
||||
unset( $query_params['h'], $query_params['w'] );
|
||||
$query_params['w'] = 300;
|
||||
$new_query_string = http_build_query( $query_params );
|
||||
|
||||
return $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'] . '?' . $new_query_string;
|
||||
return media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,7 +339,7 @@ class ProductUpdater {
|
||||
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
|
||||
|
||||
if ( wc_is_valid_url( $image_src ) ) {
|
||||
$image_src = $this->adjust_image_size_for_products( $ai_selected_images[ $i ]['URL'] );
|
||||
$image_src = ContentProcessor::adjust_image_size( $image_src, 'products' );
|
||||
}
|
||||
|
||||
$image_alt = $ai_selected_images[ $i ]['title'] ?? '';
|
||||
@@ -400,8 +349,8 @@ class ProductUpdater {
|
||||
'description' => 'A product description',
|
||||
'price' => 'The product price',
|
||||
'image' => [
|
||||
'src' => esc_url( $image_src ),
|
||||
'alt' => esc_attr( $image_alt ),
|
||||
'src' => $image_src,
|
||||
'alt' => $image_alt,
|
||||
],
|
||||
'product_id' => $dummy_products_to_update[ $i ]->get_id(),
|
||||
];
|
||||
@@ -422,8 +371,10 @@ class ProductUpdater {
|
||||
* @return array|int|string|\WP_Error
|
||||
*/
|
||||
public function assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description, $search_term ) {
|
||||
if ( empty( $business_description ) ) {
|
||||
return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate content for your site.', 'woocommerce' ) );
|
||||
$business_description = ContentProcessor::summarize_business_description( $business_description, $ai_connection, $token, 100 );
|
||||
|
||||
if ( is_wp_error( $business_description ) ) {
|
||||
return $business_description;
|
||||
}
|
||||
|
||||
$prompts = [];
|
||||
@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\Assets;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Exception;
|
||||
use Automattic\Jetpack\Constants;
|
||||
/**
|
||||
* The Api class provides an interface to various asset registration helpers.
|
||||
*
|
||||
@@ -11,6 +12,14 @@ use Exception;
|
||||
* @since 2.5.0
|
||||
*/
|
||||
class Api {
|
||||
|
||||
/**
|
||||
* Stores the prefixed WC version. Used because the WC Blocks version has not been updated since the monorepo merge.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $wc_version;
|
||||
|
||||
/**
|
||||
* Stores inline scripts already enqueued.
|
||||
*
|
||||
@@ -59,6 +68,8 @@ class Api {
|
||||
* @param Package $package An instance of Package.
|
||||
*/
|
||||
public function __construct( Package $package ) {
|
||||
// Use wc- prefix here to prevent collisions when WC Core version catches up to a version previously used by the WC Blocks feature plugin.
|
||||
$this->wc_version = 'wc-' . Constants::get_constant( 'WC_VERSION' );
|
||||
$this->package = $package;
|
||||
$this->disable_cache = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ! $this->package->feature()->is_production_environment();
|
||||
|
||||
@@ -84,7 +95,7 @@ class Api {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $this->package->get_path() . $file ) ) {
|
||||
return filemtime( $this->package->get_path( trim( $file, '/' ) ) );
|
||||
}
|
||||
return $this->package->get_version();
|
||||
return $this->wc_version;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +134,7 @@ class Api {
|
||||
* @return string The generated hash.
|
||||
*/
|
||||
private function get_script_data_hash() {
|
||||
return md5( get_option( 'siteurl', '' ) . $this->package->get_version() . $this->package->get_path() );
|
||||
return md5( get_option( 'siteurl', '' ) . $this->wc_version . $this->package->get_path() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +154,7 @@ class Api {
|
||||
empty( $transient_value ) ||
|
||||
empty( $transient_value['script_data'] ) ||
|
||||
empty( $transient_value['version'] ) ||
|
||||
$transient_value['version'] !== $this->package->get_version() ||
|
||||
$transient_value['version'] !== $this->wc_version ||
|
||||
empty( $transient_value['hash'] ) ||
|
||||
$transient_value['hash'] !== $this->script_data_hash
|
||||
) {
|
||||
@@ -165,7 +176,7 @@ class Api {
|
||||
wp_json_encode(
|
||||
array(
|
||||
'script_data' => $this->script_data,
|
||||
'version' => $this->package->get_version(),
|
||||
'version' => $this->wc_version,
|
||||
'hash' => $this->script_data_hash,
|
||||
)
|
||||
),
|
||||
|
||||
@@ -67,8 +67,8 @@ final class AssetsController {
|
||||
// Register the interactivity components here for now.
|
||||
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
|
||||
$this->api->register_script( 'wc-interactivity-checkbox-list', 'assets/client/blocks/wc-interactivity-checkbox-list.js', array() );
|
||||
$this->register_style( 'wc-interactivity-checkbox-list', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-checkbox-list', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-dropdown', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-dropdown', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-checkbox-list', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-checkbox-list', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-dropdown', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-dropdown', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
|
||||
wp_add_inline_script(
|
||||
'wc-blocks-middleware',
|
||||
@@ -279,7 +279,7 @@ final class AssetsController {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( \Automattic\WooCommerce\Blocks\Package::get_path() . $file ) ) {
|
||||
return filemtime( \Automattic\WooCommerce\Blocks\Package::get_path() . $file );
|
||||
}
|
||||
return \Automattic\WooCommerce\Blocks\Package::get_version();
|
||||
return $this->api->wc_version;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace Automattic\WooCommerce\Blocks;
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
|
||||
/**
|
||||
* Registers patterns under the `./patterns/` directory and updates their content.
|
||||
@@ -369,13 +369,13 @@ class BlockPatterns {
|
||||
return $images->get_error_message();
|
||||
}
|
||||
|
||||
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
$populate_patterns = ( new UpdatePatterns() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_patterns ) ) {
|
||||
return $populate_patterns->get_error_message();
|
||||
}
|
||||
|
||||
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_products ) ) {
|
||||
return $populate_products->get_error_message();
|
||||
|
||||
@@ -70,6 +70,15 @@ abstract class AbstractBlock {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the interactivity namespace. Only used when utilizing the interactivity API.
|
||||
|
||||
* @return string The interactivity namespace, used to namespace interactivity API actions and state.
|
||||
*/
|
||||
protected function get_full_block_name() {
|
||||
return $this->namespace . '/' . $this->block_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default render_callback for all blocks. This will ensure assets are enqueued just in time, then render
|
||||
* the block (if applicable).
|
||||
|
||||
@@ -614,6 +614,10 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( empty( $this->attributes['contentVisibility']['image'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! $product->is_on_sale() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ class Cart extends AbstractBlock {
|
||||
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
|
||||
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
|
||||
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
|
||||
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones(), true );
|
||||
|
||||
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
|
||||
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ), true );
|
||||
|
||||
@@ -169,7 +169,8 @@ class Checkout extends AbstractBlock {
|
||||
<div data-block-name="woocommerce/checkout-shipping-address-block" class="wp-block-woocommerce-checkout-shipping-address-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-billing-address-block" class="wp-block-woocommerce-checkout-billing-address-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-shipping-methods-block" class="wp-block-woocommerce-checkout-shipping-methods-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"></div>' .
|
||||
<div data-block-name="woocommerce/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' .
|
||||
( isset( $attributes['showOrderNotes'] ) && false === $attributes['showOrderNotes'] ? '' : '<div data-block-name="woocommerce/checkout-order-note-block" class="wp-block-woocommerce-checkout-order-note-block"></div>' ) .
|
||||
( isset( $attributes['showPolicyLinks'] ) && false === $attributes['showPolicyLinks'] ? '' : '<div data-block-name="woocommerce/checkout-terms-block" class="wp-block-woocommerce-checkout-terms-block"></div>' ) .
|
||||
'<div data-block-name="woocommerce/checkout-actions-block" class="wp-block-woocommerce-checkout-actions-block"></div>
|
||||
@@ -217,6 +218,18 @@ class Checkout extends AbstractBlock {
|
||||
$content = preg_replace( $shipping_address_block_regex, $local_pickup_inner_blocks, $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Additional Information block to checkouts missing it.
|
||||
*/
|
||||
$additional_information_inner_blocks = '$0' . PHP_EOL . PHP_EOL . '<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' . PHP_EOL . PHP_EOL;
|
||||
$has_additional_information_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-additional-information-block"[^>]*?>/mi';
|
||||
$has_additional_information_block = preg_match( $has_additional_information_regex, $content );
|
||||
|
||||
if ( ! $has_additional_information_block ) {
|
||||
$payment_block_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"[^>]*?><\/div>/mi';
|
||||
$content = preg_replace( $payment_block_regex, $additional_information_inner_blocks, $content );
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -302,25 +315,7 @@ class Checkout extends AbstractBlock {
|
||||
}
|
||||
|
||||
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) {
|
||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||
$formatted_shipping_zones = array_reduce(
|
||||
$shipping_zones,
|
||||
function( $acc, $zone ) {
|
||||
$acc[] = [
|
||||
'id' => $zone['id'],
|
||||
'title' => $zone['zone_name'],
|
||||
'description' => $zone['formatted_zone_location'],
|
||||
];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$formatted_shipping_zones[] = [
|
||||
'id' => 0,
|
||||
'title' => __( 'International', 'woocommerce' ),
|
||||
'description' => __( 'Locations outside all other zones', 'woocommerce' ),
|
||||
];
|
||||
$this->asset_data_registry->add( 'activeShippingZones', $formatted_shipping_zones );
|
||||
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
|
||||
}
|
||||
|
||||
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
|
||||
@@ -480,6 +475,7 @@ class Checkout extends AbstractBlock {
|
||||
return [
|
||||
'Checkout',
|
||||
'CheckoutActionsBlock',
|
||||
'CheckoutAdditionalInformationBlock',
|
||||
'CheckoutBillingAddressBlock',
|
||||
'CheckoutContactInformationBlock',
|
||||
'CheckoutExpressPaymentBlock',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CheckoutAdditionalInformationBlock class.
|
||||
*/
|
||||
class CheckoutAdditionalInformationBlock extends AbstractInnerBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'checkout-additional-information-block';
|
||||
}
|
||||
@@ -28,15 +28,12 @@ class ClassicTemplate extends AbstractDynamicBlock {
|
||||
*/
|
||||
protected $api_version = '2';
|
||||
|
||||
const FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM = 'filter_stock_status';
|
||||
|
||||
/**
|
||||
* Initialize this block.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_filter( 'render_block', array( $this, 'add_alignment_class_to_wrapper' ), 10, 2 );
|
||||
add_filter( 'woocommerce_product_query_meta_query', array( $this, 'filter_products_by_stock' ) );
|
||||
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
|
||||
}
|
||||
|
||||
@@ -377,47 +374,6 @@ class ClassicTemplate extends AbstractDynamicBlock {
|
||||
return preg_replace( $pattern_get_class, '$0 ' . $align_class_and_style['class'], $content, 1 );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter products by stock status when as query param there is "filter_stock_status"
|
||||
*
|
||||
* @param array $meta_query Meta query.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_products_by_stock( $meta_query ) {
|
||||
global $wp_query;
|
||||
|
||||
if (
|
||||
is_admin() ||
|
||||
! $wp_query->is_main_query() ||
|
||||
! isset( $_GET[ self::FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
) {
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
$stock_status = array_keys( wc_get_product_stock_status_options() );
|
||||
$values = sanitize_text_field( wp_unslash( $_GET[ self::FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$values_to_array = explode( ',', $values );
|
||||
|
||||
$filtered_values = array_filter(
|
||||
$values_to_array,
|
||||
function( $value ) use ( $stock_status ) {
|
||||
return in_array( $value, $stock_status, true );
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! empty( $filtered_values ) ) {
|
||||
|
||||
$meta_query[] = array(
|
||||
'key' => '_stock_status',
|
||||
'value' => $filtered_values,
|
||||
'compare' => 'IN',
|
||||
);
|
||||
}
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
|
||||
|
||||
/**
|
||||
* CollectionFilters class.
|
||||
*/
|
||||
final class CollectionFilters extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-filters';
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $current_response = null;
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @see $this->register_block_type()
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
* - Hook into WP lifecycle.
|
||||
* - Register the block with WordPress.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
if ( ! is_admin() ) {
|
||||
/**
|
||||
* At this point, WP starts rendering the Collection Filters block,
|
||||
* we can safely unset the current response.
|
||||
*/
|
||||
$this->current_response = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-filters' ) ),
|
||||
'class' => 'wc-block-collection-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes_data['data-wc-navigation-id'] = sprintf(
|
||||
'wc-collection-filters-%s',
|
||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the context of inner blocks.
|
||||
*
|
||||
* @param array $context The block context.
|
||||
* @param array $parsed_block The parsed block.
|
||||
* @param WP_Block $parent_block The parent block.
|
||||
* @return array
|
||||
*/
|
||||
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
|
||||
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first direct child of Collection Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
if ( empty( $this->current_response ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated collection data from the API.
|
||||
* Loop through inner blocks and build a query string to pass to the API.
|
||||
*
|
||||
* @param WP_Block $block The block instance.
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data(
|
||||
add_query_arg(
|
||||
array_merge(
|
||||
$this->get_formatted_products_params( $block->context['query'] ?? array() ),
|
||||
$collection_data_params,
|
||||
),
|
||||
'/wc/store/v1/products/collection-data'
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $response['body'] ) ) {
|
||||
return json_decode( wp_json_encode( $response['body'] ), true );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inner blocks recursively.
|
||||
*
|
||||
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
|
||||
* @param array $results The results array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_collection_data_params( $inner_blocks, &$results = array() ) {
|
||||
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
|
||||
foreach ( $inner_blocks as $inner_block ) {
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$query_param = $inner_block->attributes['queryParam'];
|
||||
/**
|
||||
* There can be multiple attribute filters so we transform
|
||||
* the query param of each filter into an array to merge
|
||||
* them together.
|
||||
*/
|
||||
if ( ! empty( $query_param['calculate_attribute_counts'] ) ) {
|
||||
$query_param['calculate_attribute_counts'] = array( $query_param['calculate_attribute_counts'] );
|
||||
}
|
||||
$results = array_merge_recursive( $results, $query_param );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted products params for ProductCollectionData route from the
|
||||
* query context.
|
||||
*
|
||||
* @param array $query The query context.
|
||||
* @return array
|
||||
*/
|
||||
private function get_formatted_products_params( $query ) {
|
||||
$params = array();
|
||||
|
||||
if ( empty( $query['isProductCollectionBlock'] ) ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following params can be passed directly to Store API endpoints.
|
||||
*/
|
||||
$shared_params = array( 'exclude', 'offset', 'search' );
|
||||
|
||||
/**
|
||||
* The following params just need to transform the key, their value can
|
||||
* be passed as it is to the Store API.
|
||||
*/
|
||||
$mapped_params = array(
|
||||
'woocommerceStockStatus' => 'stock_status',
|
||||
'woocommerceOnSale' => 'on_sale',
|
||||
'woocommerceHandPickedProducts' => 'include',
|
||||
);
|
||||
|
||||
$taxonomy_mapper = function( $key ) {
|
||||
$mapping = array(
|
||||
'product_tag' => 'tag',
|
||||
'product_cat' => 'category',
|
||||
);
|
||||
|
||||
return $mapping[ $key ] ?? '_unstable_tax_' . $key;
|
||||
};
|
||||
|
||||
array_walk(
|
||||
$query,
|
||||
function( $value, $key ) use ( $shared_params, $mapped_params, $taxonomy_mapper, &$params ) {
|
||||
if ( in_array( $key, $shared_params, true ) ) {
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
|
||||
if ( in_array( $key, array_keys( $mapped_params ), true ) ) {
|
||||
$params[ $mapped_params[ $key ] ] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of taxQuery and woocommerceAttributes need additional
|
||||
* transformation to the shape that Store API accepts.
|
||||
*/
|
||||
if ( 'taxQuery' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $terms, $taxonomy ) use ( $taxonomy_mapper, &$params ) {
|
||||
$params[ $taxonomy_mapper( $taxonomy ) ] = implode( ',', $terms );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'woocommerceAttributes' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $attribute ) use ( &$params ) {
|
||||
$params['attributes'][] = array(
|
||||
'attribute' => $attribute['taxonomy'],
|
||||
'term_id' => $attribute['termId'],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product Collection determines the product visibility based on stock
|
||||
* statuses. We need to pass the catalog_visibility param to the Store
|
||||
* API to make sure the product visibility is correct.
|
||||
*/
|
||||
$params['catalog_visibility'] = is_search() ? 'search' : 'visible';
|
||||
|
||||
/**
|
||||
* `false` values got removed from `add_query_arg`, so we need to convert
|
||||
* them to numeric.
|
||||
*/
|
||||
return array_map(
|
||||
function( $param ) {
|
||||
return is_bool( $param ) ? +$param : $param;
|
||||
},
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* FilledCartBlock class.
|
||||
* FilterWrapper class.
|
||||
*/
|
||||
class FilterWrapper extends AbstractBlock {
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\Utils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
|
||||
|
||||
/**
|
||||
* Mini-Cart class.
|
||||
@@ -17,6 +18,8 @@ use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
|
||||
* @internal
|
||||
*/
|
||||
class MiniCart extends AbstractBlock {
|
||||
use BlockHooksTrait;
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
@@ -52,6 +55,19 @@ class MiniCart extends AbstractBlock {
|
||||
*/
|
||||
protected $display_cart_prices_including_tax = false;
|
||||
|
||||
/**
|
||||
* Block Hook API placements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hooked_block_placements = array(
|
||||
array(
|
||||
'position' => 'after',
|
||||
'anchor' => 'core/navigation',
|
||||
'area' => 'header',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -72,8 +88,8 @@ class MiniCart extends AbstractBlock {
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'wp_loaded', array( $this, 'register_empty_cart_message_block_pattern' ) );
|
||||
add_action( 'hooked_block_types', array( $this, 'register_auto_insert' ), 10, 4 );
|
||||
add_action( 'wp_print_footer_scripts', array( $this, 'print_lazy_load_scripts' ), 2 );
|
||||
add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,111 +594,6 @@ class MiniCart extends AbstractBlock {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `hooked_block_types` to auto-inject the mini-cart block into headers after navigation.
|
||||
*
|
||||
* @param array $hooked_blocks An array of block slugs hooked into a given context.
|
||||
* @param string $position Position of the block insertion point.
|
||||
* @param string $anchor_block The block acting as the anchor for the inserted block.
|
||||
* @param \WP_Block_Template|array $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return array An array of block slugs hooked into a given context.
|
||||
*/
|
||||
public function register_auto_insert( $hooked_blocks, $position, $anchor_block, $context ) {
|
||||
// Cache for active theme.
|
||||
static $active_theme_name = null;
|
||||
if ( is_null( $active_theme_name ) ) {
|
||||
$active_theme_name = wp_get_theme()->get( 'Name' );
|
||||
}
|
||||
/**
|
||||
* A list of pattern slugs to exclude from auto-insert (useful when
|
||||
* there are patterns that have a very specific location for the block)
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$pattern_exclude_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_pattern_exclude_list', array( 'twentytwentytwo/header-centered-logo', 'twentytwentytwo/header-stacked' ) );
|
||||
|
||||
/**
|
||||
* A list of theme slugs to execute this with. This is a temporary
|
||||
* measure until improvements to the Block Hooks API allow for exposing
|
||||
* to all block themes.
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$theme_include_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet' ) );
|
||||
|
||||
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
|
||||
if (
|
||||
'after' === $position &&
|
||||
'core/navigation' === $anchor_block &&
|
||||
$this->is_header_part_or_pattern( $context ) &&
|
||||
! $this->pattern_is_excluded( $context, $pattern_exclude_list ) &&
|
||||
! $this->has_mini_cart_block( $context )
|
||||
) {
|
||||
$hooked_blocks[] = 'woocommerce/' . $this->block_name;
|
||||
}
|
||||
}
|
||||
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the pattern is excluded or not
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param array $pattern_exclude_list List of pattern slugs to exclude.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function pattern_is_excluded( $context, $pattern_exclude_list ) {
|
||||
$pattern_slug = is_array( $context ) && isset( $context['slug'] ) ? $context['slug'] : '';
|
||||
if ( ! $pattern_slug ) {
|
||||
/**
|
||||
* Woo patterns have a slug property in $context, but core/theme patterns dont.
|
||||
* In that case, we fallback to the name property, as they're the same.
|
||||
*/
|
||||
$pattern_slug = is_array( $context ) && isset( $context['name'] ) ? $context['name'] : '';
|
||||
}
|
||||
return in_array( $pattern_slug, $pattern_exclude_list, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided context contains a mini-cart block.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_mini_cart_block( $context ) {
|
||||
/**
|
||||
* Note: this won't work for parsing WP_Block_Template instance until it's fixed in core
|
||||
* because $context->content is set as the result of `traverse_and_serialize_blocks` so
|
||||
* the filter callback doesn't get the original content.
|
||||
*
|
||||
* @see https://core.trac.wordpress.org/ticket/59882
|
||||
*/
|
||||
$content = is_array( $context ) && isset( $context['content'] ) ? $context['content'] : '';
|
||||
$content = '' === $content && $context instanceof \WP_Block_Template ? $context->content : $content;
|
||||
return strpos( $content, 'wp:woocommerce/mini-cart' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a provided context, returns whether the context refers to header content.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_header_part_or_pattern( $context ) {
|
||||
$is_header_pattern = is_array( $context ) &&
|
||||
(
|
||||
( isset( $context['blockTypes'] ) && in_array( 'core/template-part/header', $context['blockTypes'], true ) ) ||
|
||||
( isset( $context['categories'] ) && in_array( 'header', $context['categories'], true ) )
|
||||
);
|
||||
$is_header_part = $context instanceof \WP_Block_Template && 'header' === $context->area;
|
||||
return ( $is_header_pattern || $is_header_part );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the Mini-Cart should be rendered or not.
|
||||
*
|
||||
|
||||
@@ -298,5 +298,40 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
|
||||
)
|
||||
);
|
||||
|
||||
register_block_pattern(
|
||||
'woocommerce/order-confirmation-additional-fields-heading',
|
||||
array(
|
||||
'title' => '',
|
||||
'inserter' => false,
|
||||
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Additional information', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom fields for the order.
|
||||
*
|
||||
* @param array $fields List of additional fields with values.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_fields( $fields ) {
|
||||
if ( empty( $fields ) ) {
|
||||
return '';
|
||||
}
|
||||
return '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom field row.
|
||||
*
|
||||
* @param array $field An additional field and value.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_field( $field ) {
|
||||
return sprintf(
|
||||
'<dt>%1$s</dt><dd>%2$s</dd>',
|
||||
esc_html( $field['label'] ),
|
||||
esc_html( $field['value'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* AdditionalFields class.
|
||||
*/
|
||||
class AdditionalFields extends AbstractOrderConfirmationBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'order-confirmation-additional-fields';
|
||||
|
||||
/**
|
||||
* This renders the content of the block within the wrapper.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string|false $permission If the current user can view the order details or not.
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Original block content.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
|
||||
if ( ! $permission ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$content .= $this->render_additional_fields(
|
||||
array_merge(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
|
||||
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
|
||||
)
|
||||
);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* AdditionalFieldsWrapper class.
|
||||
*/
|
||||
class AdditionalFieldsWrapper extends AbstractOrderConfirmationBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'order-confirmation-additional-fields-wrapper';
|
||||
|
||||
/**
|
||||
* This renders the content of the downloads wrapper.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string|false $permission If the current user can view the order details or not.
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Original block content.
|
||||
*/
|
||||
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
|
||||
if ( ! $permission ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Contact and additional fields are currently grouped in this section.
|
||||
$additional_fields = array_merge(
|
||||
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ),
|
||||
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' )
|
||||
);
|
||||
|
||||
return empty( $additional_fields ) ? '' : $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' ) );
|
||||
$this->asset_data_registry->add( 'additionalContactFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ) );
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* BillingAddress class.
|
||||
*/
|
||||
@@ -31,6 +34,23 @@ class BillingAddress extends AbstractOrderConfirmationBlock {
|
||||
$address = '<address>' . wp_kses_post( $order->get_formatted_billing_address() ) . '</address>';
|
||||
$phone = $order->get_billing_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_billing_phone() ) . '</p>' : '';
|
||||
|
||||
return $address . $phone;
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$custom = $this->render_additional_fields(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'address', 'billing', 'view' )
|
||||
);
|
||||
|
||||
return $address . $phone . $custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalAddressFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'address' ), true );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* ShippingAddress class.
|
||||
*/
|
||||
@@ -31,6 +34,23 @@ class ShippingAddress extends AbstractOrderConfirmationBlock {
|
||||
$address = '<address>' . wp_kses_post( $order->get_formatted_shipping_address() ) . '</address>';
|
||||
$phone = $order->get_shipping_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_shipping_phone() ) . '</p>' : '';
|
||||
|
||||
return $address . $phone;
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$custom = $this->render_additional_fields(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'address', 'shipping', 'view' )
|
||||
);
|
||||
|
||||
return $address . $phone . $custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalAddressFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'address' ), true );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ class ProductButton extends AbstractBlock {
|
||||
'wp_enqueue_scripts',
|
||||
array( $this, 'dequeue_add_to_cart_scripts' )
|
||||
);
|
||||
} else {
|
||||
$this->dequeue_add_to_cart_scripts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +214,7 @@ class ProductButton extends AbstractBlock {
|
||||
'{custom_classes}' => esc_attr( $classname . ' ' . $custom_width_classes . ' ' . $custom_align_classes ),
|
||||
'{html_element}' => $html_element,
|
||||
'{add_to_cart_url}' => esc_url( $product->add_to_cart_url() ),
|
||||
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
|
||||
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] . ' wc-interactive' ) : 'wc-interactive',
|
||||
'{button_styles}' => esc_attr( $styles_and_classes['styles'] ),
|
||||
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
|
||||
'{add_to_cart_text}' => esc_html( $initial_product_text ),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use WP_Query;
|
||||
use WC_Tax;
|
||||
|
||||
/**
|
||||
* ProductCollection class.
|
||||
@@ -95,6 +96,8 @@ class ProductCollection extends AbstractBlock {
|
||||
// Interactivity API: Add navigation directives to the product collection block.
|
||||
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'add_navigation_id_directive' ), 10, 3 );
|
||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +184,7 @@ class ProductCollection extends AbstractBlock {
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
protected function enqueue_data( array $attributes = array() ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
|
||||
@@ -209,6 +212,7 @@ class ProductCollection extends AbstractBlock {
|
||||
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
|
||||
$featured = $request->get_param( 'featured' );
|
||||
$time_frame = $request->get_param( 'timeFrame' );
|
||||
$price_range = $request->get_param( 'priceRange' );
|
||||
// This argument is required for the tests to PHP Unit Tests to run correctly.
|
||||
// Most likely this argument is being accessed in the test environment image.
|
||||
$args['author'] = '';
|
||||
@@ -223,6 +227,7 @@ class ProductCollection extends AbstractBlock {
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $featured,
|
||||
'timeFrame' => $time_frame,
|
||||
'priceRange' => $price_range,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -306,10 +311,11 @@ class ProductCollection extends AbstractBlock {
|
||||
);
|
||||
|
||||
$is_on_sale = $query['woocommerceOnSale'] ?? false;
|
||||
$product_attributes = $query['woocommerceAttributes'] ?? [];
|
||||
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
|
||||
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? [];
|
||||
$product_attributes = $query['woocommerceAttributes'] ?? array();
|
||||
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? array() );
|
||||
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? array();
|
||||
$time_frame = $query['timeFrame'] ?? null;
|
||||
$price_range = $query['priceRange'] ?? null;
|
||||
|
||||
$final_query = $this->get_final_query_args(
|
||||
$common_query_values,
|
||||
@@ -322,6 +328,7 @@ class ProductCollection extends AbstractBlock {
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $query['featured'] ?? false,
|
||||
'timeFrame' => $time_frame,
|
||||
'priceRange' => $price_range,
|
||||
),
|
||||
$is_exclude_applied_filters
|
||||
);
|
||||
@@ -337,23 +344,26 @@ class ProductCollection extends AbstractBlock {
|
||||
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
|
||||
*/
|
||||
private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
|
||||
$handpicked_products = $query['handpicked_products'] ?? [];
|
||||
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : [];
|
||||
$handpicked_products = $query['handpicked_products'] ?? array();
|
||||
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
|
||||
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
|
||||
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
|
||||
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : [];
|
||||
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : array();
|
||||
$featured_query = $this->get_featured_query( $query['featured'] ?? false );
|
||||
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
|
||||
$taxonomies_query = $query['taxonomies_query'] ?? [];
|
||||
$taxonomies_query = $query['taxonomies_query'] ?? array();
|
||||
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
|
||||
$date_query = $this->get_date_query( $query['timeFrame'] ?? [] );
|
||||
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
|
||||
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
|
||||
|
||||
// We exclude applied filters to generate product ids for the filter blocks.
|
||||
$applied_filters_query = $is_exclude_applied_filters ? [] : $this->get_queries_by_applied_filters();
|
||||
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
|
||||
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query );
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query, $price_query_args );
|
||||
|
||||
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||
$result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +391,7 @@ class ProductCollection extends AbstractBlock {
|
||||
private function merge_queries( ...$queries ) {
|
||||
$merged_query = array_reduce(
|
||||
$queries,
|
||||
function( $acc, $query ) {
|
||||
function ( $acc, $query ) {
|
||||
if ( ! is_array( $query ) ) {
|
||||
return $acc;
|
||||
}
|
||||
@@ -489,6 +499,8 @@ class ProductCollection extends AbstractBlock {
|
||||
'posts_per_page',
|
||||
'suppress_filters',
|
||||
'tax_query',
|
||||
'isProductCollection',
|
||||
'priceRange',
|
||||
)
|
||||
);
|
||||
|
||||
@@ -547,15 +559,13 @@ class ProductCollection extends AbstractBlock {
|
||||
foreach ( $new as $key => $value ) {
|
||||
if ( is_numeric( $key ) ) {
|
||||
$base[] = $value;
|
||||
} else {
|
||||
if ( is_array( $value ) ) {
|
||||
if ( ! isset( $base[ $key ] ) ) {
|
||||
$base[ $key ] = array();
|
||||
}
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
} else {
|
||||
$base[ $key ] = $value;
|
||||
} elseif ( is_array( $value ) ) {
|
||||
if ( ! isset( $base[ $key ] ) ) {
|
||||
$base[ $key ] = array();
|
||||
}
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
} else {
|
||||
$base[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,14 +685,14 @@ class ProductCollection extends AbstractBlock {
|
||||
* @return array
|
||||
*/
|
||||
private function merge_tax_queries( ...$queries ) {
|
||||
$tax_query = [];
|
||||
$tax_query = array();
|
||||
foreach ( $queries as $query ) {
|
||||
if ( ! empty( $query['tax_query'] ) ) {
|
||||
$tax_query = array_merge( $tax_query, $query['tax_query'] );
|
||||
}
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
return [ 'tax_query' => $tax_query ];
|
||||
return array( 'tax_query' => $tax_query );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -748,23 +758,23 @@ class ProductCollection extends AbstractBlock {
|
||||
*/
|
||||
private function get_filter_by_taxonomies_query( $tax_query ): array {
|
||||
if ( ! is_array( $tax_query ) ) {
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of taxonomy names associated with the "product" post type because
|
||||
* we also want to include custom taxonomies associated with the "product" post type.
|
||||
*/
|
||||
$product_taxonomies = get_taxonomies( [ 'object_type' => [ 'product' ] ], 'names' );
|
||||
$product_taxonomies = get_taxonomies( array( 'object_type' => array( 'product' ) ), 'names' );
|
||||
$result = array_filter(
|
||||
$tax_query,
|
||||
function( $item ) use ( $product_taxonomies ) {
|
||||
function ( $item ) use ( $product_taxonomies ) {
|
||||
return isset( $item['taxonomy'] ) && in_array( $item['taxonomy'], $product_taxonomies, true );
|
||||
}
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery
|
||||
return ! empty( $result ) ? [ 'tax_query' => $result ] : [];
|
||||
return ! empty( $result ) ? array( 'tax_query' => $result ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -807,19 +817,19 @@ class ProductCollection extends AbstractBlock {
|
||||
$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() : [
|
||||
$max_price_query = empty( $max_price ) ? array() : array(
|
||||
'key' => '_price',
|
||||
'value' => $max_price,
|
||||
'compare' => '<',
|
||||
'type' => 'numeric',
|
||||
];
|
||||
);
|
||||
|
||||
$min_price_query = empty( $min_price ) ? array() : [
|
||||
$min_price_query = empty( $min_price ) ? array() : array(
|
||||
'key' => '_price',
|
||||
'value' => $min_price,
|
||||
'compare' => '>=',
|
||||
'type' => 'numeric',
|
||||
];
|
||||
);
|
||||
|
||||
if ( empty( $min_price_query ) && empty( $max_price_query ) ) {
|
||||
return array();
|
||||
@@ -847,7 +857,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$queries = array_reduce(
|
||||
$attributes_filter_query_args,
|
||||
function( $acc, $query_args ) {
|
||||
function ( $acc, $query_args ) {
|
||||
$attribute_name = $query_args['filter'];
|
||||
$attribute_query_type = $query_args['query_type'];
|
||||
|
||||
@@ -912,7 +922,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$this->attributes_filter_query_args = array_reduce(
|
||||
wc_get_attribute_taxonomies(),
|
||||
function( $acc, $attribute ) {
|
||||
function ( $acc, $attribute ) {
|
||||
$acc[ $attribute->attribute_name ] = array(
|
||||
'filter' => AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute->attribute_name,
|
||||
'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute->attribute_name,
|
||||
@@ -939,7 +949,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$filtered_stock_status_values = array_filter(
|
||||
explode( ',', $filter_stock_status_values ),
|
||||
function( $stock_status ) {
|
||||
function ( $stock_status ) {
|
||||
return in_array( $stock_status, StockFilter::get_stock_status_query_var_values(), true );
|
||||
}
|
||||
);
|
||||
@@ -980,7 +990,7 @@ class ProductCollection extends AbstractBlock {
|
||||
}
|
||||
|
||||
$rating_terms = array_map(
|
||||
function( $rating ) use ( $product_visibility_terms ) {
|
||||
function ( $rating ) use ( $product_visibility_terms ) {
|
||||
return $product_visibility_terms[ 'rated-' . $rating ];
|
||||
},
|
||||
$parsed_filter_rating_values
|
||||
@@ -1011,7 +1021,7 @@ class ProductCollection extends AbstractBlock {
|
||||
* }
|
||||
* @return array Date query array; empty if parameters are invalid.
|
||||
*/
|
||||
private function get_date_query( array $time_frame ) : array {
|
||||
private function get_date_query( array $time_frame ): array {
|
||||
// Validate time_frame elements.
|
||||
if ( empty( $time_frame['operator'] ) || empty( $time_frame['value'] ) ) {
|
||||
return array();
|
||||
@@ -1032,5 +1042,184 @@ class ProductCollection extends AbstractBlock {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query arguments for price range filter.
|
||||
* We are adding these extra query arguments to be used in `posts_clauses`
|
||||
* because there are 2 special edge cases we wanna handle for Price range filter:
|
||||
* Case 1: Prices excluding tax are displayed including tax
|
||||
* Case 2: Prices including tax are displayed excluding tax
|
||||
*
|
||||
* Both of these cases require us to modify SQL query to get the correct results.
|
||||
*
|
||||
* See add_price_range_filter_posts_clauses function in this file for more details.
|
||||
*
|
||||
* @param array $price_range Price range with min and max values.
|
||||
* @return array Query arguments.
|
||||
*/
|
||||
public function get_price_range_query_args( $price_range ) {
|
||||
if ( empty( $price_range ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'isProductCollection' => true,
|
||||
'priceRange' => $price_range,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the `posts_clauses` filter to the main query.
|
||||
*
|
||||
* @param array $clauses The query clauses.
|
||||
* @param WP_Query $query The WP_Query instance.
|
||||
*/
|
||||
public function add_price_range_filter_posts_clauses( $clauses, $query ) {
|
||||
$query_vars = $query->query_vars;
|
||||
$is_product_collection_block = $query_vars['isProductCollection'] ?? false;
|
||||
if ( ! $is_product_collection_block ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$price_range = $query_vars['priceRange'] ?? null;
|
||||
if ( empty( $price_range ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$adjust_for_taxes = $this->should_adjust_price_range_for_taxes();
|
||||
$clauses['join'] = $this->append_product_sorting_table_join( $clauses['join'] );
|
||||
|
||||
$min_price = $price_range['min'] ?? null;
|
||||
if ( $min_price ) {
|
||||
if ( $adjust_for_taxes ) {
|
||||
$clauses['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price, 'min_price', '>=' );
|
||||
} else {
|
||||
$clauses['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', $min_price );
|
||||
}
|
||||
}
|
||||
|
||||
$max_price = $price_range['max'] ?? null;
|
||||
if ( $max_price ) {
|
||||
if ( $adjust_for_taxes ) {
|
||||
$clauses['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price, 'max_price', '<=' );
|
||||
} else {
|
||||
$clauses['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', $max_price );
|
||||
}
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if price filters need adjustment based on the tax display settings.
|
||||
*
|
||||
* This function checks if there's a discrepancy between how prices are stored in the database
|
||||
* and how they are displayed to the user, specifically with respect to tax inclusion or exclusion.
|
||||
* It returns true if an adjustment is needed, indicating that the price filters should account for this
|
||||
* discrepancy to display accurate prices.
|
||||
*
|
||||
* @return bool True if the price filters need to be adjusted for tax display settings, false otherwise.
|
||||
*/
|
||||
private function should_adjust_price_range_for_taxes() {
|
||||
$display_setting = get_option( 'woocommerce_tax_display_shop' ); // Tax display setting ('incl' or 'excl').
|
||||
$price_storage_method = wc_prices_include_tax() ? 'incl' : 'excl';
|
||||
|
||||
return $display_setting !== $price_storage_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wc_product_meta_lookup to posts if not already joined.
|
||||
*
|
||||
* @param string $sql SQL join.
|
||||
* @return string
|
||||
*/
|
||||
protected function append_product_sorting_table_join( $sql ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
|
||||
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query for price filters when dealing with displayed taxes.
|
||||
*
|
||||
* @param float $price_filter Price filter to apply.
|
||||
* @param string $column Price being filtered (min or max).
|
||||
* @param string $operator Comparison operator for column.
|
||||
* @return string Constructed query.
|
||||
*/
|
||||
protected function get_price_filter_query_for_displayed_taxes( $price_filter, $column = 'min_price', $operator = '>=' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Select only used tax classes to avoid unwanted calculations.
|
||||
$product_tax_classes = $wpdb->get_col( "SELECT DISTINCT tax_class FROM {$wpdb->wc_product_meta_lookup};" );
|
||||
|
||||
if ( empty( $product_tax_classes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$or_queries = array();
|
||||
|
||||
// We need to adjust the filter for each possible tax class and combine the queries into one.
|
||||
foreach ( $product_tax_classes as $tax_class ) {
|
||||
$adjusted_price_filter = $this->adjust_price_filter_for_tax_class( $price_filter, $tax_class );
|
||||
$or_queries[] = $wpdb->prepare(
|
||||
'( wc_product_meta_lookup.tax_class = %s AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )',
|
||||
$tax_class,
|
||||
$adjusted_price_filter
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $wpdb->prepare(
|
||||
' AND (
|
||||
wc_product_meta_lookup.tax_status = "taxable" AND ( 0=1 OR ' . implode( ' OR ', $or_queries ) . ')
|
||||
OR ( wc_product_meta_lookup.tax_status != "taxable" AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )
|
||||
) ',
|
||||
$price_filter
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a price filter based on a tax class and whether or not the amount includes or excludes taxes.
|
||||
*
|
||||
* This calculation logic is based on `wc_get_price_excluding_tax` and `wc_get_price_including_tax` in core.
|
||||
*
|
||||
* @param float $price_filter Price filter amount as entered.
|
||||
* @param string $tax_class Tax class for adjustment.
|
||||
* @return float
|
||||
*/
|
||||
protected function adjust_price_filter_for_tax_class( $price_filter, $tax_class ) {
|
||||
$tax_display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$tax_rates = WC_Tax::get_rates( $tax_class );
|
||||
$base_tax_rates = WC_Tax::get_base_tax_rates( $tax_class );
|
||||
|
||||
// If prices are shown incl. tax, we want to remove the taxes from the filter amount to match prices stored excl. tax.
|
||||
if ( 'incl' === $tax_display ) {
|
||||
/**
|
||||
* Filters if taxes should be removed from locations outside the store base location.
|
||||
*
|
||||
* The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing
|
||||
* with out of base locations. e.g. If a product costs 10 including tax, all users will pay 10
|
||||
* regardless of location and taxes.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*
|
||||
* @internal Matches filter name in WooCommerce core.
|
||||
*
|
||||
* @param boolean $adjust_non_base_location_prices True by default.
|
||||
* @return boolean
|
||||
*/
|
||||
$taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $price_filter, $base_tax_rates, true ) : WC_Tax::calc_tax( $price_filter, $tax_rates, true );
|
||||
return $price_filter - array_sum( $taxes );
|
||||
}
|
||||
|
||||
// If prices are shown excl. tax, add taxes to match the prices stored in the DB.
|
||||
$taxes = WC_Tax::calc_tax( $price_filter, $tax_rates, false );
|
||||
|
||||
return $price_filter + array_sum( $taxes );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,25 @@ class ProductDetails extends AbstractBlock {
|
||||
* It isn't necessary register block assets because it is a server side block.
|
||||
*/
|
||||
protected function register_block_type_assets() {
|
||||
|
||||
// Register block styles.
|
||||
register_block_style(
|
||||
'woocommerce/product-details',
|
||||
array(
|
||||
'name' => 'classic',
|
||||
'label' => __( 'Classic', 'woocommerce' ),
|
||||
'is_default' => true,
|
||||
)
|
||||
);
|
||||
|
||||
register_block_style(
|
||||
'woocommerce/product-details',
|
||||
array(
|
||||
'name' => 'minimal',
|
||||
'label' => __( 'Minimal', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* Product Filter Block.
|
||||
*/
|
||||
final class ProductFilter extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'product-filter';
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $current_response = null;
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @see $this->register_block_type()
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
* - Hook into WP lifecycle.
|
||||
* - Register the block with WordPress.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
global $pagenow;
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
|
||||
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy(), true );
|
||||
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow, true );
|
||||
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the collection data is empty.
|
||||
*
|
||||
* @param mixed $attributes - Block attributes.
|
||||
* @return bool - Whether the collection data is empty.
|
||||
*/
|
||||
private function collection_data_is_empty( $attributes ) {
|
||||
$filter_type = $attributes['filterType'];
|
||||
|
||||
if ( 'active-filters' !== $filter_type && empty( $this->current_response ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( 'attribute-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['attribute_counts'] );
|
||||
}
|
||||
|
||||
if ( 'rating-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['rating_counts'] );
|
||||
}
|
||||
|
||||
if ( 'price-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['price_range'] ) || ( $this->current_response['price_range']['min_price'] === $this->current_response['price_range']['max_price'] );
|
||||
}
|
||||
|
||||
if ( 'stock-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['stock_status_counts'] );
|
||||
}
|
||||
|
||||
if ( 'active-filters' === $filter_type ) {
|
||||
// Duplicate query param logic from ProductFilterActive block, to determine if we should
|
||||
// display the ProductFilter block or not.
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$parsed_url = wp_parse_url( esc_url_raw( $request_uri ) );
|
||||
|
||||
$url_query_params = [];
|
||||
|
||||
if ( isset( $parsed_url['query'] ) ) {
|
||||
parse_str( $parsed_url['query'], $url_query_params );
|
||||
}
|
||||
|
||||
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
return empty( array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if ( is_admin() ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ( $this->collection_data_is_empty( $attributes ) ) {
|
||||
return $this->render_empty_block( $block );
|
||||
}
|
||||
|
||||
return $this->render_filter_block( $content, $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current response, must be done before rendering.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function reset_current_response() {
|
||||
/**
|
||||
* When WP starts rendering the Product Filters block,
|
||||
* we can safely unset the current response.
|
||||
*/
|
||||
$this->current_response = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's empty.
|
||||
*
|
||||
* @param mixed $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_empty_block( $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s></nav>',
|
||||
get_block_wrapper_attributes(
|
||||
$attributes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique navigation ID for the block.
|
||||
*
|
||||
* @param mixed $block - Block instance.
|
||||
* @return string - Unique navigation ID.
|
||||
*/
|
||||
private function generate_navigation_id( $block ) {
|
||||
return sprintf(
|
||||
'wc-product-filter-%s',
|
||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's not empty.
|
||||
*
|
||||
* @param string $content - Block content.
|
||||
* @param WP_Block $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_filter_block( $content, $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the context of inner blocks.
|
||||
*
|
||||
* @param array $context The block context.
|
||||
* @param array $parsed_block The parsed block.
|
||||
* @param WP_Block $parent_block The parent block.
|
||||
* @return array
|
||||
*/
|
||||
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
|
||||
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first direct child of Product Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
if ( empty( $this->current_response ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated collection data from the API.
|
||||
* Loop through inner blocks and build a query string to pass to the API.
|
||||
*
|
||||
* @param WP_Block $block The block instance.
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'min_price' => null,
|
||||
'max_price' => null,
|
||||
'attribute_counts' => null,
|
||||
'stock_status_counts' => null,
|
||||
'rating_counts' => null,
|
||||
);
|
||||
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
|
||||
if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
|
||||
$query_vars = build_query_vars_from_query_block( $block, 1 );
|
||||
} else {
|
||||
global $wp_query;
|
||||
$query_vars = array_filter( $wp_query->query_vars );
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_price_range'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['min_price'], $filter_query_vars['max_price'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_price' );
|
||||
}
|
||||
|
||||
$price_results = $filters->get_filtered_price( $filter_query_vars );
|
||||
$data['price_range'] = array(
|
||||
'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
|
||||
'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_stock_status_counts'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['filter_stock_status'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_stock_status' );
|
||||
}
|
||||
|
||||
$counts = $filters->get_stock_status_counts( $filter_query_vars );
|
||||
|
||||
$data['stock_status_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['stock_status_counts'][] = array(
|
||||
'status' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_rating_counts'] ) ) {
|
||||
// Regenerate the products query vars without rating filter.
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'rating_filter', true );
|
||||
}
|
||||
|
||||
$counts = $filters->get_rating_counts( $filter_query_vars );
|
||||
$data['rating_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['rating_counts'][] = array(
|
||||
'rating' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_attribute_counts'] ) ) {
|
||||
foreach ( $collection_data_params['calculate_attribute_counts'] as $attributes_to_count ) {
|
||||
if ( ! isset( $attributes_to_count['taxonomy'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( 'and' !== strtolower( $attributes_to_count['queryType'] ) ) {
|
||||
unset( $filter_query_vars[ 'filter_' . str_replace( 'pa_', '', $attributes_to_count['taxonomy'] ) ] );
|
||||
}
|
||||
|
||||
unset(
|
||||
$filter_query_vars['taxonomy'],
|
||||
$filter_query_vars['term']
|
||||
);
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'taxonomy', $attributes_to_count['taxonomy'] );
|
||||
}
|
||||
|
||||
$counts = $filters->get_attribute_counts( $filter_query_vars, $attributes_to_count['taxonomy'] );
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['attribute_counts'][] = array(
|
||||
'term' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query array from tax or meta query by searching for arrays that
|
||||
* contain exact key => value pair.
|
||||
*
|
||||
* @param array $queries tax_query or meta_query.
|
||||
* @param string $key Array key to search for.
|
||||
* @param mixed $value Value to compare with search result.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function remove_query_array( $queries, $key, $value ) {
|
||||
if ( empty( $queries ) ) {
|
||||
return $queries;
|
||||
}
|
||||
|
||||
foreach ( $queries as $query_key => $query ) {
|
||||
if ( isset( $query[ $key ] ) && $query[ $key ] === $value ) {
|
||||
unset( $queries[ $query_key ] );
|
||||
}
|
||||
|
||||
if ( isset( $query['relation'] ) ) {
|
||||
$queries[ $query_key ] = $this->remove_query_array( $query, $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inner blocks recursively.
|
||||
*
|
||||
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
|
||||
* @param array $results The results array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_collection_data_params( $inner_blocks, &$results = array() ) {
|
||||
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
|
||||
foreach ( $inner_blocks as $inner_block ) {
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$query_param = $inner_block->attributes['queryParam'];
|
||||
/**
|
||||
* There can be multiple attribute filters so we transform
|
||||
* the query param of each filter into an array to merge
|
||||
* them together.
|
||||
*/
|
||||
if ( ! empty( $query_param['calculate_attribute_counts'] ) ) {
|
||||
$query_param['calculate_attribute_counts'] = array( $query_param['calculate_attribute_counts'] );
|
||||
}
|
||||
$results = array_merge_recursive( $results, $query_param );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CollectionAttributeFilter class.
|
||||
* Product Filter: Active Block.
|
||||
*/
|
||||
final class CollectionActiveFilters extends AbstractBlock {
|
||||
final class ProductFilterActive extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-active-filters';
|
||||
protected $block_name = 'product-filter-active';
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
@@ -48,10 +48,6 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
*/
|
||||
$active_filters = apply_filters( 'collection_active_filters_data', array(), $this->get_filter_query_params( $query_id ) );
|
||||
|
||||
if ( empty( $active_filters ) ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'queryId' => $query_id,
|
||||
'params' => array_keys( $this->get_filter_query_params( $query_id ) ),
|
||||
@@ -60,7 +56,7 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => 'wc-block-active-filters',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-active-filters' ) ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
)
|
||||
);
|
||||
@@ -68,22 +64,23 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div <?php echo $wrapper_attributes; ?>>
|
||||
<ul class="wc-block-active-filters__list %3$s">
|
||||
<?php foreach ( $active_filters as $filter ) : ?>
|
||||
<li>
|
||||
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<ul>
|
||||
<?php $this->render_items( $filter['items'], $attributes['displayStyle'] ); ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<button class="wc-block-active-filters__clear-all" data-wc-on--click="actions.clearAll">
|
||||
<span aria-hidden="true"><?php echo esc_html__( 'Clear All', 'woocommerce' ); ?></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Clear All Filters', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<?php if ( ! empty( $active_filters ) ) : ?>
|
||||
<ul class="wc-block-active-filters__list %3$s">
|
||||
<?php foreach ( $active_filters as $filter ) : ?>
|
||||
<li>
|
||||
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<ul>
|
||||
<?php $this->render_items( $filter['items'], $attributes['displayStyle'] ); ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<button class="wc-block-active-filters__clear-all" data-wc-on--click="actions.clearAll">
|
||||
<span aria-hidden="true"><?php echo esc_html__( 'Clear All', 'woocommerce' ); ?></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Clear All Filters', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
@@ -2,18 +2,19 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
|
||||
/**
|
||||
* CollectionAttributeFilter class.
|
||||
* Product Filter: Attribute Block.
|
||||
*/
|
||||
final class CollectionAttributeFilter extends AbstractBlock {
|
||||
final class ProductFilterAttribute extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-attribute-filter';
|
||||
protected $block_name = 'product-filter-attribute';
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
@@ -85,18 +86,20 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
}
|
||||
);
|
||||
|
||||
$action_namespace = $this->get_full_block_name();
|
||||
|
||||
foreach ( $active_product_attributes as $product_attribute ) {
|
||||
$terms = explode( ',', get_query_var( "filter_{$product_attribute}" ) );
|
||||
|
||||
// Get attribute term by slug.
|
||||
$terms = array_map(
|
||||
function( $term ) use ( $product_attribute ) {
|
||||
function( $term ) use ( $product_attribute, $action_namespace ) {
|
||||
$term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" );
|
||||
return array(
|
||||
'title' => $term_object->name,
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-attribute-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-attribute-filter::' . wp_json_encode(
|
||||
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
|
||||
'data-wc-context' => "$action_namespace::" . wp_json_encode(
|
||||
array(
|
||||
'value' => $term,
|
||||
'attributeSlug' => $product_attribute,
|
||||
@@ -127,18 +130,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if (
|
||||
is_admin() ||
|
||||
empty( $block->context['collectionData']['attribute_counts'] ) ||
|
||||
empty( $attributes['attributeId'] )
|
||||
) {
|
||||
return $content;
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
||||
|
||||
$attribute_counts = array_reduce(
|
||||
$block->context['collectionData']['attribute_counts'],
|
||||
$attribute_counts = array_reduce(
|
||||
$block->context['collectionData']['attribute_counts'] ?? [],
|
||||
function( $acc, $count ) {
|
||||
$acc[ $count['term'] ] = $count['count'];
|
||||
return $acc;
|
||||
@@ -146,6 +145,17 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
[]
|
||||
);
|
||||
|
||||
if ( empty( $attribute_counts ) ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$attribute_terms = get_terms(
|
||||
array(
|
||||
'taxonomy' => $product_attribute->slug,
|
||||
@@ -170,7 +180,16 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
$attribute_terms
|
||||
);
|
||||
|
||||
$filter_content = 'dropdown' === $attributes['displayStyle'] ? $this->render_attribute_dropdown( $attribute_options, $attributes ) : $this->render_attribute_list( $attribute_options, $attributes );
|
||||
$filtered_options = array_filter(
|
||||
$attribute_options,
|
||||
function( $option ) {
|
||||
return $option['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
$filter_content = 'dropdown' === $attributes['displayStyle'] ?
|
||||
$this->render_attribute_dropdown( $filtered_options, $attributes ) :
|
||||
$this->render_attribute_checkbox_list( $filtered_options, $attributes );
|
||||
|
||||
$context = array(
|
||||
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
||||
@@ -179,13 +198,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>%2$s</div>',
|
||||
'<div %1$s>%2$s%3$s</div>',
|
||||
get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-attribute-filter' ) ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
)
|
||||
),
|
||||
$content,
|
||||
$filter_content
|
||||
);
|
||||
}
|
||||
@@ -197,8 +217,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
* @param bool $attributes Block attributes.
|
||||
*/
|
||||
private function render_attribute_dropdown( $options, $attributes ) {
|
||||
$list_items = array();
|
||||
$selected_item = array();
|
||||
if ( empty( $options ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$list_items = array();
|
||||
$selected_items = array();
|
||||
|
||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
||||
|
||||
foreach ( $options as $option ) {
|
||||
$item = array(
|
||||
@@ -209,79 +235,53 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
$list_items[] = $item;
|
||||
|
||||
if ( $option['selected'] ) {
|
||||
$selected_item = $item;
|
||||
$selected_items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'woocommerce/collection-attribute-filter::actions.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
'items' => $list_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.navigate",
|
||||
'selected_items' => $selected_items,
|
||||
'select_type' => $attributes['selectType'] ?? 'multiple',
|
||||
// translators: %s is a product attribute name.
|
||||
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list.
|
||||
* Render the attribute filter checkbox list.
|
||||
*
|
||||
* @param array $options Data to render the list.
|
||||
* @param bool $attributes Block attributes.
|
||||
* @param mixed $options Attribute filter options to render in the checkbox list.
|
||||
* @param mixed $attributes Block attributes.
|
||||
* @return string
|
||||
*/
|
||||
private function render_attribute_list( $options, $attributes ) {
|
||||
$output = '<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">';
|
||||
foreach ( $options as $option ) {
|
||||
$output .= $this->render_list_item_template( $option, $attributes['showCounts'] );
|
||||
private function render_attribute_checkbox_list( $options, $attributes ) {
|
||||
if ( empty( $options ) ) {
|
||||
return '';
|
||||
}
|
||||
$output .= '</ul>';
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list item.
|
||||
*
|
||||
* @param array $option Data to render the list item.
|
||||
* @param bool $show_counts Whether to display the count.
|
||||
*/
|
||||
private function render_list_item_template( $option, $show_counts ) {
|
||||
$count_html = $show_counts ?
|
||||
sprintf(
|
||||
'<span class="wc-filter-element-label-list-count">
|
||||
<span aria-hidden="true">%1$s</span>
|
||||
<span class="screen-reader-text">%2$s</span>
|
||||
</span>',
|
||||
$option['count'],
|
||||
// translators: %d is the number of products.
|
||||
sprintf( _n( '%d product', '%d products', $option['count'], 'woocommerce' ), $option['count'] )
|
||||
) :
|
||||
'';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
$template = '<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="%1$s">
|
||||
<input
|
||||
id="%1$s"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
data-wc-context=\'{ "attributeTermSlug": "%5$s" }\'
|
||||
value="%5$s"
|
||||
%4$s
|
||||
/>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path></svg>
|
||||
<span class="wc-block-components-checkbox__label">%2$s %3$s</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>';
|
||||
$list_options = array_map(
|
||||
function( $option ) use ( $show_counts ) {
|
||||
return array(
|
||||
'id' => $option['slug'] . '-' . $option['term_id'],
|
||||
'checked' => $option['selected'],
|
||||
'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
|
||||
'value' => $option['slug'],
|
||||
);
|
||||
},
|
||||
$options
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
$template,
|
||||
esc_attr( $option['slug'] ) . '-' . $option['term_id'],
|
||||
esc_html( $option['name'] ),
|
||||
$count_html,
|
||||
checked( $option['selected'], true, false ),
|
||||
esc_attr( $option['slug'] )
|
||||
return CheckboxList::render(
|
||||
array(
|
||||
'items' => $list_options,
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CollectionPriceFilter class.
|
||||
* Product Filter: Price Block.
|
||||
*/
|
||||
final class CollectionPriceFilter extends AbstractBlock {
|
||||
final class ProductFilterPrice extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-price-filter';
|
||||
protected $block_name = 'product-filter-price';
|
||||
|
||||
const MIN_PRICE_QUERY_VAR = 'min_price';
|
||||
const MAX_PRICE_QUERY_VAR = 'max_price';
|
||||
@@ -93,7 +93,7 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
array(
|
||||
'title' => $title,
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-price-filter::actions.reset',
|
||||
'data-wc-on--click' => "{$this->get_full_block_name()}::actions.reset",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -111,19 +111,14 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if (
|
||||
is_admin() ||
|
||||
empty( $block->context['collectionData'] ) ||
|
||||
empty( $block->context['collectionData']['price_range'] )
|
||||
) {
|
||||
return $content;
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$price_range = $block->context['collectionData']['price_range'];
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
$min_range = $price_range['min_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$max_range = $price_range['max_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$price_range = $block->context['collectionData']['price_range'] ?? [];
|
||||
$min_range = $price_range['min_price'] ?? 0;
|
||||
$max_range = $price_range['max_price'] ?? 0;
|
||||
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
|
||||
$max_price = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
|
||||
$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
|
||||
@@ -136,19 +131,24 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
'maxRange' => $max_range,
|
||||
);
|
||||
|
||||
wc_initial_state(
|
||||
'woocommerce/collection-price-filter',
|
||||
$data
|
||||
);
|
||||
|
||||
list (
|
||||
'showInputFields' => $show_input_fields,
|
||||
'inlineInput' => $inline_input
|
||||
) = $attributes;
|
||||
|
||||
// Max range shouldn't be 0.
|
||||
if ( ! $max_range ) {
|
||||
return $content;
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $data ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $min_range === $max_range || ! $max_range ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
// CSS variables for the range bar style.
|
||||
@@ -156,23 +156,13 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
$__high = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
|
||||
$range_style = "--low: $__low%; --high: $__high%";
|
||||
|
||||
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-price-filter' ) );
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => $data_directive,
|
||||
)
|
||||
);
|
||||
|
||||
$price_min = $show_input_fields ?
|
||||
sprintf(
|
||||
'<input
|
||||
class="min"
|
||||
type="text"
|
||||
value="%d"
|
||||
data-wc-bind--value="state.minPrice"
|
||||
data-wc-on--input="actions.setMinPrice"
|
||||
data-wc-bind--value="context.minPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
/>',
|
||||
esc_attr( $min_price )
|
||||
@@ -188,8 +178,7 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
class="max"
|
||||
type="text"
|
||||
value="%d"
|
||||
data-wc-bind--value="state.maxPrice"
|
||||
data-wc-on--input="actions.setMaxPrice"
|
||||
data-wc-bind--value="context.maxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
/>',
|
||||
esc_attr( $max_price )
|
||||
@@ -202,41 +191,44 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
ob_start();
|
||||
?>
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<div
|
||||
class="range"
|
||||
style="<?php echo esc_attr( $range_style ); ?>"
|
||||
data-wc-bind--style="state.rangeStyle"
|
||||
>
|
||||
<div class="range-bar"></div>
|
||||
<input
|
||||
type="range"
|
||||
class="min"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $min_price ); ?>"
|
||||
data-wc-bind--max="state.maxRange"
|
||||
data-wc-bind--value="state.minPrice"
|
||||
data-wc-class--active="state.isMinActive"
|
||||
data-wc-on--input="actions.setMinPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div class="filter-controls">
|
||||
<div
|
||||
class="range"
|
||||
style="<?php echo esc_attr( $range_style ); ?>"
|
||||
data-wc-bind--style="state.rangeStyle"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
class="max"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $max_price ); ?>"
|
||||
data-wc-bind--max="state.maxRange"
|
||||
data-wc-bind--value="state.maxPrice"
|
||||
data-wc-class--active="state.isMaxActive"
|
||||
data-wc-on--input="actions.setMaxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
</div>
|
||||
<div class="text">
|
||||
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
|
||||
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div class="range-bar"></div>
|
||||
<input
|
||||
type="range"
|
||||
class="min"
|
||||
name="min"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $min_price ); ?>"
|
||||
data-wc-bind--min="context.minRange"
|
||||
data-wc-bind--max="context.maxRange"
|
||||
data-wc-bind--value="context.minPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
class="max"
|
||||
name="max"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $max_price ); ?>"
|
||||
data-wc-bind--min="context.minRange"
|
||||
data-wc-bind--max="context.maxRange"
|
||||
data-wc-bind--value="context.maxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
</div>
|
||||
<div class="text">
|
||||
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
|
||||
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
@@ -5,17 +5,17 @@ use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
|
||||
/**
|
||||
* Collection Rating Filter Block
|
||||
* Product Filter: Rating Block
|
||||
*
|
||||
* @package Automattic\WooCommerce\Blocks\BlockTypes
|
||||
*/
|
||||
final class CollectionRatingFilter extends AbstractBlock {
|
||||
final class ProductFilterRating extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-rating-filter';
|
||||
protected $block_name = 'product-filter-rating';
|
||||
|
||||
const RATING_FILTER_QUERY_VAR = 'rating_filter';
|
||||
|
||||
@@ -80,8 +80,8 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
/* translators: %d is the rating value. */
|
||||
'title' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating ),
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-rating-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-rating-filter::' . wp_json_encode( array( 'value' => $rating ) ),
|
||||
'data-wc-on--click' => "{$this->get_full_block_name()}::actions.removeFilter",
|
||||
'data-wc-context' => "{$this->get_full_block_name()}::" . wp_json_encode( array( 'value' => $rating ) ),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -114,31 +114,47 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_ratings_query_param = isset( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => 'woocommerce/collection-rating-filter',
|
||||
'data-wc-interactive' => $this->get_full_block_name(),
|
||||
'class' => 'wc-block-rating-filter',
|
||||
)
|
||||
);
|
||||
|
||||
$filtered_rating_counts = array_filter(
|
||||
$rating_counts,
|
||||
function( $rating ) {
|
||||
return $rating['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $filtered_rating_counts ) ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_ratings_query_param = isset( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$input = 'list' === $display_style ? CheckboxList::render(
|
||||
array(
|
||||
'items' => $this->get_checkbox_list_items( $rating_counts, $selected_ratings_query_param, $show_counts ),
|
||||
'on_change' => 'woocommerce/collection-rating-filter::actions.onCheckboxChange',
|
||||
'items' => $this->get_checkbox_list_items( $filtered_rating_counts, $selected_ratings_query_param, $show_counts ),
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.onCheckboxChange",
|
||||
)
|
||||
) : Dropdown::render(
|
||||
$this->get_dropdown_props( $rating_counts, $selected_ratings_query_param, $show_counts )
|
||||
$this->get_dropdown_props( $filtered_rating_counts, $selected_ratings_query_param, $show_counts, $attributes['selectType'] )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-rating-filter__controls">%2$s</div>
|
||||
%2$s
|
||||
<div class="wc-block-rating-filter__controls">%3$s</div>
|
||||
<div class="wc-block-rating-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$input
|
||||
);
|
||||
}
|
||||
@@ -205,22 +221,24 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
/**
|
||||
* Get the dropdown props.
|
||||
*
|
||||
* @param mixed $rating_counts The rating counts.
|
||||
* @param mixed $selected_ratings_query The url query param for selected ratings.
|
||||
* @param mixed $show_counts Whether to show the counts.
|
||||
* @param mixed $rating_counts The rating counts.
|
||||
* @param mixed $selected_ratings_query The url query param for selected ratings.
|
||||
* @param bool $show_counts Whether to show the counts.
|
||||
* @param string $select_type The select type. (single|multiple).
|
||||
* @return array<array-key, array>
|
||||
*/
|
||||
private function get_dropdown_props( $rating_counts, $selected_ratings_query, $show_counts ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
private function get_dropdown_props( $rating_counts, $selected_ratings_query, $show_counts, $select_type ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
$placeholder_text = 'single' === $select_type ? __( 'Select a rating', 'woocommerce' ) : __( 'Select ratings', 'woocommerce' );
|
||||
|
||||
$selected_item = array_reduce(
|
||||
$selected_items = array_reduce(
|
||||
$rating_counts,
|
||||
function( $carry, $rating ) use ( $ratings_array, $show_counts ) {
|
||||
if ( in_array( (string) $rating['rating'], $ratings_array, true ) ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
$rating_str = (string) $rating['rating'];
|
||||
return array(
|
||||
$carry[] = array(
|
||||
/* translators: %d is referring to the average rating value. Example: Rated 4 out of 5. */
|
||||
'label' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating_str ) . ' ' . $count_label,
|
||||
'value' => $rating['rating'],
|
||||
@@ -232,7 +250,7 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
);
|
||||
|
||||
return array(
|
||||
'items' => array_map(
|
||||
'items' => array_map(
|
||||
function ( $rating ) use ( $show_counts ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
@@ -245,8 +263,10 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
},
|
||||
$rating_counts
|
||||
),
|
||||
'selected_item' => $selected_item,
|
||||
'action' => 'woocommerce/collection-rating-filter::actions.onDropdownChange',
|
||||
'select_type' => $select_type,
|
||||
'selected_items' => $selected_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.onDropdownChange",
|
||||
'placeholder' => $placeholder_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,19 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
|
||||
/**
|
||||
* CollectionStockFilter class.
|
||||
* Product Filter: Stock Status Block.
|
||||
*/
|
||||
final class CollectionStockFilter extends AbstractBlock {
|
||||
final class ProductFilterStockStatus extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-stock-filter';
|
||||
protected $block_name = 'product-filter-stock-status';
|
||||
|
||||
const STOCK_STATUS_QUERY_VAR = 'filter_stock_status';
|
||||
|
||||
@@ -74,13 +75,15 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$action_namespace = $this->get_full_block_name();
|
||||
|
||||
$active_stock_statuses = array_map(
|
||||
function( $status ) use ( $stock_status_options ) {
|
||||
function( $status ) use ( $stock_status_options, $action_namespace ) {
|
||||
return array(
|
||||
'title' => $stock_status_options[ $status ],
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-stock-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-stock-filter::' . wp_json_encode( array( 'value' => $status ) ),
|
||||
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
|
||||
'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ) ),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -127,10 +130,12 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-stock-filter__controls">%2$s</div>
|
||||
%2$s
|
||||
<div class="wc-block-stock-filter__controls">%3$s</div>
|
||||
<div class="wc-block-stock-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$this->get_stock_filter_html( $stock_status_counts, $attributes ),
|
||||
);
|
||||
}
|
||||
@@ -145,97 +150,78 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
private function get_stock_filter_html( $stock_counts, $attributes ) {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
$select_type = $attributes['selectType'] ?? 'single';
|
||||
$stock_statuses = wc_get_product_stock_status_options();
|
||||
|
||||
$placeholder_text = 'single' === $select_type ? __( 'Select stock status', 'woocommerce' ) : __( 'Select stock statuses', 'woocommerce' );
|
||||
|
||||
// check the url params to select initial item on page load.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_stock_status = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
$query = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
$selected_stock_statuses = explode( ',', $query );
|
||||
|
||||
$list_items = array_map(
|
||||
function( $item ) use ( $stock_statuses, $show_counts ) {
|
||||
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
|
||||
return array(
|
||||
'label' => $label,
|
||||
'value' => $item['status'],
|
||||
);
|
||||
},
|
||||
$stock_counts
|
||||
$filtered_stock_counts = array_filter(
|
||||
$stock_counts,
|
||||
function( $stock_count ) {
|
||||
return $stock_count['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $filtered_stock_counts ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$list_items = array_values(
|
||||
array_map(
|
||||
function( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) {
|
||||
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
|
||||
return array(
|
||||
'label' => $label,
|
||||
'value' => $item['status'],
|
||||
'checked' => in_array( $item['status'], $selected_stock_statuses, true ),
|
||||
);
|
||||
},
|
||||
$filtered_stock_counts
|
||||
)
|
||||
);
|
||||
|
||||
$selected_items = array_values(
|
||||
array_filter(
|
||||
$list_items,
|
||||
function( $item ) use ( $selected_stock_status ) {
|
||||
return $item['value'] === $selected_stock_status;
|
||||
function( $item ) use ( $selected_stock_statuses ) {
|
||||
return in_array( $item['value'], $selected_stock_statuses, true );
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Just for the dropdown, we can only select 1 item.
|
||||
$selected_item = $selected_items[0] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-stock-filter' ) );
|
||||
$data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div data-wc-interactive='<?php echo esc_attr( $data_directive ); ?>'>
|
||||
<?php if ( 'list' === $display_style ) : ?>
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $stock_counts as $stock_count ) { ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $stock_count['status'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
value="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
<?php checked( strpos( $selected_stock_status, $stock_count['status'] ) !== false, 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php echo esc_html( $stock_statuses[ $stock_count['status'] ] ); ?>
|
||||
|
||||
<?php if ( $show_counts ) : ?>
|
||||
<?php
|
||||
// translators: %s: number of products.
|
||||
$screen_reader_text = sprintf( _n( '%s product', '%s products', $stock_count['count'], 'woocommerce' ), number_format_i18n( $stock_count['count'] ) );
|
||||
?>
|
||||
<span>
|
||||
<span aria-hidden="true">
|
||||
<?php $show_counts ? print( esc_html( '(' . $stock_count['count'] . ')' ) ) : null; ?>
|
||||
</span>
|
||||
<span class="screen-reader-text">
|
||||
<?php esc_html( $screen_reader_text ); ?>
|
||||
</span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'list' === $display_style ) { ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CheckboxList::render() escapes output.
|
||||
echo CheckboxList::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.onCheckboxChange",
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ( 'dropdown' === $display_style ) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dropdown::render() escapes output.
|
||||
echo Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'woocommerce/collection-stock-filter::actions.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
'items' => $list_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.onDropdownChange",
|
||||
'selected_items' => $selected_items,
|
||||
'select_type' => $select_type,
|
||||
'placeholder' => $placeholder_text,
|
||||
)
|
||||
);
|
||||
?>
|
||||
@@ -87,9 +87,9 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
|
||||
return strtr(
|
||||
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
|
||||
<div class="wc-block-product-gallery-large-image__container">
|
||||
<ul class="wc-block-product-gallery-large-image__container">
|
||||
{main_images}
|
||||
</div>
|
||||
</ul>
|
||||
{content}
|
||||
</div>',
|
||||
array(
|
||||
@@ -142,7 +142,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
|
||||
$main_image_with_wrapper = array_map(
|
||||
function( $main_image_element ) {
|
||||
return "<div class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</div>';
|
||||
return "<li class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</li>';
|
||||
},
|
||||
$main_images
|
||||
);
|
||||
|
||||
@@ -115,11 +115,13 @@ class ProductGalleryPager extends AbstractBlock {
|
||||
|
||||
$is_first_pager_item = 0 === $key;
|
||||
$pager_item = sprintf(
|
||||
'<li class="wc-block-product-gallery-pager__pager-item %2$s">%1$s</li>',
|
||||
'<li class="wc-block-product-gallery-pager__pager-item %2$s"><button aria-pressed="%3$s" data-wc-bind--aria-pressed="state.pagerButtonPressed">%1$s</button></li>',
|
||||
'dots' === $pager_display_mode ? $this->get_dot_icon( $is_first_pager_item ) : $key + 1,
|
||||
$is_first_pager_item ? 'wc-block-product-gallery-pager__pager-item--is-active' : ''
|
||||
$is_first_pager_item ? 'wc-block-product-gallery-pager__pager-item--is-active' : '',
|
||||
$is_first_pager_item ? 'true' : 'false'
|
||||
);
|
||||
$p = new \WP_HTML_Tag_Processor( $pager_item );
|
||||
|
||||
$p = new \WP_HTML_Tag_Processor( $pager_item );
|
||||
|
||||
if ( $p->next_tag() ) {
|
||||
$p->set_attribute(
|
||||
@@ -158,7 +160,7 @@ class ProductGalleryPager extends AbstractBlock {
|
||||
$initial_opacity = $is_active ? '1' : '0.2';
|
||||
return sprintf(
|
||||
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="6" cy="6" r="6" fill="black" fill-opacity="%1$s" data-wc-bind--fill-opacity="state.pagerDotFillOpacity" />
|
||||
<circle cx="6" cy="6" r="6" fill="black" fill-opacity="%1$s" data-wc-bind--fill-opacity="state.pagerDotFillOpacity" />
|
||||
</svg>',
|
||||
$initial_opacity
|
||||
);
|
||||
|
||||
@@ -240,6 +240,11 @@ final class BlockTypesController {
|
||||
'ProductCategory',
|
||||
'ProductCollection',
|
||||
'ProductCollectionNoResults',
|
||||
'ProductGallery',
|
||||
'ProductGalleryLargeImage',
|
||||
'ProductGalleryLargeImageNextPrevious',
|
||||
'ProductGalleryPager',
|
||||
'ProductGalleryThumbnails',
|
||||
'ProductImage',
|
||||
'ProductImageGallery',
|
||||
'ProductNew',
|
||||
@@ -281,6 +286,8 @@ final class BlockTypesController {
|
||||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
'OrderConfirmation\AdditionalFieldsWrapper',
|
||||
'OrderConfirmation\AdditionalFields',
|
||||
);
|
||||
|
||||
$block_types = array_merge(
|
||||
@@ -291,17 +298,12 @@ final class BlockTypesController {
|
||||
);
|
||||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
$block_types[] = 'ProductGallery';
|
||||
$block_types[] = 'ProductGalleryLargeImage';
|
||||
$block_types[] = 'ProductGalleryLargeImageNextPrevious';
|
||||
$block_types[] = 'ProductGalleryPager';
|
||||
$block_types[] = 'ProductGalleryThumbnails';
|
||||
$block_types[] = 'CollectionFilters';
|
||||
$block_types[] = 'CollectionStockFilter';
|
||||
$block_types[] = 'CollectionPriceFilter';
|
||||
$block_types[] = 'CollectionAttributeFilter';
|
||||
$block_types[] = 'CollectionRatingFilter';
|
||||
$block_types[] = 'CollectionActiveFilters';
|
||||
$block_types[] = 'ProductFilter';
|
||||
$block_types[] = 'ProductFilterStockStatus';
|
||||
$block_types[] = 'ProductFilterPrice';
|
||||
$block_types[] = 'ProductFilterAttribute';
|
||||
$block_types[] = 'ProductFilterRating';
|
||||
$block_types[] = 'ProductFilterActive';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,6 +344,8 @@ final class BlockTypesController {
|
||||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
'OrderConfirmation\AdditionalFieldsWrapper',
|
||||
'OrderConfirmation\AdditionalFields',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Blocks\AssetsController;
|
||||
use Automattic\WooCommerce\Blocks\BlockPatterns;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Blocks\BlockTypesController;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
@@ -14,6 +15,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
|
||||
use Automattic\WooCommerce\Blocks\InboxNotifications;
|
||||
use Automattic\WooCommerce\Blocks\Installer;
|
||||
use Automattic\WooCommerce\Blocks\Migration;
|
||||
@@ -140,6 +142,7 @@ class Bootstrap {
|
||||
$this->container->get( Installer::class )->init();
|
||||
$this->container->get( GoogleAnalytics::class )->init();
|
||||
$this->container->get( CheckoutFields::class )->init();
|
||||
$this->container->get( CheckoutFieldsAdmin::class )->init();
|
||||
}
|
||||
|
||||
// Load assets unless this is a request specifically for the store API.
|
||||
@@ -160,6 +163,8 @@ class Bootstrap {
|
||||
$this->container->get( SingleProductTemplateCompatibility::class )->init();
|
||||
$this->container->get( Notices::class )->init();
|
||||
}
|
||||
|
||||
$this->container->get( QueryFilters::class )->init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +355,13 @@ class Bootstrap {
|
||||
return new CheckoutFields( $container->get( AssetDataRegistry::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsAdmin::class,
|
||||
function( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsAdmin( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PaymentsApi::class,
|
||||
function ( Container $container ) {
|
||||
@@ -413,6 +425,12 @@ class Bootstrap {
|
||||
return new TasksController();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
QueryFilters::class,
|
||||
function() {
|
||||
return new QueryFilters();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,153 +242,262 @@ class CheckoutFields {
|
||||
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
*/
|
||||
public function register_checkout_field( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
wc_get_logger()->warning( 'A checkout field cannot be registered without an id.' );
|
||||
|
||||
// Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered.
|
||||
$result = $this->validate_options( $options );
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The above validate_options function ensures these options are valid. Type might not be supplied but then it defaults to text.
|
||||
$id = $options['id'];
|
||||
$location = $options['location'];
|
||||
$type = $options['type'] ?? 'text';
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? sprintf(
|
||||
/* translators: %s Field label. */
|
||||
__( '%s (optional)', 'woocommerce' ),
|
||||
$options['label']
|
||||
) : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
);
|
||||
|
||||
$field_data['attributes'] = $this->register_field_attributes( $id, $options['attributes'] ?? [] );
|
||||
|
||||
if ( 'checkbox' === $type ) {
|
||||
$result = $this->process_checkbox_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
if ( 'select' === $type ) {
|
||||
$result = $this->process_select_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the "base" options (id, label, location) and shows warnings if they're not supplied.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @return bool false if an error was encountered, true otherwise.
|
||||
*/
|
||||
private function validate_options( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Having fewer than 2 after exploding around a / means there is no namespace.
|
||||
if ( count( explode( '/', $options['id'] ) ) < 2 ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'A checkout field id must consist of namespace/name.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'A checkout field id must consist of namespace/name.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field label is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field label is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['location'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is invalid.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is invalid.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = 'text';
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
esc_html( $options['id'] ),
|
||||
esc_html( $options['type'] ),
|
||||
implode( ', ', $this->supported_field_types )
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
$type = $options['type'];
|
||||
}
|
||||
|
||||
// At this point, the essentials fields and its location should be set.
|
||||
// At this point, the essentials fields and its location should be set and valid.
|
||||
$location = $options['location'];
|
||||
$id = $options['id'];
|
||||
|
||||
// Check to see if field is already in the array.
|
||||
if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'The field is already registered.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The field is already registered.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hidden fields are not supported right now. They will be registered with hidden => false.
|
||||
if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', esc_html( $id ) )
|
||||
$message = sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
// Don't return here unlike the other fields because this is not an issue that will prevent registration.
|
||||
}
|
||||
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
$message = sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
$id,
|
||||
$options['type'],
|
||||
implode( ', ', $this->supported_field_types )
|
||||
);
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a select field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_select_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options".' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
$message = sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
$message = sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', $id, $sanitized_value );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? '' : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
'autocomplete' => empty( $options['autocomplete'] ) ? '' : $options['autocomplete'],
|
||||
'autocapitalize' => empty( $options['autocapitalize'] ) ? '' : $options['autocapitalize'],
|
||||
$field_data['options'] = $cleaned_options;
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a checkbox field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_checkbox_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
$message = sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the attributes supplied during field registration.
|
||||
*
|
||||
* @param array $id The field ID.
|
||||
* @param array $attributes The attributes supplied during field registration.
|
||||
*
|
||||
* @return array The processed attributes.
|
||||
*/
|
||||
private function register_field_attributes( $id, $attributes ) {
|
||||
|
||||
// We check if attributes are valid. This is done to prevent too much nesting and also to allow field registration
|
||||
// even if the attributes property is invalid. We can just skip it and register the field without attributes.
|
||||
$has_attributes = false;
|
||||
|
||||
if ( empty( $attributes ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! is_array( $attributes ) || 0 === count( $attributes ) ) {
|
||||
$message = sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". %s', $id, 'Attributes must be a non-empty array.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return [];
|
||||
}
|
||||
|
||||
// These are formatted in camelCase because React components expect them that way.
|
||||
$allowed_attributes = array(
|
||||
'maxLength',
|
||||
'readOnly',
|
||||
'pattern',
|
||||
'autocomplete',
|
||||
'autocapitalize',
|
||||
'title',
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle Checkbox fields.
|
||||
*/
|
||||
if ( 'checkbox' === $type ) {
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
$valid_attributes = array_filter(
|
||||
$attributes,
|
||||
function( $_, $key ) use ( $allowed_attributes ) {
|
||||
return in_array( $key, $allowed_attributes, true ) || strpos( $key, 'aria-' ) === 0 || strpos( $key, 'data-' ) === 0;
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
// Any invalid attributes should show a doing_it_wrong warning. It shouldn't stop field registration, though.
|
||||
if ( count( $attributes ) !== count( $valid_attributes ) ) {
|
||||
$invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) );
|
||||
$message = sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Select fields.
|
||||
*/
|
||||
if ( 'select' === $type ) {
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options".' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', esc_html( $id ), esc_html( $sanitized_value ) )
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data['options'] = $cleaned_options;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
// Escape attributes to remove any malicious code and return them.
|
||||
return array_map(
|
||||
function( $value ) {
|
||||
return esc_attr( $value );
|
||||
},
|
||||
$valid_attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,6 +518,91 @@ class CheckoutFields {
|
||||
return $this->additional_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location of a field.
|
||||
*
|
||||
* @param string $field_key The key of the field to get the location for.
|
||||
* @return string The location of the field.
|
||||
*/
|
||||
public function get_field_location( $field_key ) {
|
||||
foreach ( $this->fields_locations as $location => $fields ) {
|
||||
if ( in_array( $field_key, $fields, true ) ) {
|
||||
return $location;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
|
||||
*
|
||||
* @param string $key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
public function validate_field( $key, $field_value, $request, $address_type = null ) {
|
||||
|
||||
$error = new \WP_Error();
|
||||
try {
|
||||
/**
|
||||
* Filter the result of validating an additional field.
|
||||
*
|
||||
* @param \WP_Error $error A WP_Error that extensions may add errors to.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$filtered_result = apply_filters( 'woocommerce_blocks_validate_additional_field_' . $key, $error, $field_value, $request, $address_type );
|
||||
|
||||
if ( $error !== $filtered_result ) {
|
||||
|
||||
// Different WP_Error was returned. This would remove errors from other filters. Skip filtering and allow the order to place without validating this field.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. One of the filters returned a new WP_Error. Filters should use the same WP_Error passed to the filter and use the WP_Error->add function to add errors. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them and validate the field. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. The field will not have any custom validation applied to it. %s',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $filtered_result ) ) {
|
||||
return $filtered_result;
|
||||
}
|
||||
|
||||
// If the filters didn't return a valid value, ignore them and return an empty WP_Error. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s did not return a valid value. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the default locale with additional fields without country limitations.
|
||||
*
|
||||
@@ -451,17 +645,43 @@ class CheckoutFields {
|
||||
return $this->fields_locations['additional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields definitions only meant for order.
|
||||
*
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_order_only_fields() {
|
||||
// For now, all contact fields are order only fields, along with additional fields.
|
||||
$order_fields_keys = array_merge( $this->get_contact_fields_keys(), $this->get_additional_fields_keys() );
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields for a given group.
|
||||
*
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
*
|
||||
* @return array An array of fields.
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_fields_for_location( $location ) {
|
||||
if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) {
|
||||
return $this->fields_locations[ $location ];
|
||||
$order_fields_keys = $this->fields_locations[ $location ];
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,7 +689,7 @@ class CheckoutFields {
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param string $location The gslocation to validate the field for (address|contact|additional).
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
*
|
||||
* @return true|\WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
*/
|
||||
@@ -791,4 +1011,54 @@ class CheckoutFields {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional fields for an order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $context The context to get the field value for (edit|view).
|
||||
* @return array An array of fields definitions as well as their values formatted for display.
|
||||
*/
|
||||
public function get_order_additional_fields_with_values( $order, $location, $group = '', $context = 'edit' ) {
|
||||
$fields = $this->get_fields_for_location( $location );
|
||||
$fields_with_values = array();
|
||||
|
||||
foreach ( $fields as $field_key => $field ) {
|
||||
$value = $this->get_field_from_order( $field_key, $order, $group );
|
||||
|
||||
if ( '' === $value || null === $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'view' === $context ) {
|
||||
$value = $this->format_additional_field_value( $value, $field );
|
||||
}
|
||||
|
||||
$field['value'] = $value;
|
||||
$fields_with_values[ $field_key ] = $field;
|
||||
}
|
||||
|
||||
return $fields_with_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a raw field value for display based on its type definition.
|
||||
*
|
||||
* @param string $value Value to format.
|
||||
* @param array $field Additional field definition.
|
||||
* @return string
|
||||
*/
|
||||
public function format_additional_field_value( $value, $field ) {
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$value = $value ? __( 'Yes', 'woocommerce' ) : __( 'No', 'woocommerce' );
|
||||
}
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$options = array_column( $field['options'], 'label', 'value' );
|
||||
$value = isset( $options[ $value ] ) ? $options[ $value ] : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points in the admin area.
|
||||
*/
|
||||
class CheckoutFieldsAdmin {
|
||||
|
||||
/**
|
||||
* Checkout field controller.
|
||||
*
|
||||
* @var CheckoutFields
|
||||
*/
|
||||
private $checkout_fields_controller;
|
||||
|
||||
/**
|
||||
* Sets up core fields.
|
||||
*
|
||||
* @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller.
|
||||
*/
|
||||
public function __construct( CheckoutFields $checkout_fields_controller ) {
|
||||
$this->checkout_fields_controller = $checkout_fields_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks. This is not run Store API requests.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_contact_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_additional_fields' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the shape of a checkout field to match whats needed in the WooCommerce meta boxes.
|
||||
*
|
||||
* @param array $field The field to format.
|
||||
* @param string $key The field key. This will be used for the ID of the field when passed to the meta box.
|
||||
* @return array Formatted field.
|
||||
*/
|
||||
protected function format_field_for_meta_box( $field, $key ) {
|
||||
$formatted_field = array(
|
||||
'id' => $key,
|
||||
'label' => $field['label'],
|
||||
'value' => $field['value'],
|
||||
'type' => $field['type'],
|
||||
'update_callback' => array( $this, 'update_callback' ),
|
||||
'show' => true,
|
||||
'wrapper_class' => 'form-field-wide',
|
||||
);
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$formatted_field['options'] = array_column( $field['options'], 'label', 'value' );
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$formatted_field['checked_value'] = '1';
|
||||
$formatted_field['unchecked_value'] = '0';
|
||||
}
|
||||
|
||||
return $formatted_field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a field value for an order.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Order $order The order to update the field for.
|
||||
*/
|
||||
public function update_callback( $key, $value, $order ) {
|
||||
$this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects address fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_address_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$group = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping';
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group, $context );
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$group_key = '/' . $group . '/' . $key;
|
||||
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $group_key );
|
||||
}
|
||||
|
||||
array_splice(
|
||||
$fields,
|
||||
array_search(
|
||||
'state',
|
||||
array_keys( $fields ),
|
||||
true
|
||||
) + 1,
|
||||
0,
|
||||
$additional_fields
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects contact fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_contact_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects additional fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_additional_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,19 @@ class GoogleAnalytics {
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) {
|
||||
// Using an array with strategies as the final argument to wp_register_script was introduced in WP 6.3.
|
||||
// WC requires at least 6.3 at the point of adding this, so it's safe to leave in without version checks.
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false );
|
||||
wp_register_script(
|
||||
'google-tag-manager',
|
||||
'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'],
|
||||
[],
|
||||
null,
|
||||
[
|
||||
'in_footer' => false,
|
||||
'strategy' => 'async',
|
||||
]
|
||||
);
|
||||
wp_add_inline_script(
|
||||
'google-tag-manager',
|
||||
"
|
||||
|
||||
@@ -41,8 +41,12 @@ class Notices {
|
||||
* Initialize notice hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
if ( wp_is_block_theme() ) {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +72,40 @@ class Notices {
|
||||
return array_merge( $allowed_tags, $svg_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces core notice templates with those from blocks.
|
||||
*
|
||||
* The new notice templates match block components with matching icons and styling. The differences are:
|
||||
* 1. Core has notices for info, success, and error notices, blocks has notices for info, success, error,
|
||||
* warning, and a default notice type.
|
||||
* 2. The block notices use different CSS classes to the core notices. Core uses `woocommerce-message`, `is-info`
|
||||
* and `is-error` classes, blocks uses `wc-block-components-notice-banner is-error`,
|
||||
* `wc-block-components-notice-banner is-info`, and `wc-block-components-notice-banner is-success`.
|
||||
* 3. The markup of the notices is different, with the block notices using SVG icons and a slightly different
|
||||
* structure to accommodate this.
|
||||
*
|
||||
* @param string $template Located template path.
|
||||
* @param string $template_name Template name.
|
||||
* @param array $args Template arguments.
|
||||
* @param string $template_path Template path.
|
||||
* @param string $default_path Default path.
|
||||
* @return string
|
||||
*/
|
||||
public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) {
|
||||
$directory = get_stylesheet_directory();
|
||||
$file = $directory . '/woocommerce/' . $template_name;
|
||||
if ( file_exists( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if ( in_array( $template_name, $this->notice_templates, true ) ) {
|
||||
$template = $this->package->get_path( 'templates/block-' . $template_name );
|
||||
wp_enqueue_style( 'wc-blocks-style' );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all notices with the new block based notices.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace Automattic\WooCommerce\Blocks\Images;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\ContentProcessor;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
|
||||
/**
|
||||
* Pexels API client.
|
||||
@@ -27,6 +28,8 @@ class Pexels {
|
||||
* @return array|\WP_Error Array of images, or WP_Error if the request failed.
|
||||
*/
|
||||
public function get_images( $ai_connection, $token, $business_description ) {
|
||||
$business_description = ContentProcessor::summarize_business_description( $business_description, $ai_connection, $token );
|
||||
|
||||
if ( str_word_count( $business_description ) === 1 ) {
|
||||
$search_term = $business_description;
|
||||
} else {
|
||||
@@ -111,6 +114,7 @@ class Pexels {
|
||||
* @return mixed|\WP_Error
|
||||
*/
|
||||
private function define_search_term( $ai_connection, $token, $business_description ) {
|
||||
|
||||
$prompt = sprintf( 'You are a teacher. Based on the following business description, \'%s\', describe to a child exactly what this store is selling in one or two words and be as precise as you can possibly be. Do not reply with generic words that could cause confusion and be associated with other businesses as a response. Make sure you do not add double quotes in your response. Do not add any explanations in the response', $business_description );
|
||||
|
||||
$response = $ai_connection->fetch_ai_response( $token, $prompt, 30 );
|
||||
@@ -119,6 +123,10 @@ class Pexels {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ( isset( $response['code'] ) && 'completion_error' === $response['code'] ) {
|
||||
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $response['completion'] ) ) {
|
||||
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woocommerce' ) );
|
||||
}
|
||||
@@ -222,7 +230,7 @@ class Pexels {
|
||||
* @return array|\WP_Error The total number of required images, or WP_Error if the request failed.
|
||||
*/
|
||||
private function total_number_required_images() {
|
||||
$patterns_dictionary = PatternUpdater::get_patterns_dictionary();
|
||||
$patterns_dictionary = UpdatePatterns::get_patterns_dictionary();
|
||||
|
||||
if ( is_wp_error( $patterns_dictionary ) ) {
|
||||
return $patterns_dictionary;
|
||||
|
||||
@@ -24,12 +24,9 @@ class CheckboxList {
|
||||
wp_enqueue_script( 'wc-interactivity-checkbox-list' );
|
||||
wp_enqueue_style( 'wc-interactivity-checkbox-list' );
|
||||
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$items = $props['items'] ?? array();
|
||||
$checkbox_list_context = array( 'items' => $items );
|
||||
|
||||
// Items should be an array of objects with a label (which can be plaintext or HTML) and value property.
|
||||
$items = $props['items'] ?? array();
|
||||
$on_change = $props['on_change'] ?? '';
|
||||
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ) );
|
||||
|
||||
@@ -50,7 +47,7 @@ class CheckboxList {
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $props['on_change'] ?? '' ); ?>"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
|
||||
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||
<?php checked( $item['checked'], 1 ); ?>
|
||||
>
|
||||
|
||||
@@ -18,69 +18,107 @@ class Dropdown {
|
||||
wp_enqueue_script( 'wc-interactivity-dropdown' );
|
||||
wp_enqueue_style( 'wc-interactivity-dropdown' );
|
||||
|
||||
$selected_item = $props['selected_item'] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
$dropdown_context = array(
|
||||
'selectedItem' => $selected_item,
|
||||
'hoveredItem' => array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
),
|
||||
'isOpen' => false,
|
||||
);
|
||||
|
||||
wc_initial_state( 'woocommerce/interactivity-dropdown', array( 'selectedItem' => $selected_item ) );
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
$select_type = $props['select_type'] ?? 'single';
|
||||
$selected_items = $props['selected_items'] ?? array();
|
||||
|
||||
// Items should be an array of objects with a label and value property.
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$default_placeholder = 'single' === $select_type ? __( 'Select an option', 'woocommerce' ) : __( 'Select options', 'woocommerce' );
|
||||
$placeholder = $props['placeholder'] ?? $default_placeholder;
|
||||
|
||||
$dropdown_context = array(
|
||||
'selectedItems' => $selected_items,
|
||||
'isOpen' => false,
|
||||
'selectType' => $select_type,
|
||||
'defaultPlaceholder' => $placeholder,
|
||||
);
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
|
||||
<div class="wc-interactivity-dropdown" data-wc-context='<?php echo esc_attr( wp_json_encode( $dropdown_context ) ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
data-wc-class--is-active="context.isOpen"
|
||||
tabindex="-1"
|
||||
data-wc-on--click="actions.toggleIsOpen"
|
||||
>
|
||||
<input id="components-form-token-input-1" type="text" autocomplete="off" data-wc-bind--placeholder="state.placeholderText" class="components-form-token-field__input" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-describedby="components-form-token-suggestions-howto-1" value="">
|
||||
<ul hidden data-wc-bind--hidden="!context.isOpen" class="components-form-token-field__suggestions-list" id="components-form-token-suggestions-1" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array(
|
||||
'currentItem' => $item,
|
||||
);
|
||||
?>
|
||||
<li
|
||||
role="option"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
data-wc-context='<?php echo esc_attr( wp_json_encode( $context ) ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="state.isSelected"
|
||||
<div class="wc-interactivity-dropdown" data-wc-on--click="actions.toggleIsOpen" data-wc-context='<?php echo esc_attr( wp_json_encode( $dropdown_context ) ); ?>' >
|
||||
<div class="wc-interactivity-dropdown__dropdown" tabindex="-1" >
|
||||
<div class="wc-interactivity-dropdown__dropdown-selection" id="options-dropdown" tabindex="0" aria-haspopup="listbox">
|
||||
<span class="wc-interactivity-dropdown__placeholder" data-wc-text="state.placeholderText">
|
||||
<?php echo esc_html( $placeholder ); ?>
|
||||
</span>
|
||||
<?php if ( 'multiple' === $select_type ) { ?>
|
||||
<div class="selected-options">
|
||||
<template
|
||||
data-wc-each="context.selectedItems"
|
||||
data-wc-each-key="context.item.value"
|
||||
>
|
||||
<?php // This attribute supports HTML so should be sanitized by caller. ?>
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $item['label']; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wc-interactivity-dropdown__selected-badge">
|
||||
<span class="wc-interactivity-dropdown__badge-text" data-wc-text="context.item.label"></span>
|
||||
<svg
|
||||
data-wc-on--click="actions.unselectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
class="wc-interactivity-dropdown__badge-remove"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<?php foreach ( $selected_items as $selected ) { ?>
|
||||
<div
|
||||
class="wc-interactivity-dropdown__selected-badge"
|
||||
data-wc-key="<?php echo esc_attr( $selected['value'] ); ?>"
|
||||
data-wc-each-child
|
||||
>
|
||||
<span class="wc-interactivity-dropdown__badge-text"><?php echo esc_html( $selected['label'] ); ?></span>
|
||||
<svg
|
||||
data-wc-on--click="actions.unselectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
class="wc-interactivity-dropdown__badge-remove"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<span class="wc-interactivity-dropdown__svg-container">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div data-wc-bind--hidden="!context.isOpen" class="wc-interactivity-dropdown__dropdown-list" aria-labelledby="options-dropdown" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array( 'item' => $item );
|
||||
?>
|
||||
<div
|
||||
class="wc-interactivity-dropdown__dropdown-option"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="state.isSelected"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
>
|
||||
<?php echo $item['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
516
wp/wp-content/plugins/woocommerce/src/Blocks/QueryFilters.php
Normal file
516
wp/wp-content/plugins/woocommerce/src/Blocks/QueryFilters.php
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks;
|
||||
|
||||
use WC_Tax;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
|
||||
/**
|
||||
* Process the query data for filtering purposes.
|
||||
*/
|
||||
final class QueryFilters {
|
||||
/**
|
||||
* Initialization method.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'posts_clauses', array( $this, 'main_query_filter' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the posts clauses of the main query to suport global filters.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
public function main_query_filter( $args, $wp_query ) {
|
||||
if (
|
||||
! $wp_query->is_main_query() ||
|
||||
'product_query' !== $wp_query->get( 'wc_query' )
|
||||
) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
if ( $wp_query->get( 'filter_stock_status' ) ) {
|
||||
$args = $this->stock_filter_clauses( $args, $wp_query );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditional query clauses based on the filter params in query vars.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
public function add_query_clauses( $args, $wp_query ) {
|
||||
$args = $this->stock_filter_clauses( $args, $wp_query );
|
||||
$args = $this->price_filter_clauses( $args, $wp_query );
|
||||
$args = $this->attribute_filter_clauses( $args, $wp_query );
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price data for current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return object
|
||||
*/
|
||||
public function get_filtered_price( $query_vars ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$price_filter_sql = "
|
||||
SELECT min( min_price ) as min_price, MAX( max_price ) as max_price
|
||||
FROM {$wpdb->wc_product_meta_lookup}
|
||||
WHERE product_id IN ( {$product_query_sql} )
|
||||
";
|
||||
|
||||
return $wpdb->get_row( $price_filter_sql ); // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock status counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array status=>count pairs.
|
||||
*/
|
||||
public function get_stock_status_counts( $query_vars ) {
|
||||
global $wpdb;
|
||||
$stock_status_options = array_map( 'esc_sql', array_keys( wc_get_product_stock_status_options() ) );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$stock_status_counts = array();
|
||||
|
||||
foreach ( $stock_status_options as $status ) {
|
||||
$stock_status_count_sql = $this->generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options );
|
||||
|
||||
$result = $wpdb->get_row( $stock_status_count_sql ); // phpcs:ignore
|
||||
$stock_status_counts[ $status ] = $result->status_count;
|
||||
}
|
||||
|
||||
return $stock_status_counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rating counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array rating=>count pairs.
|
||||
*/
|
||||
public function get_rating_counts( $query_vars ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$rating_count_sql = "
|
||||
SELECT COUNT( DISTINCT product_id ) as product_count, ROUND( average_rating, 0 ) as rounded_average_rating
|
||||
FROM {$wpdb->wc_product_meta_lookup}
|
||||
WHERE product_id IN ( {$product_query_sql} )
|
||||
AND average_rating > 0
|
||||
GROUP BY rounded_average_rating
|
||||
ORDER BY rounded_average_rating ASC
|
||||
";
|
||||
|
||||
$results = $wpdb->get_results( $rating_count_sql ); // phpcs:ignore
|
||||
|
||||
return array_map( 'absint', wp_list_pluck( $results, 'product_count', 'rounded_average_rating' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @param string $attribute_to_count Attribute taxonomy name.
|
||||
* @return array termId=>count pairs.
|
||||
*/
|
||||
public function get_attribute_counts( $query_vars, $attribute_to_count ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$attributes_to_count_sql = 'AND term_taxonomy.taxonomy IN ("' . esc_sql( wc_sanitize_taxonomy_name( $attribute_to_count ) ) . '")';
|
||||
$attribute_count_sql = "
|
||||
SELECT COUNT( DISTINCT posts.ID ) as term_count, terms.term_id as term_count_id
|
||||
FROM {$wpdb->posts} AS posts
|
||||
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id
|
||||
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
|
||||
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
|
||||
WHERE posts.ID IN ( {$product_query_sql} )
|
||||
{$attributes_to_count_sql}
|
||||
GROUP BY terms.term_id
|
||||
";
|
||||
|
||||
$results = $wpdb->get_results( $attribute_count_sql ); // phpcs:ignore
|
||||
|
||||
return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for stock filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function stock_filter_clauses( $args, $wp_query ) {
|
||||
if ( ! $wp_query->get( 'filter_stock_status' ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
|
||||
$args['where'] .= ' AND wc_product_meta_lookup.stock_status IN ("' . implode( '","', array_map( 'esc_sql', explode( ',', $wp_query->get( 'filter_stock_status' ) ) ) ) . '")';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for price filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function price_filter_clauses( $args, $wp_query ) {
|
||||
if ( ! $wp_query->get( 'min_price' ) && ! $wp_query->get( 'max_price' ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$adjust_for_taxes = $this->adjust_price_filters_for_displayed_taxes();
|
||||
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
|
||||
|
||||
if ( $wp_query->get( 'min_price' ) ) {
|
||||
$min_price_filter = intval( $wp_query->get( 'min_price' ) );
|
||||
|
||||
if ( $adjust_for_taxes ) {
|
||||
$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price_filter, 'min_price', '>=' );
|
||||
} else {
|
||||
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', $min_price_filter );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $wp_query->get( 'max_price' ) ) {
|
||||
$max_price_filter = intval( $wp_query->get( 'max_price' ) );
|
||||
|
||||
if ( $adjust_for_taxes ) {
|
||||
$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price_filter, 'max_price', '<=' );
|
||||
} else {
|
||||
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', $max_price_filter );
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wc_product_meta_lookup to posts if not already joined.
|
||||
*
|
||||
* @param string $sql SQL join.
|
||||
* @return string
|
||||
*/
|
||||
private function append_product_sorting_table_join( $sql ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
|
||||
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate calculate query by stock status.
|
||||
*
|
||||
* @param string $status status to calculate.
|
||||
* @param string $product_query_sql product query for current filter state.
|
||||
* @param array $stock_status_options available stock status options.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options ) {
|
||||
if ( ! in_array( $status, $stock_status_options, true ) ) {
|
||||
return false;
|
||||
}
|
||||
global $wpdb;
|
||||
$status = esc_sql( $status );
|
||||
return "
|
||||
SELECT COUNT( DISTINCT posts.ID ) as status_count
|
||||
FROM {$wpdb->posts} as posts
|
||||
INNER JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id
|
||||
AND postmeta.meta_key = '_stock_status'
|
||||
AND postmeta.meta_value = '{$status}'
|
||||
WHERE posts.ID IN ( {$product_query_sql} )
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query for price filters when dealing with displayed taxes.
|
||||
*
|
||||
* @param float $price_filter Price filter to apply.
|
||||
* @param string $column Price being filtered (min or max).
|
||||
* @param string $operator Comparison operator for column.
|
||||
* @return string Constructed query.
|
||||
*/
|
||||
private function get_price_filter_query_for_displayed_taxes( $price_filter, $column = 'min_price', $operator = '>=' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Select only used tax classes to avoid unwanted calculations.
|
||||
$product_tax_classes = $wpdb->get_col( "SELECT DISTINCT tax_class FROM {$wpdb->wc_product_meta_lookup};" );
|
||||
|
||||
if ( empty( $product_tax_classes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$or_queries = array();
|
||||
|
||||
// We need to adjust the filter for each possible tax class and combine the queries into one.
|
||||
foreach ( $product_tax_classes as $tax_class ) {
|
||||
$adjusted_price_filter = $this->adjust_price_filter_for_tax_class( $price_filter, $tax_class );
|
||||
$or_queries[] = $wpdb->prepare(
|
||||
'( wc_product_meta_lookup.tax_class = %s AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )',
|
||||
$tax_class,
|
||||
$adjusted_price_filter
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $wpdb->prepare(
|
||||
' AND (
|
||||
wc_product_meta_lookup.tax_status = "taxable" AND ( 0=1 OR ' . implode( ' OR ', $or_queries ) . ')
|
||||
OR ( wc_product_meta_lookup.tax_status != "taxable" AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )
|
||||
) ',
|
||||
$price_filter
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* If price filters need adjustment to work with displayed taxes, this returns true.
|
||||
*
|
||||
* This logic is used when prices are stored in the database differently to how they are being displayed, with regards
|
||||
* to taxes.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function adjust_price_filters_for_displayed_taxes() {
|
||||
$display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$database = wc_prices_include_tax() ? 'incl' : 'excl';
|
||||
|
||||
return $display !== $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a price filter based on a tax class and whether or not the amount includes or excludes taxes.
|
||||
*
|
||||
* This calculation logic is based on `wc_get_price_excluding_tax` and `wc_get_price_including_tax` in core.
|
||||
*
|
||||
* @param float $price_filter Price filter amount as entered.
|
||||
* @param string $tax_class Tax class for adjustment.
|
||||
* @return float
|
||||
*/
|
||||
private function adjust_price_filter_for_tax_class( $price_filter, $tax_class ) {
|
||||
$tax_display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$tax_rates = WC_Tax::get_rates( $tax_class );
|
||||
$base_tax_rates = WC_Tax::get_base_tax_rates( $tax_class );
|
||||
|
||||
// If prices are shown incl. tax, we want to remove the taxes from the filter amount to match prices stored excl. tax.
|
||||
if ( 'incl' === $tax_display ) {
|
||||
/**
|
||||
* Filters if taxes should be removed from locations outside the store base location.
|
||||
*
|
||||
* The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing
|
||||
* with out of base locations. e.g. If a product costs 10 including tax, all users will pay 10
|
||||
* regardless of location and taxes.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*
|
||||
* @internal Matches filter name in WooCommerce core.
|
||||
*
|
||||
* @param boolean $adjust_non_base_location_prices True by default.
|
||||
* @return boolean
|
||||
*/
|
||||
$taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $price_filter, $base_tax_rates, true ) : WC_Tax::calc_tax( $price_filter, $tax_rates, true );
|
||||
return $price_filter - array_sum( $taxes );
|
||||
}
|
||||
|
||||
// If prices are shown excl. tax, add taxes to match the prices stored in the DB.
|
||||
$taxes = WC_Tax::calc_tax( $price_filter, $tax_rates, false );
|
||||
|
||||
return $price_filter + array_sum( $taxes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute lookup table name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_lookup_table_name() {
|
||||
return wc_get_container()->get( LookupDataStore::class )->get_lookup_table_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for attribute filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function attribute_filter_clauses( $args, $wp_query ) {
|
||||
$chosen_attributes = $this->get_chosen_attributes( $wp_query->query_vars );
|
||||
|
||||
if ( empty( $chosen_attributes ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
|
||||
// (causes the filtering subquery to be executed only once).
|
||||
$clause_root = " {$wpdb->posts}.ID IN ( SELECT product_or_parent_id FROM (";
|
||||
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
|
||||
$in_stock_clause = ' AND in_stock = 1';
|
||||
} else {
|
||||
$in_stock_clause = '';
|
||||
}
|
||||
|
||||
$attribute_ids_for_and_filtering = array();
|
||||
|
||||
foreach ( $chosen_attributes as $taxonomy => $data ) {
|
||||
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
|
||||
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
|
||||
$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
|
||||
$term_ids_to_filter_by = array_map( 'absint', $term_ids_to_filter_by );
|
||||
$term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')';
|
||||
$is_and_query = 'and' === $data['query_type'];
|
||||
|
||||
$count = count( $term_ids_to_filter_by );
|
||||
|
||||
if ( 0 !== $count ) {
|
||||
if ( $is_and_query && $count > 1 ) {
|
||||
$attribute_ids_for_and_filtering = array_merge( $attribute_ids_for_and_filtering, $term_ids_to_filter_by );
|
||||
} else {
|
||||
$clauses[] = "
|
||||
{$clause_root}
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE term_id in {$term_ids_to_filter_by_list}
|
||||
{$in_stock_clause}
|
||||
)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $attribute_ids_for_and_filtering ) ) {
|
||||
$count = count( $attribute_ids_for_and_filtering );
|
||||
$term_ids_to_filter_by_list = '(' . join( ',', $attribute_ids_for_and_filtering ) . ')';
|
||||
$clauses[] = "
|
||||
{$clause_root}
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE is_variation_attribute=0
|
||||
{$in_stock_clause}
|
||||
AND term_id in {$term_ids_to_filter_by_list}
|
||||
GROUP BY product_id
|
||||
HAVING COUNT(product_id)={$count}
|
||||
UNION
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE is_variation_attribute=1
|
||||
{$in_stock_clause}
|
||||
AND term_id in {$term_ids_to_filter_by_list}
|
||||
)";
|
||||
}
|
||||
|
||||
if ( ! empty( $clauses ) ) {
|
||||
// "temp" is needed because the extra derived tables require an alias.
|
||||
$args['where'] .= ' AND (' . join( ' temp ) AND ', $clauses ) . ' temp ))';
|
||||
} elseif ( ! empty( $attributes_to_filter_by ) ) {
|
||||
$args['where'] .= ' AND 1=0';
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of attributes and terms selected from query arguments.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array
|
||||
*/
|
||||
private function get_chosen_attributes( $query_vars ) {
|
||||
$chosen_attributes = array();
|
||||
|
||||
if ( empty( $query_vars ) ) {
|
||||
return $chosen_attributes;
|
||||
}
|
||||
|
||||
foreach ( $query_vars as $key => $value ) {
|
||||
if ( 0 === strpos( $key, 'filter_' ) ) {
|
||||
$attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) );
|
||||
$taxonomy = wc_attribute_taxonomy_name( $attribute );
|
||||
$filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array();
|
||||
|
||||
if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query_type = ! empty( $query_vars[ 'query_type_' . $attribute ] ) && in_array( $query_vars[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $query_vars[ 'query_type_' . $attribute ] ) ) : '';
|
||||
$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
|
||||
$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : 'and';
|
||||
}
|
||||
}
|
||||
|
||||
return $chosen_attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Utils;
|
||||
|
||||
/**
|
||||
* BlockHooksTrait
|
||||
*
|
||||
* Shared functionality for using the Block Hooks API with WooCommerce Blocks.
|
||||
*/
|
||||
trait BlockHooksTrait {
|
||||
/**
|
||||
* Callback for `hooked_block_types` to auto-inject the mini-cart block into headers after navigation.
|
||||
*
|
||||
* @param array $hooked_blocks An array of block slugs hooked into a given context.
|
||||
* @param string $position Position of the block insertion point.
|
||||
* @param string $anchor_block The block acting as the anchor for the inserted block.
|
||||
* @param \WP_Block_Template|array $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return array An array of block slugs hooked into a given context.
|
||||
*/
|
||||
public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) {
|
||||
|
||||
/**
|
||||
* If the block has no hook placements, return early.
|
||||
*/
|
||||
if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) {
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
// Cache for active theme.
|
||||
static $active_theme_name = null;
|
||||
if ( is_null( $active_theme_name ) ) {
|
||||
$active_theme_name = wp_get_theme()->get( 'Name' );
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of pattern slugs to exclude from auto-insert (useful when
|
||||
* there are patterns that have a very specific location for the block)
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$pattern_exclude_list = apply_filters( 'woocommerce_hooked_blocks_pattern_exclude_list', array( 'twentytwentytwo/header-centered-logo', 'twentytwentytwo/header-stacked' ) );
|
||||
|
||||
/**
|
||||
* A list of theme slugs to execute this with. This is a temporary
|
||||
* measure until improvements to the Block Hooks API allow for exposing
|
||||
* to all block themes.
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
|
||||
|
||||
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
|
||||
foreach ( $this->hooked_block_placements as $placement ) {
|
||||
if (
|
||||
$placement['position'] === $position &&
|
||||
$placement['anchor'] === $anchor_block &&
|
||||
(
|
||||
isset( $placement['area'] ) &&
|
||||
$this->is_template_part_or_pattern( $context, $placement['area'] )
|
||||
) &&
|
||||
! $this->pattern_is_excluded( $context, $pattern_exclude_list ) &&
|
||||
! $this->has_block_in_content( $context )
|
||||
) {
|
||||
$hooked_blocks[] = $this->namespace . '/' . $this->block_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided context contains a the block already.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function has_block_in_content( $context ) {
|
||||
$content = is_array( $context ) && isset( $context['content'] ) ? $context['content'] : '';
|
||||
$content = '' === $content && $context instanceof \WP_Block_Template ? $context->content : $content;
|
||||
return strpos( $content, 'wp:' . $this->namespace . '/' . $this->block_name ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a provided context, returns whether the context refers to header content.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param string $area The area to check against before inserting.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function is_template_part_or_pattern( $context, $area ) {
|
||||
$is_pattern = is_array( $context ) &&
|
||||
(
|
||||
( isset( $context['blockTypes'] ) && in_array( 'core/template-part/' . $area, $context['blockTypes'], true ) ) ||
|
||||
( isset( $context['categories'] ) && in_array( $area, $context['categories'], true ) )
|
||||
);
|
||||
$is_template_part = $context instanceof \WP_Block_Template && $area === $context->area;
|
||||
return ( $is_pattern || $is_template_part );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the pattern is excluded or not
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param array $pattern_exclude_list List of pattern slugs to exclude.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function pattern_is_excluded( $context, $pattern_exclude_list = array() ) {
|
||||
$pattern_slug = is_array( $context ) && isset( $context['slug'] ) ? $context['slug'] : '';
|
||||
if ( ! $pattern_slug ) {
|
||||
/**
|
||||
* Woo patterns have a slug property in $context, but core/theme patterns dont.
|
||||
* In that case, we fallback to the name property, as they're the same.
|
||||
*/
|
||||
$pattern_slug = is_array( $context ) && isset( $context['name'] ) ? $context['name'] : '';
|
||||
}
|
||||
return in_array( $pattern_slug, $pattern_exclude_list, true );
|
||||
}
|
||||
}
|
||||
@@ -105,4 +105,31 @@ class CartCheckoutUtils {
|
||||
asort( $array_without_accents );
|
||||
return array_replace( $array_without_accents, $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves formatted shipping zones from WooCommerce.
|
||||
*
|
||||
* @return array An array of formatted shipping zones.
|
||||
*/
|
||||
public static function get_shipping_zones() {
|
||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||
$formatted_shipping_zones = array_reduce(
|
||||
$shipping_zones,
|
||||
function( $acc, $zone ) {
|
||||
$acc[] = [
|
||||
'id' => $zone['id'],
|
||||
'title' => $zone['zone_name'],
|
||||
'description' => $zone['formatted_zone_location'],
|
||||
];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$formatted_shipping_zones[] = [
|
||||
'id' => 0,
|
||||
'title' => __( 'International', 'woocommerce' ),
|
||||
'description' => __( 'Locations outside all other zones', 'woocommerce' ),
|
||||
];
|
||||
return $formatted_shipping_zones;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user