Plugin Updates

This commit is contained in:
Tony Volpe
2024-04-02 20:23:21 +00:00
parent 96800520e8
commit 94170ec2c4
1514 changed files with 133309 additions and 105985 deletions

View File

@@ -193,10 +193,6 @@ class UpdateProducts {
$image_alt = $product_data['title'];
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
if ( is_wp_error( $product_image_id ) ) {
return new \WP_Error( 'error_uploading_image', $product_image_id->get_error_message() );
}
$saved_product = $this->product_update( $product, $product_image_id, $product_data['title'], $product_data['description'], $product_data['price'] );
if ( is_wp_error( $saved_product ) ) {
@@ -294,10 +290,6 @@ class UpdateProducts {
$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 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'] );
}
@@ -470,10 +462,6 @@ class UpdateProducts {
$image_alt = self::DUMMY_PRODUCTS[ $i ]['title'];
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
if ( is_wp_error( $product_image_id ) ) {
continue;
}
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
$i++;
@@ -496,7 +484,17 @@ class UpdateProducts {
return new WP_Error( 'invalid_product', __( 'Invalid product.', 'woocommerce' ) );
}
$product->set_image_id( $product_image_id );
if ( ! is_wp_error( $product_image_id ) ) {
$product->set_image_id( $product_image_id );
} else {
wc_get_logger()->warning(
sprintf(
// translators: %s is a generated error message.
__( 'The image upload failed: "%s", creating the product without image', 'woocommerce' ),
$product_image_id->get_error_message()
),
);
}
$product->set_name( $product_title );
$product->set_description( $product_description );
$product->set_price( $product_price );

View File

@@ -353,6 +353,18 @@
]
}
},
{
"name": "Product Gallery",
"slug": "woocommerce-blocks/product-query-product-gallery",
"content": {
"titles": [
{
"default": "Bestsellers",
"ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
}
]
}
},
{
"name": "Featured Products 2 Columns",
"slug": "woocommerce-blocks/featured-products-2-cols",

View File

@@ -11,7 +11,6 @@ use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* BlockTypesController class.
@@ -59,6 +58,7 @@ class BlockTemplatesController {
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'pre_get_block_template', array( $this, 'get_block_template_fallback' ), 10, 3 );
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 1 );
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
@@ -66,8 +66,6 @@ class BlockTemplatesController {
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
if ( wc_current_theme_is_fse_theme() ) {
add_action( 'init', array( $this, 'maybe_migrate_content' ) );
// By default, the Template Part Block only supports template parts that are in the current theme directory.
// This render_callback wrapper allows us to add support for plugin-housed template parts.
add_filter(
@@ -162,7 +160,7 @@ class BlockTemplatesController {
*/
public function render_woocommerce_template_part( $attributes ) {
if ( isset( $attributes['theme'] ) && 'woocommerce/woocommerce' === $attributes['theme'] ) {
$template_part = BlockTemplateUtils::get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
$template_part = get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
return do_blocks( $template_part->content );
@@ -317,7 +315,7 @@ class BlockTemplatesController {
if ( BlockTemplateUtils::DEPRECATED_PLUGIN_SLUG === strtolower( $template_id ) ) {
// Because we are using get_block_templates we have to unhook this method to prevent a recursive loop where this filter is applied.
remove_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
$template_with_deprecated_id = BlockTemplateUtils::get_block_template( $id, $template_type );
$template_with_deprecated_id = get_block_template( $id, $template_type );
// Let's hook this method back now that we have used the function.
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
@@ -349,6 +347,25 @@ class BlockTemplatesController {
return $template;
}
/**
* Add the template title and description to WooCommerce templates.
*
* @param WP_Block_Template|null $block_template The found block template, or null if there isn't one.
* @return WP_Block_Template|null
*/
public function add_block_template_details( $block_template ) {
if ( ! $block_template ) {
return $block_template;
}
if ( ! BlockTemplateUtils::template_has_title( $block_template ) ) {
$block_template->title = BlockTemplateUtils::get_block_template_title( $block_template->slug );
}
if ( ! $block_template->description ) {
$block_template->description = BlockTemplateUtils::get_block_template_description( $block_template->slug );
}
return $block_template;
}
/**
* Add the block template objects to be used.
*
@@ -365,6 +382,7 @@ class BlockTemplatesController {
$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
$template_files = $this->get_block_templates( $slugs, $template_type );
$theme_slug = wp_get_theme()->get_stylesheet();
// @todo: Add apply_filters to _gutenberg_get_template_files() in Gutenberg to prevent duplication of logic.
foreach ( $template_files as $template_file ) {
@@ -391,14 +409,12 @@ class BlockTemplatesController {
if ( 'custom' !== $template_file->source ) {
$template = BlockTemplateUtils::build_template_result_from_file( $template_file, $template_type );
} else {
$template_file->title = BlockTemplateUtils::get_block_template_title( $template_file->slug );
$template_file->description = BlockTemplateUtils::get_block_template_description( $template_file->slug );
$query_result[] = $template_file;
$query_result[] = $template_file;
continue;
}
$is_not_custom = false === array_search(
wp_get_theme()->get_stylesheet() . '//' . $template_file->slug,
$theme_slug . '//' . $template_file->slug,
array_column( $query_result, 'id' ),
true
);
@@ -416,6 +432,11 @@ class BlockTemplatesController {
// This only affects saved templates that were saved BEFORE a theme template with the same slug was added.
$query_result = BlockTemplateUtils::remove_theme_templates_with_custom_alternative( $query_result );
// There is the chance that the user customized the default template, installed a theme with a custom template
// and customized that one as well. When that happens, duplicates might appear in the list.
// See: https://github.com/woocommerce/woocommerce/issues/42220.
$query_result = BlockTemplateUtils::remove_duplicate_customized_templates( $query_result, $theme_slug );
/**
* WC templates from theme aren't included in `$this->get_block_templates()` but are handled by Gutenberg.
* We need to do additional search through all templates file to update title and description for WC
@@ -453,15 +474,13 @@ class BlockTemplatesController {
}
}
if ( 'theme' === $template->origin && BlockTemplateUtils::template_has_title( $template ) ) {
return $template;
}
if ( $template->title === $template->slug ) {
if ( ! BlockTemplateUtils::template_has_title( $template ) ) {
$template->title = BlockTemplateUtils::get_block_template_title( $template->slug );
}
if ( ! $template->description ) {
$template->description = BlockTemplateUtils::get_block_template_description( $template->slug );
}
return $template;
},
$query_result
@@ -577,9 +596,8 @@ class BlockTemplatesController {
public function get_block_templates( $slugs = array(), $template_type = 'wp_template' ) {
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type );
$templates_from_woo = $this->get_block_templates_from_woocommerce( $slugs, $templates_from_db, $template_type );
$templates = array_merge( $templates_from_db, $templates_from_woo );
return BlockTemplateUtils::filter_block_templates_by_feature_flag( $templates );
return array_merge( $templates_from_db, $templates_from_woo );
}
/**
@@ -761,22 +779,4 @@ class BlockTemplatesController {
return $post_type_name;
}
/**
* Migrates page content to templates if needed.
*/
public function maybe_migrate_content() {
// Migration should occur on a normal request to ensure every requirement is met.
// We are postponing it if WP is in maintenance mode, installing, WC installing or if the request is part of a WP-CLI command.
if ( wp_is_maintenance_mode() || ! get_option( 'woocommerce_db_version', false ) || Constants::is_defined( 'WP_SETUP_CONFIG' ) || Constants::is_defined( 'WC_INSTALLING' ) || Constants::is_defined( 'WP_CLI' ) ) {
return;
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'cart' );
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'checkout' );
}
}
}

View File

@@ -666,6 +666,18 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
$attributes['class'] .= ' ajax_add_to_cart';
}
/**
* Filter to manipulate (add/modify/remove) attributes in the HTML code of the generated add to cart button.
*
* @since 8.6.0
*
* @param array $attributes An associative array containing default HTML attributes of the add to cart button.
* @param WC_Product $product The WC_Product instance of the product that will be added to the cart once the button is pressed.
*
* @return array Returns an associative array derived from the default array passed as an argument and added the extra HTML attributes.
*/
$attributes = apply_filters( 'woocommerce_blocks_product_grid_add_to_cart_attributes', $attributes, $product );
return sprintf(
'<a href="%s" %s>%s</a>',
esc_url( $product->add_to_cart_url() ),

View File

@@ -37,6 +37,21 @@ class ClassicTemplate extends AbstractDynamicBlock {
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
}
/**
* 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 );
// Indicate to interactivity powered components that this block is on the page,
// and needs refresh to update data.
$this->asset_data_registry->add( 'needsRefreshForInteractivityAPI', true, true );
}
/**
* Enqueue assets used for rendering the block in editor context.
*

View File

@@ -437,7 +437,7 @@ class MiniCart extends AbstractBlock {
if ( isset( $attributes['miniCartIcon'] ) ) {
if ( 'bag' === $attributes['miniCartIcon'] ) {
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '""/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2408 6.68254C11.4307 6.46089 11.7081 6.33333 12 6.33333H20C20.2919 6.33333 20.5693 6.46089 20.7593 6.68254L24.7593 11.3492C25.0134 11.6457 25.0717 12.0631 24.9085 12.4179C24.7453 12.7727 24.3905 13 24 13H8.00001C7.60948 13 7.25469 12.7727 7.0915 12.4179C6.92832 12.0631 6.9866 11.6457 7.24076 11.3492L11.2408 6.68254ZM12.4599 8.33333L10.1742 11H21.8258L19.5401 8.33333H12.4599Z" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 12C7 11.4477 7.44772 11 8 11H24C24.5523 11 25 11.4477 25 12V25.3333C25 25.8856 24.5523 26.3333 24 26.3333H8C7.44772 26.3333 7 25.8856 7 25.3333V12ZM9 13V24.3333H23V13H9Z" fill="' . $icon_color . '"/>
</svg>';
@@ -476,7 +476,7 @@ class MiniCart extends AbstractBlock {
$theme_has_mini_cart = BlockTemplateUtils::theme_has_template_part( 'mini-cart' );
$template_slug_to_load = $theme_has_mini_cart ? get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
}
$template_part = BlockTemplateUtils::get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
$template_part = get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
$template_part_contents = do_blocks( $template_part->content );

View File

@@ -33,9 +33,11 @@ class AdditionalFields extends AbstractOrderConfirmationBlock {
$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' ),
$controller->filter_fields_for_order_confirmation(
array_merge(
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
)
)
);

View File

@@ -45,23 +45,6 @@ class ProductCollection extends AbstractBlock {
*/
protected $custom_order_opts = array( 'popularity', 'rating' );
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Initialize this block type.
@@ -94,21 +77,24 @@ class ProductCollection extends AbstractBlock {
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
// 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_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
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 );
}
/**
* Mark the Product Collection as an interactive region so it can be updated
* during client-side navigation.
* Enhances the Product Collection block with client-side pagination.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
* This function identifies Product Collection blocks and adds necessary data attributes
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
*
* @param string $block_content The HTML content of the block.
* @param array $block Block details, including its attributes.
*
* @return string Updated block content with added interactivity attributes.
*/
public function add_navigation_id_directive( $block_content, $block, $instance ) {
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( $is_product_collection_block ) {
// Enqueue the Interactivity API runtime.
@@ -116,15 +102,56 @@ class ProductCollection extends AbstractBlock {
$p = new \WP_HTML_Tag_Processor( $block_content );
// Add `data-wc-navigation-id to the query block.
// Add `data-wc-navigation-id to the product collection block.
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) ) ) {
$p->set_attribute(
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array(
// The message to be announced by the screen reader when the page is loading or loaded.
'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
// We don't prefetch the links if user haven't clicked on pagination links yet.
// This way we avoid prefetching when the page loads.
'isPrefetchNextOrPreviousLink' => false,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
)
);
$block_content = $p->get_updated_html();
}
/**
* Add two div's:
* 1. Pagination animation for visual users.
* 2. Accessibility div for screen readers, to announce page load states.
*/
$last_tag_position = strripos( $block_content, '</div>' );
$accessibility_and_animation_html = '
<div
data-wc-interactive="{&quot;namespace&quot;:&quot;woocommerce/product-collection&quot;}"
class="wc-block-product-collection__pagination-animation"
data-wc-class--start-animation="state.startAnimation"
data-wc-class--finish-animation="state.finishAnimation">
</div>
<div
data-wc-interactive="{&quot;namespace&quot;:&quot;woocommerce/product-collection&quot;}"
class="screen-reader-text"
aria-live="polite"
data-wc-text="context.accessibilityMessage">
</div>
';
$block_content = substr_replace(
$block_content,
$accessibility_and_animation_html,
$last_tag_position,
0
);
}
return $block_content;
@@ -132,51 +159,78 @@ class ProductCollection extends AbstractBlock {
/**
* Add interactive links to all anchors inside the Query Pagination block.
* This enabled client-side navigation for the product collection block.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
*/
public function add_navigation_link_directives( $block_content, $block, $instance ) {
$is_product_collection_block = $instance->context['query']['isProductCollectionBlock'] ?? false;
$query_context = $instance->context['query'] ?? array();
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
$query_id = $instance->context['queryId'] ?? null;
$parsed_query_id = $this->parsed_block['attrs']['queryId'] ?? null;
if (
$is_product_collection_block &&
$instance->context['queryId'] === $this->parsed_block['attrs']['queryId']
) {
$p = new \WP_HTML_Tag_Processor( $block_content );
$p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) );
while ( $p->next_tag( 'a' ) ) {
$class_attr = $p->get_attribute( 'class' );
$class_list = preg_split( '/\s+/', $class_attr );
$is_previous = in_array( 'wp-block-query-pagination-previous', $class_list, true );
$is_next = in_array( 'wp-block-query-pagination-next', $class_list, true );
$is_previous_or_next = $is_previous || $is_next;
$navigation_link_payload = array(
'prefetch' => $is_previous_or_next,
'scroll' => false,
);
$p->set_attribute(
'data-wc-navigation-link',
wp_json_encode( $navigation_link_payload )
);
if ( $is_previous ) {
$p->set_attribute( 'key', 'pagination-previous' );
} elseif ( $is_next ) {
$p->set_attribute( 'key', 'pagination-next' );
}
}
$block_content = $p->get_updated_html();
// Only proceed if the block is a product collection block and query IDs match.
if ( $is_product_collection_block && $query_id === $parsed_query_id ) {
$block_content = $this->process_pagination_links( $block_content );
}
return $block_content;
}
/**
* Process pagination links within the block content.
*
* @param string $block_content The block content.
* @return string The updated block content.
*/
private function process_pagination_links( $block_content ) {
if ( ! $block_content ) {
return $block_content;
}
$p = new \WP_HTML_Tag_Processor( $block_content );
$p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) );
// This will help us to find the start of the block content using the `seek` method.
$p->set_bookmark( 'start' );
$this->update_pagination_anchors( $p, 'page-numbers', 'product-collection-pagination-numbers' );
$this->update_pagination_anchors( $p, 'wp-block-query-pagination-next', 'product-collection-pagination--next' );
$this->update_pagination_anchors( $p, 'wp-block-query-pagination-previous', 'product-collection-pagination--previous' );
return $p->get_updated_html();
}
/**
* Sets up data attributes required for interactivity and client-side navigation.
*
* @param \WP_HTML_Tag_Processor $processor The HTML tag processor.
* @param string $class_name The class name of the anchor tags.
* @param string $key_prefix The prefix for the data-wc-key attribute.
*/
private function update_pagination_anchors( $processor, $class_name, $key_prefix ) {
// Start from the beginning of the block content.
$processor->seek( 'start' );
while ( $processor->next_tag(
array(
'tag_name' => 'a',
'class_name' => $class_name,
)
) ) {
$processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
$processor->set_attribute( 'data-wc-on--click', 'actions.navigate' );
$processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) );
if ( in_array( $class_name, array( 'wp-block-query-pagination-next', 'wp-block-query-pagination-previous' ), true ) ) {
$processor->set_attribute( 'data-wc-watch', 'callbacks.prefetch' );
$processor->set_attribute( 'data-wc-on--mouseenter', 'actions.prefetchOnHover' );
}
}
}
/**
* Extra data passed through from server to client for block.
*
@@ -765,7 +819,7 @@ class ProductCollection extends AbstractBlock {
* 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( array( 'object_type' => array( 'product' ) ), 'names' );
$product_taxonomies = array_diff( get_object_taxonomies( 'product', 'names' ), array( 'product_visibility', 'product_shipping_class' ) );
$result = array_filter(
$tax_query,
function ( $item ) use ( $product_taxonomies ) {

View File

@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
use WP_HTML_Tag_Processor;
/**
* Product Filter Block.
@@ -15,13 +16,6 @@ final class ProductFilter extends AbstractBlock {
*/
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.
*
@@ -42,17 +36,6 @@ final class ProductFilter extends AbstractBlock {
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.
*
@@ -70,55 +53,6 @@ final class ProductFilter extends AbstractBlock {
$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.
*
@@ -132,49 +66,30 @@ final class ProductFilter extends AbstractBlock {
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(
$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-wc-navigation-id'] = $this->generate_navigation_id( $block );
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
}
$tags = new WP_HTML_Tag_Processor( $content );
while ( $tags->next_tag( 'div' ) ) {
if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) {
return sprintf(
'<nav %1$s>%2$s</nav>',
get_block_wrapper_attributes( $attributes_data ),
$content
);
}
}
return sprintf(
'<nav %1$s></nav>',
get_block_wrapper_attributes(
$attributes
)
get_block_wrapper_attributes( $attributes_data ),
);
}
@@ -190,257 +105,4 @@ final class ProductFilter extends AbstractBlock {
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;
}
}

View File

@@ -55,28 +55,34 @@ final class ProductFilterActive extends AbstractBlock {
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => 'wc-block-active-filters',
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
'data-wc-context' => wp_json_encode( $context ),
'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes',
)
);
$list_classes = 'filter-list';
if ( 'chips' === $attributes['displayStyle'] ) {
$list_classes .= ' list-chips';
}
ob_start();
?>
<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">
<ul class="<?php echo esc_attr( $list_classes ); ?>">
<?php foreach ( $active_filters as $filter ) : ?>
<li>
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
<span class="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">
<button class="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>
@@ -124,10 +130,10 @@ final class ProductFilterActive extends AbstractBlock {
$remove_label = sprintf( 'Remove %s filter', wp_strip_all_tags( $title ) );
?>
<li class="wc-block-active-filters__list-item">
<span class="wc-block-active-filters__list-item-name">
<li class="list-item">
<span class="list-item-name">
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<button class="wc-block-active-filters__list-item-remove" <?php echo $this->get_html_attributes( $attributes ); ?>>
<button class="list-item-remove" <?php echo $this->get_html_attributes( $attributes ); ?>>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="wc-block-components-chip__remove-icon" aria-hidden="true" focusable="false"><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>
<span class="screen-reader-text"><?php echo esc_html( $remove_label ); ?></span>
</button>
@@ -158,7 +164,7 @@ final class ProductFilterActive extends AbstractBlock {
$remove_label = sprintf( 'Remove %s filter', wp_strip_all_tags( $title ) );
?>
<li class="wc-block-active-filters__list-item">
<li class="list-item">
<span class="is-removable wc-block-components-chip wc-block-components-chip--radius-large">
<span aria-hidden="false" class="wc-block-components-chip__text"><?php echo wp_kses_post( $title ); ?></span>
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

View File

@@ -1,8 +1,11 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
/**
* Product Filter: Attribute Block.
@@ -136,14 +139,7 @@ final class ProductFilterAttribute extends AbstractBlock {
}
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
$attribute_counts = array_reduce(
$block->context['collectionData']['attribute_counts'] ?? [],
function( $acc, $count ) {
$acc[ $count['term'] ] = $count['count'];
return $acc;
},
[]
);
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] );
if ( empty( $attribute_counts ) ) {
return sprintf(
@@ -151,6 +147,7 @@ final class ProductFilterAttribute extends AbstractBlock {
get_block_wrapper_attributes(
array(
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
'data-has-filter' => 'no',
)
),
);
@@ -203,6 +200,7 @@ final class ProductFilterAttribute extends AbstractBlock {
array(
'data-wc-context' => wp_json_encode( $context ),
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
'data-has-filter' => 'yes',
)
),
$content,
@@ -284,4 +282,51 @@ final class ProductFilterAttribute extends AbstractBlock {
)
);
}
/**
* Retrieve the attribute count for current block.
*
* @param WP_Block $block Block instance.
* @param string $slug Attribute slug.
* @param string $query_type Query type, accept 'and' or 'or'.
*/
private function get_attribute_counts( $block, $slug, $query_type ) {
$filters = Package::container()->get( QueryFilters::class );
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
if ( 'and' !== strtolower( $query_type ) ) {
unset( $query_vars[ 'filter_' . str_replace( 'pa_', '', $slug ) ] );
}
unset(
$query_vars['taxonomy'],
$query_vars['term']
);
if ( ! empty( $query_vars['tax_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $slug );
}
$counts = $filters->get_attribute_counts( $query_vars, $slug );
$attribute_counts = array();
foreach ( $counts as $key => $value ) {
$attribute_counts[] = array(
'term' => $key,
'count' => $value,
);
}
$attribute_counts = array_reduce(
$attribute_counts,
function( $acc, $count ) {
$acc[ $count['term'] ] = $count['count'];
return $acc;
},
[]
);
return $attribute_counts;
}
}

View File

@@ -1,6 +1,10 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
/**
* Product Filter: Price Block.
*/
@@ -116,7 +120,7 @@ final class ProductFilterPrice extends AbstractBlock {
return '';
}
$price_range = $block->context['collectionData']['price_range'] ?? [];
$price_range = $this->get_filtered_price( $block );
$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 ) );
@@ -136,18 +140,17 @@ final class ProductFilterPrice extends AbstractBlock {
'inlineInput' => $inline_input
) = $attributes;
$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 ),
)
$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 ),
'data-has-filter' => 'no',
);
if ( $min_range === $max_range || ! $max_range ) {
return sprintf(
'<div %s></div>',
$wrapper_attributes
get_block_wrapper_attributes( $wrapper_attributes )
);
}
@@ -188,9 +191,11 @@ final class ProductFilterPrice extends AbstractBlock {
$formatted_max_price
);
$wrapper_attributes['data-has-filter'] = 'yes';
ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<div class="filter-controls">
<div
@@ -234,4 +239,28 @@ final class ProductFilterPrice extends AbstractBlock {
<?php
return ob_get_clean();
}
/**
* Retrieve the price filter data for current block.
*
* @param WP_Block $block Block instance.
*/
private function get_filtered_price( $block ) {
$filters = Package::container()->get( QueryFilters::class );
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
unset( $query_vars['min_price'], $query_vars['max_price'] );
if ( ! empty( $query_vars['meta_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$query_vars['meta_query'] = ProductCollectionUtils::remove_query_array( $query_vars['meta_query'], 'key', '_price' );
}
$price_results = $filters->get_filtered_price( $query_vars );
return array(
'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
);
}
}

View File

@@ -3,6 +3,10 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
/**
* Product Filter: Rating Block
@@ -41,7 +45,7 @@ final class ProductFilterRating extends AbstractBlock {
* @return array Active filters param keys.
*/
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
$price_param_keys = array_filter(
$rating_param_keys = array_filter(
$url_param_keys,
function( $param ) {
return self::RATING_FILTER_QUERY_VAR === $param;
@@ -50,7 +54,7 @@ final class ProductFilterRating extends AbstractBlock {
return array_merge(
$filter_param_keys,
$price_param_keys
$rating_param_keys
);
}
@@ -110,17 +114,10 @@ final class ProductFilterRating extends AbstractBlock {
return '';
}
$rating_counts = $block->context['collectionData']['rating_counts'] ?? array();
$rating_counts = $this->get_rating_counts( $block );
$display_style = $attributes['displayStyle'] ?? 'list';
$show_counts = $attributes['showCounts'] ?? false;
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wc-interactive' => $this->get_full_block_name(),
'class' => 'wc-block-rating-filter',
)
);
$filtered_rating_counts = array_filter(
$rating_counts,
function( $rating ) {
@@ -128,6 +125,13 @@ final class ProductFilterRating extends AbstractBlock {
}
);
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-wc-interactive' => $this->get_full_block_name(),
'data-has-filter' => empty( $filtered_rating_counts ) ? 'no' : 'yes',
)
);
if ( empty( $filtered_rating_counts ) ) {
return sprintf(
'<div %s></div>',
@@ -150,11 +154,8 @@ final class ProductFilterRating extends AbstractBlock {
return sprintf(
'<div %1$s>
%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
);
}
@@ -207,11 +208,18 @@ final class ProductFilterRating extends AbstractBlock {
$count = $rating['count'];
$count_label = $show_counts ? "($count)" : '';
$aria_label = sprintf(
/* translators: %1$d is referring to rating value. Example: Rated 4 out of 5. */
__( 'Rated %s out of 5', 'woocommerce' ),
$rating_str,
);
return array(
'id' => 'rating-' . $rating_str,
'checked' => in_array( $rating_str, $ratings_array, true ),
'label' => $this->render_rating_label( (int) $rating_str, $count_label ),
'value' => $rating_str,
'id' => 'rating-' . $rating_str,
'checked' => in_array( $rating_str, $ratings_array, true ),
'label' => $this->render_rating_label( (int) $rating_str, $count_label ),
'aria_label' => $aria_label,
'value' => $rating_str,
);
},
$rating_counts
@@ -269,4 +277,31 @@ final class ProductFilterRating extends AbstractBlock {
'placeholder' => $placeholder_text,
);
}
/**
* Retrieve the rating filter data for current block.
*
* @param WP_Block $block Block instance.
*/
private function get_rating_counts( $block ) {
$filters = Package::container()->get( QueryFilters::class );
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
if ( ! empty( $query_vars['tax_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'rating_filter', true );
}
$counts = $filters->get_rating_counts( $query_vars );
$data = array();
foreach ( $counts as $key => $value ) {
$data[] = array(
'rating' => $key,
'count' => $value,
);
}
return $data;
}
}

View File

@@ -3,6 +3,9 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;
/**
* Product Filter: Stock Status Block.
@@ -125,17 +128,16 @@ final class ProductFilterStockStatus extends AbstractBlock {
return '';
}
$stock_status_counts = $block->context['collectionData']['stock_status_counts'] ?? [];
$wrapper_attributes = get_block_wrapper_attributes();
$stock_status_counts = $this->get_stock_status_counts( $block );
$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-has-filter' => empty( $stock_status_counts ) ? 'no' : 'yes',
)
);
return sprintf(
'<div %1$s>
%2$s
<div class="wc-block-stock-filter__controls">%3$s</div>
<div class="wc-block-stock-filter__actions"></div>
</div>',
'<div %1$s>%2$s</div>',
$wrapper_attributes,
$content,
$this->get_stock_filter_html( $stock_status_counts, $attributes ),
);
}
@@ -148,6 +150,10 @@ final class ProductFilterStockStatus extends AbstractBlock {
* @return string Rendered block type output.
*/
private function get_stock_filter_html( $stock_counts, $attributes ) {
if ( empty( $stock_counts ) ) {
return '';
}
$display_style = $attributes['displayStyle'] ?? 'list';
$show_counts = $attributes['showCounts'] ?? false;
$select_type = $attributes['selectType'] ?? 'single';
@@ -160,17 +166,6 @@ final class ProductFilterStockStatus extends AbstractBlock {
$query = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
$selected_stock_statuses = explode( ',', $query );
$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 ) {
@@ -181,7 +176,7 @@ final class ProductFilterStockStatus extends AbstractBlock {
'checked' => in_array( $item['status'], $selected_stock_statuses, true ),
);
},
$filtered_stock_counts
$stock_counts
)
);
@@ -231,4 +226,38 @@ final class ProductFilterStockStatus extends AbstractBlock {
<?php
return ob_get_clean();
}
/**
* Retrieve the stock status filter data for current block.
*
* @param WP_Block $block Block instance.
*/
private function get_stock_status_counts( $block ) {
$filters = Package::container()->get( QueryFilters::class );
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
unset( $query_vars['filter_stock_status'] );
if ( ! empty( $query_vars['meta_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$query_vars['meta_query'] = ProductCollectionUtils::remove_query_array( $query_vars['meta_query'], 'key', '_stock_status' );
}
$counts = $filters->get_stock_status_counts( $query_vars );
$data = array();
foreach ( $counts as $key => $value ) {
$data[] = array(
'status' => $key,
'count' => $value,
);
}
return array_filter(
$data,
function( $stock_count ) {
return $stock_count['count'] > 0;
}
);
}
}

View File

@@ -77,25 +77,25 @@ class ProductGallery extends AbstractBlock {
$gallery_dialog = strtr(
'
<div class="wc-block-product-gallery-dialog__overlay" hidden data-wc-bind--hidden="!context.isDialogOpen" data-wc-watch="callbacks.keyboardAccess">
<dialog data-wc-bind--open="context.isDialogOpen">
<div class="wc-block-product-gallery-dialog__header">
<div class="wc-block-product-galler-dialog__header-right">
<button class="wc-block-product-gallery-dialog__close" data-wc-on--click="actions.closeDialog">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="2"/>
<path d="M13 11.8L19.1 5.5L18.1 4.5L12 10.7L5.9 4.5L4.9 5.5L11 11.8L4.5 18.5L5.5 19.5L12 12.9L18.5 19.5L19.5 18.5L13 11.8Z" fill="black"/>
</svg>
</button>
</div>
</div>
<div class="wc-block-product-gallery-dialog__body">
{{html}}
</div>
</dialog>
</div>',
<dialog data-wc-bind--open="context.isDialogOpen" role="dialog" aria-modal="true" aria-label="{{dialog_aria_label}}" hidden data-wc-bind--hidden="!context.isDialogOpen" data-wc-watch="callbacks.keyboardAccess" data-wc-watch--dialog-focus-trap="callbacks.dialogFocusTrap" data-wc-class--wc-block-product-gallery--dialog-open="context.isDialogOpen">
<div class="wc-block-product-gallery-dialog__header">
<div class="wc-block-product-galler-dialog__header-right">
<button class="wc-block-product-gallery-dialog__close" data-wc-on--click="actions.closeDialog" aria-label="{{close_dialog_aria_label}}">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="2"/>
<path d="M13 11.8L19.1 5.5L18.1 4.5L12 10.7L5.9 4.5L4.9 5.5L11 11.8L4.5 18.5L5.5 19.5L12 12.9L18.5 19.5L19.5 18.5L13 11.8Z" fill="black"/>
</svg>
</button>
</div>
</div>
<div class="wc-block-product-gallery-dialog__body">
{{html}}
</div>
</dialog>',
array(
'{{html}}' => $html_processor->get_updated_html(),
'{{html}}' => $html_processor->get_updated_html(),
'{{dialog_aria_label}}' => __( 'Product gallery', 'woocommerce' ),
'{{close_dialog_aria_label}}' => __( 'Close Product Gallery dialog', 'woocommerce' ),
)
);
return $gallery_dialog;
@@ -146,6 +146,7 @@ class ProductGallery extends AbstractBlock {
'dialogVisibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, null, false ),
'mouseIsOverPreviousOrNextButton' => false,
'productId' => $product_id,
'elementThatTriggeredDialogOpening' => null,
)
)
);

View File

@@ -87,7 +87,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
return strtr(
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
<ul class="wc-block-product-gallery-large-image__container">
<ul class="wc-block-product-gallery-large-image__container" tabindex="-1">
{main_images}
</ul>
{content}
@@ -117,9 +117,11 @@ class ProductGalleryLargeImage extends AbstractBlock {
*/
private function get_main_images_html( $context, $product_id ) {
$attributes = array(
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
'data-wc-bind--hidden' => '!state.isSelected',
'data-wc-watch' => 'callbacks.scrollInto',
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
'data-wc-bind--hidden' => '!state.isSelected',
'data-wc-watch' => 'callbacks.scrollInto',
'data-wc-bind--tabindex' => 'state.thumbnailTabIndex',
'data-wc-on--keydown' => 'actions.onSelectedLargeImageKeyDown',
'data-wc-class--wc-block-woocommerce-product-gallery-large-image__image--active-image-slide' => 'state.isSelected',
);

View File

@@ -89,6 +89,10 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
'data-wc-on--click',
'actions.selectPreviousImage'
);
$p->set_attribute(
'aria-label',
__( 'Previous image', 'woocommerce' )
);
$prev_button = $p->get_updated_html();
}
@@ -100,6 +104,10 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
'data-wc-on--click',
'actions.selectNextImage'
);
$p->set_attribute(
'aria-label',
__( 'Next image', 'woocommerce' )
);
$next_button = $p->get_updated_html();
}

View File

@@ -48,7 +48,7 @@ class ProductGalleryThumbnails extends AbstractBlock {
* @return string
*/
protected function generate_view_all_html( $remaining_thumbnails_count ) {
$view_all_html = '<div class="wc-block-product-gallery-thumbnails__thumbnail__overlay wc-block-product-gallery-dialog-on-click" data-wc-on--click="actions.openDialog">
$view_all_html = '<div class="wc-block-product-gallery-thumbnails__thumbnail__overlay wc-block-product-gallery-dialog-on-click" data-wc-on--click="actions.openDialog" data-wc-on--keydown="actions.onViewAllImagesKeyDown" tabindex="0">
<span class="wc-block-product-gallery-thumbnails__thumbnail__remaining-thumbnails-count wc-block-product-gallery-dialog-on-click">+%1$s</span>
<span class="wc-block-product-gallery-thumbnails__thumbnail__view-all wc-block-product-gallery-dialog-on-click">%2$s</span>
</div>';
@@ -156,6 +156,9 @@ class ProductGalleryThumbnails extends AbstractBlock {
$processor = new \WP_HTML_Tag_Processor( $product_gallery_image_html );
if ( $processor->next_tag( 'img' ) ) {
$processor->set_attribute( 'data-wc-on--keydown', 'actions.onThumbnailKeyDown' );
$processor->set_attribute( 'tabindex', '0' );
$processor->set_attribute(
'data-wc-on--click',
'actions.selectImage'

View File

@@ -176,6 +176,13 @@ class ProductQuery extends AbstractBlock {
$this->parsed_block = $parsed_block;
if ( self::is_woocommerce_variation( $parsed_block ) ) {
// Indicate to interactivity powered components that this block is on the page
// and needs refresh to update data.
$this->asset_data_registry->add(
'needsRefreshForInteractivityAPI',
true,
true
);
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
@@ -941,7 +948,7 @@ class ProductQuery extends AbstractBlock {
* 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( array( 'object_type' => array( 'product' ) ), 'names' );
$product_taxonomies = array_diff( get_object_taxonomies( 'product', 'names' ), array( 'product_visibility', 'product_shipping_class' ) );
$result = array_filter(
$tax_query,
function( $item ) use ( $product_taxonomies ) {

View File

@@ -97,8 +97,13 @@ class ProductSaleBadge extends AbstractBlock {
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( ! $product ) {
return null;
}
$is_on_sale = $product->is_on_sale();
if ( ! $is_on_sale ) {

View File

@@ -84,8 +84,13 @@ class ProductStockIndicator extends AbstractBlock {
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( ! $product ) {
return '';
}
$is_in_stock = $product->is_in_stock();
$is_on_backorder = $product->is_on_backorder();

View File

@@ -16,6 +16,7 @@ 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\Domain\Services\CheckoutFieldsFrontend;
use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Installer;
use Automattic\WooCommerce\Blocks\Migration;
@@ -132,7 +133,7 @@ class Bootstrap {
$this->container->get( CreateAccount::class )->init();
$this->container->get( ShippingController::class )->init();
$this->container->get( TasksController::class )->init();
$this->container->get( CheckoutFields::class );
$this->container->get( CheckoutFields::class )->init();
// Load assets in admin and on the frontend.
if ( ! $is_rest ) {
@@ -141,8 +142,7 @@ class Bootstrap {
$this->container->get( AssetsController::class );
$this->container->get( Installer::class )->init();
$this->container->get( GoogleAnalytics::class )->init();
$this->container->get( CheckoutFields::class )->init();
$this->container->get( CheckoutFieldsAdmin::class )->init();
$this->container->get( is_admin() ? CheckoutFieldsAdmin::class : CheckoutFieldsFrontend::class )->init();
}
// Load assets unless this is a request specifically for the store API.
@@ -362,6 +362,13 @@ class Bootstrap {
return new CheckoutFieldsAdmin( $checkout_fields_controller );
}
);
$this->container->register(
CheckoutFieldsFrontend::class,
function( Container $container ) {
$checkout_fields_controller = $container->get( CheckoutFields::class );
return new CheckoutFieldsFrontend( $checkout_fields_controller );
}
);
$this->container->register(
PaymentsApi::class,
function ( Container $container ) {

View File

@@ -4,6 +4,8 @@ namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use WC_Customer;
use WC_Order;
use WP_Error;
/**
* Service class managing checkout fields and its related extensibility points.
@@ -219,11 +221,12 @@ class CheckoutFields {
}
/**
* Initialize hooks. This is not run Store API requests.
* Initialize hooks.
*/
public function init() {
add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) );
add_action( 'woocommerce_blocks_cart_enqueue_data', array( $this, 'add_fields_data' ) );
add_filter( 'woocommerce_customer_allowed_session_meta_keys', array( $this, 'add_session_meta_keys' ) );
}
/**
@@ -234,63 +237,100 @@ class CheckoutFields {
$this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true );
}
/**
* Add session meta keys.
*
* This is an allow-list of meta data keys which we want to store in session.
*
* @param array $keys Session meta keys.
* @return array
*/
public function add_session_meta_keys( $keys ) {
return array_merge( $keys, array( self::BILLING_FIELDS_KEY, self::SHIPPING_FIELDS_KEY, self::ADDITIONAL_FIELDS_KEY ) );
}
/**
* If a field does not declare a sanitization callback, this is the default sanitization callback.
*
* @param mixed $value Value to sanitize.
* @param array $field Field data.
* @return mixed
*/
public function default_sanitize_callback( $value, $field ) {
return $value;
}
/**
* If a field does not declare a validation callback, this is the default validation callback.
*
* @param mixed $value Value to sanitize.
* @param array $field Field data.
* @return WP_Error|void If there is a validation error, return an WP_Error object.
*/
public function default_validate_callback( $value, $field ) {
if ( ! empty( $field['required'] ) && empty( $value ) ) {
return new WP_Error(
'woocommerce_blocks_checkout_field_required',
sprintf(
// translators: %s is field key.
__( 'The field %s is required.', 'woocommerce' ),
$field['id']
)
);
}
}
/**
* Registers an additional field for Checkout.
*
* @param array $options The field options.
*
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
* @return WP_Error|void True if the field was registered, a WP_Error otherwise.
*/
public function register_checkout_field( $options ) {
// 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 ) {
if ( false === $this->validate_options( $options ) ) {
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 = wp_parse_args(
$options,
array(
'id' => '',
'label' => '',
'optionalLabel' => sprintf(
/* translators: %s Field label. */
__( '%s (optional)', 'woocommerce' ),
$options['label']
),
'location' => '',
'type' => 'text',
'hidden' => false,
'required' => false,
'attributes' => array(),
'show_in_order_confirmation' => true,
'sanitize_callback' => array( $this, 'default_sanitize_callback' ),
'validate_callback' => array( $this, 'default_validate_callback' ),
)
);
$field_data['attributes'] = $this->register_field_attributes( $id, $options['attributes'] ?? [] );
$field_data['attributes'] = $this->register_field_attributes( $field_data['id'], $field_data['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 ( 'checkbox' === $field_data['type'] ) {
$field_data = $this->process_checkbox_field( $field_data, $options );
} elseif ( 'select' === $field_data['type'] ) {
$field_data = $this->process_select_field( $field_data, $options );
}
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;
// $field_data will be false if an error that will prevent the field being registered is encountered.
if ( false === $field_data ) {
return;
}
// Insert new field into the correct location array.
$this->additional_fields[ $id ] = $field_data;
$this->fields_locations[ $location ][] = $id;
$this->additional_fields[ $field_data['id'] ] = $field_data;
$this->fields_locations[ $field_data['location'] ][] = $field_data['id'];
}
/**
@@ -301,32 +341,32 @@ class CheckoutFields {
*/
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' );
_doing_it_wrong( '__experimental_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 ) {
$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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
if ( empty( $options['label'] ) ) {
$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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
if ( empty( $options['location'] ) ) {
$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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) {
$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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
@@ -337,14 +377,14 @@ class CheckoutFields {
// Check to see if field is already in the array.
if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) {
$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' );
_doing_it_wrong( '__experimental_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'] ) {
$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' );
_doing_it_wrong( '__experimental_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.
}
@@ -356,38 +396,42 @@ class CheckoutFields {
$options['type'],
implode( ', ', $this->supported_field_types )
);
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
}
if ( ! empty( $options['sanitize_callback'] ) && ! is_callable( $options['sanitize_callback'] ) ) {
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The sanitize_callback must be a valid callback.' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
if ( ! empty( $options['validate_callback'] ) && ! is_callable( $options['validate_callback'] ) ) {
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The validate_callback must be a valid callback.' );
_doing_it_wrong( '__experimental_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.
* @param array $options The options supplied during field registration.
*
* @return array|false The updated $field_data array or false if an error was encountered.
*/
private function process_select_field( $options, $field_data ) {
private function process_select_field( $field_data, $options ) {
$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' );
_doing_it_wrong( '__experimental_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();
@@ -395,7 +439,7 @@ class CheckoutFields {
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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return false;
}
@@ -404,7 +448,7 @@ class CheckoutFields {
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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
continue;
}
@@ -417,18 +461,32 @@ class CheckoutFields {
}
$field_data['options'] = $cleaned_options;
// If the field is not required, inject an empty option at the start.
if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
$field_data['options'] = array_merge(
array(
array(
'value' => '',
'label' => '',
),
),
$field_data['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.
* @param array $options The options supplied during field registration.
*
* @return array|false The updated $field_data array or false if an error was encountered.
*/
private function process_checkbox_field( $options, $field_data ) {
private function process_checkbox_field( $field_data, $options ) {
$id = $options['id'];
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
@@ -436,7 +494,7 @@ class CheckoutFields {
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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
}
return $field_data;
@@ -451,18 +509,15 @@ class CheckoutFields {
* @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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
return [];
}
@@ -488,7 +543,7 @@ class CheckoutFields {
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' );
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
}
// Escape attributes to remove any malicious code and return them.
@@ -534,73 +589,97 @@ class CheckoutFields {
}
/**
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
* Sanitize an additional field against any custom sanitization rules.
*
* @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.7.0
* @param string $field_key The key of the field.
* @param mixed $field_value The value of the field.
* @return mixed
*/
public function sanitize_field( $field_key, $field_value ) {
try {
$field = $this->additional_fields[ $field_key ] ?? null;
if ( $field ) {
$field_value = call_user_func( $field['sanitize_callback'], $field_value, $field );
}
/**
* Allow custom sanitization of an additional field.
*
* @param mixed $field_value The value of the field being sanitized.
* @param string $field_key Key of the field being sanitized.
*
* @since 8.7.0
*/
return apply_filters( '__experimental_woocommerce_blocks_sanitize_additional_field', $field_value, $field_key );
} catch ( \Throwable $e ) {
// One of the filters errored so skip it. This allows the checkout process to continue.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error(
sprintf(
'Field sanitization for %s encountered an error. %s',
esc_html( $field_key ),
esc_html( $e->getMessage() )
),
E_USER_WARNING
);
}
return $field_value;
}
/**
* Validate an additional field against any custom validation rules.
*
* @since 8.6.0
*
* @param string $field_key The key of the field.
* @param mixed $field_value The value of the field.
* @return WP_Error
*/
public function validate_field( $key, $field_value, $request, $address_type = null ) {
public function validate_field( $field_key, $field_value ) {
$errors = new WP_Error();
$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 );
$field = $this->additional_fields[ $field_key ] ?? null;
if ( $error !== $filtered_result ) {
if ( $field ) {
$validation = call_user_func( $field['validate_callback'], $field_value, $field );
// 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
);
if ( is_wp_error( $validation ) ) {
$errors->merge_from( $validation );
}
}
/**
* Pass an error object to allow validation of an additional field.
*
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
* @param string $field_key Key of the field being sanitized.
* @param mixed $field_value The value of the field being validated.
*
* @since 8.7.0
*/
do_action( '__experimental_woocommerce_blocks_validate_additional_field', $errors, $field_key, $field_value );
} 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 ),
'Field validation for %s encountered an error. %s',
esc_html( $field_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();
return $errors;
}
/**
@@ -645,24 +724,6 @@ 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.
*
@@ -685,17 +746,60 @@ class CheckoutFields {
}
/**
* Validates a field value for a given group.
* Validates a set of fields for a given location against custom validation rules.
*
* @param array $fields Array of key value pairs of field values to validate.
* @param string $location The location being validated (address|contact|additional).
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
* @return WP_Error
*/
public function validate_fields_for_location( $fields, $location, $group = '' ) {
$errors = new WP_Error();
try {
/**
* Pass an error object to allow validation of an additional field.
*
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
* @param mixed $fields List of fields (key value pairs) in this location.
* @param string $group The group of this location (shipping|billing|'').
*
* @since 8.7.0
*/
do_action( '__experimental_woocommerce_blocks_validate_location_' . $location . '_fields', $errors, $fields, $group );
} catch ( \Throwable $e ) {
// One of the filters errored so skip them. This allows the checkout process to continue.
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error(
sprintf(
'The action %s encountered an error. The field location %s may not have any custom validation applied to it. %s',
esc_html( 'woocommerce_blocks_validate_' . $location . '_fields' ),
esc_html( $location ),
esc_html( $e->getMessage() )
),
E_USER_WARNING
);
}
return $errors;
}
/**
* Validates a field to check it belongs to the given location and is valid according to its registration.
*
* This does not apply any custom validation rules on the value.
*
* @param string $key The field key.
* @param mixed $value The field value.
* @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.
* @return true|WP_Error True if the field is valid, a WP_Error otherwise.
*/
public function validate_field_for_location( $key, $value, $location ) {
if ( ! $this->is_field( $key ) ) {
return new \WP_Error(
return new WP_Error(
'woocommerce_blocks_checkout_field_invalid',
\sprintf(
// translators: % is field key.
@@ -706,7 +810,7 @@ class CheckoutFields {
}
if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) {
return new \WP_Error(
return new WP_Error(
'woocommerce_blocks_checkout_field_invalid_location',
\sprintf(
// translators: %1$s is field key, %2$s location.
@@ -719,7 +823,7 @@ class CheckoutFields {
$field = $this->additional_fields[ $key ];
if ( ! empty( $field['required'] ) && empty( $value ) ) {
return new \WP_Error(
return new WP_Error(
'woocommerce_blocks_checkout_field_required',
\sprintf(
// translators: %s is field key.
@@ -744,33 +848,29 @@ class CheckoutFields {
}
/**
* Persists a field value for a given order. This would also optionally set the field value on the customer.
* Persists a field value for a given order. This would also optionally set the field value on the customer object if the order is linked to a registered customer.
*
* @param string $key The field key.
* @param mixed $value The field value.
* @param \WC_Order $order The order to persist the field for.
* @param bool $set_customer Whether to set the field value on the customer or not.
* @param string $key The field key.
* @param mixed $value The field value.
* @param WC_Order $order The order to persist the field for.
* @param bool $set_customer Whether to set the field value on the customer or not.
*
* @return void
*/
public function persist_field_for_order( $key, $value, $order, $set_customer = true ) {
$this->set_array_meta( $key, $value, $order );
if ( $set_customer ) {
if ( isset( wc()->customer ) ) {
$this->set_array_meta( $key, $value, wc()->customer );
} elseif ( $order->get_customer_id() ) {
$customer = new \WC_Customer( $order->get_customer_id() );
$this->set_array_meta( $key, $value, $customer );
}
if ( $set_customer && $order->get_customer_id() ) {
$customer = new WC_Customer( $order->get_customer_id() );
$this->persist_field_for_customer( $key, $value, $customer );
}
}
/**
* Persists a field value for a given customer.
*
* @param string $key The field key.
* @param mixed $value The field value.
* @param \WC_Customer $customer The customer to persist the field for.
* @param string $key The field key.
* @param mixed $value The field value.
* @param WC_Customer $customer The customer to persist the field for.
*
* @return void
*/
@@ -781,9 +881,9 @@ class CheckoutFields {
/**
* Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key.
*
* @param string $key The field key.
* @param mixed $value The field value.
* @param \WC_Customer|\WC_Order $object The object to set the field value for.
* @param string $key The field key.
* @param mixed $value The field value.
* @param WC_Customer|WC_Order $object The object to set the field value for.
*
* @return void
*/
@@ -800,39 +900,24 @@ class CheckoutFields {
$meta_key = self::ADDITIONAL_FIELDS_KEY;
}
if ( $object instanceof \WC_Customer ) {
if ( ! $object->get_id() ) {
$meta_data = wc()->session->get( $meta_key, array() );
} else {
$meta_data = get_user_meta( $object->get_id(), $meta_key, true );
}
} elseif ( $object instanceof \WC_Order ) {
$meta_data = $object->get_meta( $meta_key, true );
}
$meta_data = $object->get_meta( $meta_key, true );
if ( ! is_array( $meta_data ) ) {
$meta_data = array();
}
$meta_data[ $key ] = $value;
if ( $object instanceof \WC_Customer ) {
if ( ! $object->get_id() ) {
wc()->session->set( $meta_key, $meta_data );
} else {
update_user_meta( $object->get_id(), $meta_key, $meta_data );
}
} elseif ( $object instanceof \WC_Order ) {
$object->update_meta_data( $meta_key, $meta_data );
}
// Replacing all meta using `add_meta_data`. For some reason `update_meta_data` causes duplicate keys.
$object->add_meta_data( $meta_key, $meta_data, true );
}
/**
* Returns a field value for a given object.
*
* @param string $key The field key.
* @param \WC_Customer $customer The customer to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
* @param string $key The field key.
* @param WC_Customer $customer The customer to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
*
* @return mixed The field value.
*/
@@ -843,9 +928,9 @@ class CheckoutFields {
/**
* Returns a field value for a given order.
*
* @param string $field The field key.
* @param \WC_Order $order The order to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
* @param string $field The field key.
* @param WC_Order $order The order to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
*
* @return mixed The field value.
*/
@@ -856,9 +941,9 @@ class CheckoutFields {
/**
* Returns a field value for a given object.
*
* @param string $key The field key.
* @param \WC_Customer|\WC_Order $object The customer to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
* @param string $key The field key.
* @param WC_Customer|WC_Order $object The customer to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
*
* @return mixed The field value.
*/
@@ -874,15 +959,7 @@ class CheckoutFields {
$meta_key = self::ADDITIONAL_FIELDS_KEY;
}
if ( $object instanceof \WC_Customer ) {
if ( ! $object->get_id() ) {
$meta_data = wc()->session->get( $meta_key, array() );
} else {
$meta_data = get_user_meta( $object->get_id(), $meta_key, true );
}
} elseif ( $object instanceof \WC_Order ) {
$meta_data = $object->get_meta( $meta_key, true );
}
$meta_data = $object->get_meta( $meta_key, true );
if ( ! is_array( $meta_data ) ) {
return '';
@@ -898,38 +975,30 @@ class CheckoutFields {
/**
* Returns an array of all fields values for a given customer.
*
* @param \WC_Customer $customer The customer to get the fields for.
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
* @param WC_Customer $customer The customer to get the fields for.
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
*
* @return array An array of fields.
*/
public function get_all_fields_from_customer( $customer, $all = false ) {
$customer_id = $customer->get_id();
$meta_data = array(
$meta_data = array(
'billing' => array(),
'shipping' => array(),
'additional' => array(),
);
if ( ! $customer_id ) {
if ( isset( wc()->session ) ) {
$meta_data['billing'] = wc()->session->get( self::BILLING_FIELDS_KEY, array() );
$meta_data['shipping'] = wc()->session->get( self::SHIPPING_FIELDS_KEY, array() );
$meta_data['additional'] = wc()->session->get( self::ADDITIONAL_FIELDS_KEY, array() );
}
} else {
$meta_data['billing'] = get_user_meta( $customer_id, self::BILLING_FIELDS_KEY, true );
$meta_data['shipping'] = get_user_meta( $customer_id, self::SHIPPING_FIELDS_KEY, true );
$meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true );
if ( $customer instanceof WC_Customer ) {
$meta_data['billing'] = $customer->get_meta( self::BILLING_FIELDS_KEY, true );
$meta_data['shipping'] = $customer->get_meta( self::SHIPPING_FIELDS_KEY, true );
$meta_data['additional'] = $customer->get_meta( self::ADDITIONAL_FIELDS_KEY, true );
}
return $this->format_meta_data( $meta_data, $all );
}
/**
* Returns an array of all fields values for a given order.
*
* @param \WC_Order $order The order to get the fields for.
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
* @param WC_Order $order The order to get the fields for.
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
*
* @return array An array of fields.
*/
@@ -939,7 +1008,7 @@ class CheckoutFields {
'shipping' => array(),
'additional' => array(),
);
if ( $order instanceof \WC_Order ) {
if ( $order instanceof WC_Order ) {
$meta_data['billing'] = $order->get_meta( self::BILLING_FIELDS_KEY, true );
$meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true );
$meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true );
@@ -997,27 +1066,71 @@ class CheckoutFields {
* For now, this only supports fields in address location.
*
* @param array $fields The fields to filter.
*
* @return array The filtered fields.
*/
public function filter_fields_for_customer( $fields ) {
$customer_fields_keys = $this->get_address_fields_keys();
$customer_fields_keys = array_merge(
$this->get_address_fields_keys(),
$this->get_contact_fields_keys(),
);
return array_filter(
$fields,
function( $key ) use ( $customer_fields_keys ) {
if ( 0 === strpos( $key, '/billing/' ) ) {
$key = str_replace( '/billing/', '', $key );
} elseif ( 0 === strpos( $key, '/shipping/' ) ) {
$key = str_replace( '/shipping/', '', $key );
}
return in_array( $key, $customer_fields_keys, true );
},
ARRAY_FILTER_USE_KEY
);
}
/**
* From a set of fields, returns only the ones for a given location.
*
* @param array $fields The fields to filter.
* @param string $location The location to validate the field for (address|contact|additional).
* @return array The filtered fields.
*/
public function filter_fields_for_location( $fields, $location ) {
return array_filter(
$fields,
function( $key ) use ( $location ) {
if ( 0 === strpos( $key, '/billing/' ) ) {
$key = str_replace( '/billing/', '', $key );
} elseif ( 0 === strpos( $key, '/shipping/' ) ) {
$key = str_replace( '/shipping/', '', $key );
}
return $this->is_field( $key ) && $this->get_field_location( $key ) === $location;
},
ARRAY_FILTER_USE_KEY
);
}
/**
* Filter fields for order confirmation.
*
* @param array $fields The fields to filter.
* @return array The filtered fields.
*/
public function filter_fields_for_order_confirmation( $fields ) {
return array_filter(
$fields,
function( $field ) {
return ! empty( $field['show_in_order_confirmation'] );
}
);
}
/**
* 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).
* @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' ) {

View File

@@ -0,0 +1,300 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use WC_Customer;
use WC_Order;
/**
* Service class managing checkout fields and its related extensibility points on the frontend.
*/
class CheckoutFieldsFrontend {
/**
* 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() {
// Show custom checkout fields on the order details page.
add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 );
add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_additional_fields' ), 10 );
// Show custom checkout fields on the My Account page.
add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 );
// Edit account form under my account (for contact details).
add_filter( 'woocommerce_save_account_details_required_fields', array( $this, 'edit_account_form_required_fields' ), 10, 1 );
add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 );
add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 );
// Edit address form under my account.
add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 );
add_action( 'woocommerce_after_save_address_validation', array( $this, 'save_address_fields' ), 10, 4 );
}
/**
* Render custom fields.
*
* @param array $fields List of additional fields with values.
* @return string
*/
protected function render_additional_fields( $fields ) {
return ! empty( $fields ) ? '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>' : '';
}
/**
* Render custom field.
*
* @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'] )
);
}
/**
* Renders address fields on the order details page.
*
* @param string $address_type Type of address (billing or shipping).
* @param WC_Order $order Order object.
*/
public function render_order_address_fields( $address_type, $order ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->render_additional_fields( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $address_type, 'view' ) );
}
/**
* Renders additional fields on the order details page.
*
* @param WC_Order $order Order object.
*/
public function render_order_additional_fields( $order ) {
$fields = array_merge(
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
);
if ( ! $fields ) {
return;
}
echo '<section class="wc-block-order-confirmation-additional-fields-wrapper">';
echo '<h2>' . esc_html__( 'Additional information', 'woocommerce' ) . '</h2>';
echo $this->render_additional_fields( $fields ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</section>';
}
/**
* Renders address fields on the account page.
*
* @param string $address_type Type of address (billing or shipping).
*/
public function render_address_fields( $address_type ) {
if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) {
return;
}
$customer = new WC_Customer( get_current_user_id() );
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
if ( ! $fields || ! $customer ) {
return;
}
foreach ( $fields as $key => $field ) {
$value = $this->checkout_fields_controller->format_additional_field_value(
$this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type ),
$field
);
if ( ! $value ) {
continue;
}
printf( '<br><strong>%s</strong>: %s', wp_kses_post( $field['label'] ), wp_kses_post( $value ) );
}
}
/**
* Register required additional contact fields.
*
* @param array $fields Required fields.
* @return array
*/
public function edit_account_form_required_fields( $fields ) {
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
foreach ( $additional_fields as $key => $field ) {
if ( ! empty( $field['required'] ) ) {
$fields[ $key ] = $field['label'];
}
}
return $fields;
}
/**
* Adds additional contact fields to the My Account edit account form.
*/
public function edit_account_form_fields() {
$customer = new WC_Customer( get_current_user_id() );
$fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
foreach ( $fields as $key => $field ) {
$form_field = $field;
$form_field['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, 'contact' );
if ( 'select' === $field['type'] ) {
$form_field['options'] = array_column( $field['options'], 'label', 'value' );
}
if ( 'checkbox' === $field['type'] ) {
$form_field['checked_value'] = '1';
$form_field['unchecked_value'] = '0';
}
woocommerce_form_field( $key, $form_field, wc_get_post_data_by_key( $key, $form_field['value'] ) );
}
}
/**
* Validates and saves additional address fields to the customer object on the My Account page.
*
* Customer is not provided by this hook so we handle save here.
*
* @param integer $user_id User ID.
*/
public function save_account_form_fields( $user_id ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing
$customer = new WC_Customer( $user_id );
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
$field_values = array();
foreach ( $additional_fields as $key => $field ) {
if ( ! isset( $_POST[ $key ] ) ) {
continue;
}
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $key ] ) ) );
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
wc_add_notice( $validation->get_error_message(), 'error' );
continue;
}
$field_values[ $key ] = $field_value;
}
// Persist individual additional fields to customer.
foreach ( $field_values as $key => $value ) {
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer );
}
// Validate all fields for this location.
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( $field_values, 'contact' );
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
wc_add_notice( $location_validation->get_error_message(), 'error' );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
$customer->save();
}
/**
* Adds additional address fields to the My Account edit address form.
*
* @param array $address Address fields.
* @param string $address_type Type of address (billing or shipping).
* @return array Updated address fields.
*/
public function edit_address_fields( $address, $address_type ) {
$customer = new WC_Customer( get_current_user_id() );
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
foreach ( $fields as $key => $field ) {
$field_key = "/{$address_type}/{$key}";
$address[ $field_key ] = $field;
$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type );
if ( 'select' === $field['type'] ) {
$address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' );
}
if ( 'checkbox' === $field['type'] ) {
$address[ $field_key ]['checked_value'] = '1';
$address[ $field_key ]['unchecked_value'] = '0';
}
}
return $address;
}
/**
* For the My Account page, save address fields. This uses the Store API endpoint for saving addresses so
* extensibility hooks are consistent across the codebase.
*
* The caller saves the customer object if there are no errors. Nonces are checked before this method executes.
*
* @param integer $user_id User ID.
* @param string $address_type Type of address (billing or shipping).
* @param array $address Address fields.
* @param WC_Customer $customer Customer object.
*/
public function save_address_fields( $user_id, $address_type, $address, $customer ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
$field_values = array();
foreach ( $additional_fields as $key => $field ) {
$post_key = "/{$address_type}/{$key}";
if ( ! isset( $_POST[ $post_key ] ) ) {
continue;
}
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $post_key ] ) ) );
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
wc_add_notice( $validation->get_error_message(), 'error' );
continue;
}
$field_values[ $key ] = $field_value;
}
// Persist individual additional fields to customer.
foreach ( $field_values as $key => $value ) {
$this->checkout_fields_controller->persist_field_for_customer( "/{$address_type}/{$key}", $value, $customer );
}
// Validate all fields for this location.
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( array_merge( $address, $field_values ), 'address', $address_type );
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
wc_add_notice( $location_validation->get_error_message(), 'error' );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
}

View File

@@ -1,16 +1,16 @@
<?php
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) && Package::feature()->is_experimental_build() ) {
if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_field' ) ) {
/**
* Register a checkout field.
*
* @param array $options Field arguments.
* @throws Exception If field registration fails.
* @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details.
* @throws \Exception If field registration fails.
*/
function woocommerce_blocks_register_checkout_field( $options ) {
function __experimental_woocommerce_blocks_register_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
// Check if `woocommerce_blocks_loaded` ran. If not then the CheckoutFields class will not be available yet.
// In that case, re-hook `woocommerce_blocks_loaded` and try running this again.
@@ -19,15 +19,15 @@ if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) && Packag
add_action(
'woocommerce_blocks_loaded',
function() use ( $options ) {
woocommerce_blocks_register_checkout_field( $options );
__experimental_woocommerce_blocks_register_checkout_field( $options );
}
);
return;
}
$checkout_fields = Package::container()->get( \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields::class );
$checkout_fields = Package::container()->get( CheckoutFields::class );
$result = $checkout_fields->register_checkout_field( $options );
if ( is_wp_error( $result ) ) {
throw new Exception( $result->get_error_message() );
throw new \Exception( $result->get_error_message() );
}
}
}

View File

@@ -1,4 +1,6 @@
<?php
use Automattic\Jetpack\Constants;
/**
* Move interactive scripts to the footer. This is a temporary measure to make
* it work with `wc_store` and it should be replaced with deferred scripts or
@@ -36,7 +38,8 @@ function woocommerce_interactivity_register_runtime() {
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file_path ) ) {
$version = filemtime( $file_path );
} else {
$version = \Automattic\WooCommerce\Blocks\Package::get_version();
// Use wc- prefix here to prevent collisions when WC Core version catches up to a version previously used by the WC Blocks feature plugin.
$version = 'wc-' . Constants::get_constant( 'WC_VERSION' );
}
wp_register_script(

View File

@@ -16,6 +16,7 @@ class CheckboxList {
* - id: string of the id to use for the checkbox (optional).
* - checked: boolean to indicate if the checkbox is checked.
* - label: string of the label to display (plaintext or HTML).
* - aria_label: string of the aria label to use for the checkbox. (optional, plaintext only).
* - value: string of the value to use.
* on_change: string of the action to perform when the dropdown changes.
* @return string|false
@@ -35,17 +36,22 @@ class CheckboxList {
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
<div data-wc-context='<?php echo esc_attr( wp_json_encode( $checkbox_list_context ) ); ?>' >
<div class="wc-block-stock-filter style-list">
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
<ul class="wc-block-components-checkbox-list">
<?php foreach ( $items as $item ) { ?>
<?php $item['id'] = $item['id'] ?? uniqid( 'checkbox-' ); ?>
<?php
$item['id'] = $item['id'] ?? uniqid( 'checkbox-' );
// translators: %s: checkbox label.
$i18n_label = sprintf( __( 'Checkbox: %s', 'woocommerce' ), $item['aria_label'] ?? '' );
?>
<li>
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
<div class="wc-block-components-checkbox">
<label for="<?php echo esc_attr( $item['id'] ); ?>">
<input
id="<?php echo esc_attr( $item['id'] ); ?>"
class="wc-block-components-checkbox__input"
type="checkbox"
aria-invalid="false"
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
data-wc-on--change--select-item="actions.selectCheckboxItem"
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
value="<?php echo esc_attr( $item['value'] ); ?>"

View File

@@ -70,7 +70,7 @@ class Migration {
* Rename `checkout` template to `page-checkout`.
*/
public static function wc_blocks_update_1120_rename_checkout_template() {
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//checkout', 'wp_template' );
$template = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//checkout', 'wp_template' );
if ( $template && ! empty( $template->wp_id ) ) {
if ( ! defined( 'WP_POST_REVISIONS' ) ) {
@@ -90,7 +90,7 @@ class Migration {
* Rename `cart` template to `page-cart`.
*/
public static function wc_blocks_update_1120_rename_cart_template() {
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//cart', 'wp_template' );
$template = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//cart', 'wp_template' );
if ( $template && ! empty( $template->wp_id ) ) {
if ( ! defined( 'WP_POST_REVISIONS' ) ) {

View File

@@ -15,7 +15,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
* In the context of this plugin, it handles init and is called from the main
* plugin file (woocommerce-gutenberg-products-block.php).
*
* In the context of WooCommere core, it handles init and is called from
* In the context of WooCommerce core, it handles init and is called from
* WooCommerce's package loader. The main plugin file is _not_ loaded.
*
* @since 2.5.0

View File

@@ -70,8 +70,8 @@ final class QueryFilters {
$query_vars['posts_per_page'] = -1;
$query_vars['fields'] = 'ids';
$query = new \WP_Query();
$result = $query->query( $query_vars );
$product_query_sql = $query->request;
$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' );
@@ -136,8 +136,8 @@ final class QueryFilters {
$query_vars['posts_per_page'] = -1;
$query_vars['fields'] = 'ids';
$query = new \WP_Query();
$result = $query->query( $query_vars );
$product_query_sql = $query->request;
$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' );

View File

@@ -77,7 +77,6 @@ class ShippingController {
add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 );
add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
add_filter( 'woocommerce_shipping_chosen_method', array( $this, 'prevent_shipping_method_selection_changes' ), 20, 3 );
// This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function
// returns based on the option's value in the DB and we can't override it any other way.
@@ -86,29 +85,6 @@ class ShippingController {
add_action( 'rest_pre_serve_request', array( $this, 'track_local_pickup' ), 10, 4 );
}
/**
* Prevent changes in the selected shipping method when new rates are added or removed.
*
* If the chosen method exists within package rates, it is returned to maintain the selection.
* Otherwise, the default rate is returned.
*
* @param string $default Default shipping method.
* @param array $package_rates Associative array of available package rates.
* @param string $chosen_method Previously chosen shipping method.
*
* @return string Chosen shipping method or default.
*/
public function prevent_shipping_method_selection_changes( $default, $package_rates, $chosen_method ) {
// If the chosen method exists in the package rates, return it.
if ( $chosen_method && isset( $package_rates[ $chosen_method ] ) ) {
return $chosen_method;
}
// Otherwise, return the default method.
return $default;
}
/**
* Overrides the option to force shipping calculations NOT to wait until an address is entered, but only if the
* Checkout page contains the Checkout Block.

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* CartTemplate class.
*
@@ -34,11 +32,6 @@ class CartTemplate extends AbstractPageTemplate {
* @return boolean
*/
protected function is_active_template() {
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
return false;
}
global $post;
$placeholder = $this->get_placeholder_page();
return null !== $placeholder && $post instanceof \WP_Post && $placeholder->post_name === $post->post_name;

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* CheckoutTemplate class.
*
@@ -34,11 +32,6 @@ class CheckoutTemplate extends AbstractPageTemplate {
* @return boolean
*/
protected function is_active_template() {
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
return false;
}
global $post;
$placeholder = $this->get_placeholder_page();
return null !== $placeholder && $post instanceof \WP_Post && $placeholder->post_name === $post->post_name;

View File

@@ -1,129 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Utils;
/**
* Utility methods used for migrating pages to block templates.
* {@internal This class and its methods should only be used within the BlockTemplateController.php and is not intended for public use.}
*/
class BlockTemplateMigrationUtils {
/**
* Check if a page has been migrated to a template.
*
* @param string $page_id Page ID.
* @return boolean
*/
public static function has_migrated_page( $page_id ) {
return (bool) get_option( 'has_migrated_' . $page_id, false );
}
/**
* Stores an option to indicate that a template has been migrated.
*
* @param string $page_id Page ID.
* @param string $status Status of the migration.
*/
public static function set_has_migrated_page( $page_id, $status = 'success' ) {
update_option( 'has_migrated_' . $page_id, $status );
}
/**
* Migrates a page to a template if needed.
*
* @param string $template_slug Template slug.
*/
public static function migrate_page( $template_slug ) {
// Get the block template for this page. If it exists, we won't migrate because the user already has custom content.
$block_template = BlockTemplateUtils::get_block_template( 'woocommerce/woocommerce//page-' . $template_slug, 'wp_template' );
// If we were unable to get the block template, bail. Try again later.
if ( ! $block_template ) {
return;
}
// Skip migration if the theme has a template file for this page.
$theme_template = BlockTemplateUtils::get_block_template( get_stylesheet() . '//page-' . $template_slug, 'wp_template' );
if ( $theme_template ) {
return self::set_has_migrated_page( $template_slug, 'theme-file-exists' );
}
// If a custom template is present already, no need to migrate.
if ( $block_template->wp_id ) {
return self::set_has_migrated_page( $template_slug, 'custom-template-exists' );
}
$template_content = self::get_default_template( $template_slug );
if ( self::create_custom_template( $block_template, $template_content ) ) {
return self::set_has_migrated_page( $template_slug );
}
}
/**
* Prepare default page template.
*
* @param string $template_slug Template slug.
* @return string
*/
protected static function get_default_template( $template_slug ) {
$default_template_content = '
<!-- wp:group {"layout":{"inherit":true,"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:woocommerce/page-content-wrapper {"page":"' . $template_slug . '"} -->
<!-- wp:post-title {"align":"wide", "level":1} /-->
<!-- wp:post-content {"align":"wide"} /-->
<!-- /wp:woocommerce/page-content-wrapper --></div>
<!-- /wp:group -->
';
return self::get_block_template_part( 'header' ) . $default_template_content . self::get_block_template_part( 'footer' );
}
/**
* Create a custom template with given content.
*
* @param \WP_Block_Template|null $template Template object.
* @param string $content Template content.
* @return boolean Success.
*/
protected static function create_custom_template( $template, $content ) {
$term = get_term_by( 'slug', $template->theme, 'wp_theme', ARRAY_A );
if ( ! $term ) {
$term = wp_insert_term( $template->theme, 'wp_theme' );
}
$template_id = wp_insert_post(
[
'post_name' => $template->slug,
'post_type' => 'wp_template',
'post_status' => 'publish',
'tax_input' => array(
'wp_theme' => $template->theme,
),
'meta_input' => array(
'origin' => $template->source,
),
'post_content' => $content,
],
true
);
wp_set_post_terms( $template_id, array( $term['term_id'] ), 'wp_theme' );
return $template_id && ! is_wp_error( $template_id );
}
/**
* Returns the requested template part.
*
* @param string $part The part to return.
* @return string
*/
protected static function get_block_template_part( $part ) {
$template_part = BlockTemplateUtils::get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' );
if ( ! $template_part || empty( $template_part->content ) ) {
return '';
}
return $template_part->content;
}
}

View File

@@ -492,28 +492,6 @@ class BlockTemplateUtils {
return false;
}
/**
* Retrieves a single unified template object using its id.
*
* @param string $id Template unique identifier (example: theme_slug//template_slug).
* @param string $template_type Optional. Template type: `wp_template` or 'wp_template_part`.
* Default `wp_template`.
*
* @return WP_Block_Template|null Template.
*/
public static function get_block_template( $id, $template_type ) {
if ( function_exists( 'get_block_template' ) ) {
return get_block_template( $id, $template_type );
}
if ( function_exists( 'gutenberg_get_block_template' ) ) {
return gutenberg_get_block_template( $id, $template_type );
}
return null;
}
/**
* Checks if we can fall back to the `archive-product` template for a given slug.
*
@@ -621,35 +599,6 @@ class BlockTemplateUtils {
return false;
}
/**
* Filter block templates by feature flag.
*
* @param WP_Block_Template[] $block_templates An array of block template objects.
*
* @return WP_Block_Template[] An array of block template objects.
*/
public static function filter_block_templates_by_feature_flag( $block_templates ) {
$feature_gating = new FeatureGating();
$flag = $feature_gating->get_flag();
/**
* An array of block templates with slug as key and flag as value.
*
* @var array
*/
$block_templates_with_feature_gate = array();
return array_filter(
$block_templates,
function( $block_template ) use ( $flag, $block_templates_with_feature_gate ) {
if ( isset( $block_templates_with_feature_gate[ $block_template->slug ] ) ) {
return $block_templates_with_feature_gate[ $block_template->slug ] <= $flag;
}
return true;
}
);
}
/**
* Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
*
@@ -690,20 +639,48 @@ class BlockTemplateUtils {
);
}
/**
* Removes customized templates that shouldn't be available. That means customized templates based on the
* WooCommerce default template when there is a customized template based on the theme template.
*
* @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
* @param string $theme_slug Slug of the theme currently active.
*
* @return array Filtered list of templates with only relevant templates available.
*/
public static function remove_duplicate_customized_templates( $templates, $theme_slug ) {
$filtered_templates = array_filter(
$templates,
function( $template ) use ( $templates, $theme_slug ) {
if ( $template->theme === $theme_slug ) {
// This is a customized template based on the theme template, so it should be returned.
return true;
}
// This is a template customized from the WooCommerce default template.
// Only return it if there isn't a customized version of the theme template.
$is_there_a_customized_theme_template = array_filter(
$templates,
function( $theme_template ) use ( $template, $theme_slug ) {
return $theme_template->slug === $template->slug && $theme_template->theme === $theme_slug;
}
);
if ( $is_there_a_customized_theme_template ) {
return false;
}
return true;
},
);
return $filtered_templates;
}
/**
* Returns whether the blockified templates should be used or not.
* First, we need to make sure WordPress version is higher than 6.1 (lowest that supports Products block).
* Then, if the option is not stored on the db, we need to check if the current theme is a block one or not.
* If the option is not stored on the db, we need to check if the current theme is a block one or not.
*
* @return boolean
*/
public static function should_use_blockified_product_grid_templates() {
$minimum_wp_version = '6.1';
if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) {
return false;
}
$use_blockified_templates = get_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE );
if ( false === $use_blockified_templates ) {
@@ -785,7 +762,7 @@ class BlockTemplateUtils {
$theme_has_template = self::theme_has_template_part( $slug );
$template_slug_to_load = $theme_has_template ? get_stylesheet() : self::PLUGIN_SLUG;
}
$template_part = self::get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
$template_part = get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
return $template_part->content;

View File

@@ -31,4 +31,65 @@ class ProductCollectionUtils {
return $query;
}
/**
* Helper function that constructs a WP_Query args array from
* a Product Collection or global query.
*
* @param WP_Block $block Block instance.
* @param int $page Current query's page.
*
* @return array Returns the constructed WP_Query arguments.
*/
public static function get_query_vars( $block, $page ) {
if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
return build_query_vars_from_query_block( $block, $page );
}
global $wp_query;
return array_filter( $wp_query->query_vars );
}
/**
* 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
*/
public static function remove_query_array( $queries, $key, $value ) {
if ( ! is_array( $queries ) || 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'] ) || ! isset( $query[ $key ] ) ) {
$queries[ $query_key ] = self::remove_query_array( $query, $key, $value );
}
}
return self::remove_empty_array_recursive( $queries );
}
/**
* Remove falsy item from array, recursively.
*
* @param array $array The input array to filter.
*/
private static function remove_empty_array_recursive( $array ) {
$array = array_filter( $array );
foreach ( $array as $key => $item ) {
if ( is_array( $item ) ) {
$array[ $key ] = self::remove_empty_array_recursive( $item );
}
}
return $array;
}
}