Merged in feature/117-dev-dev01 (pull request #8)
auto-patch 117-dev-dev01-2023-12-15T16_09_06 * auto-patch 117-dev-dev01-2023-12-15T16_09_06
This commit is contained in:
@@ -64,6 +64,9 @@ final class AssetsController {
|
||||
$this->api->register_script( 'wc-blocks-checkout', 'build/blocks-checkout.js', [] );
|
||||
$this->api->register_script( 'wc-blocks-components', 'build/blocks-components.js', [] );
|
||||
|
||||
// Register the interactivity components here for now.
|
||||
$this->api->register_script( 'wc-interactivity-dropdown', 'build/wc-interactivity-dropdown.js', [] );
|
||||
|
||||
wp_add_inline_script(
|
||||
'wc-blocks-middleware',
|
||||
"
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\Blocks;
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
|
||||
@@ -33,8 +34,9 @@ use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
* @internal
|
||||
*/
|
||||
class BlockPatterns {
|
||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
|
||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||
const COMMA_SEPARATED_REGEX = '/[\s,]+/';
|
||||
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
||||
|
||||
/**
|
||||
* Path to the patterns directory.
|
||||
@@ -92,6 +94,22 @@ class BlockPatterns {
|
||||
return;
|
||||
}
|
||||
|
||||
register_post_type(
|
||||
self::PATTERNS_AI_DATA_POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
'singular_name' => __( 'Patterns AI Data', 'woocommerce' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'delete_with_user' => false,
|
||||
'can_export' => true,
|
||||
)
|
||||
);
|
||||
|
||||
$default_headers = array(
|
||||
'title' => 'Title',
|
||||
'slug' => 'Slug',
|
||||
@@ -112,6 +130,8 @@ class BlockPatterns {
|
||||
return;
|
||||
}
|
||||
|
||||
$dictionary = PatternsHelper::get_patterns_dictionary();
|
||||
|
||||
foreach ( $files as $file ) {
|
||||
$pattern_data = get_file_data( $file, $default_headers );
|
||||
|
||||
@@ -209,10 +229,27 @@ class BlockPatterns {
|
||||
$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woo-gutenberg-products-block' );
|
||||
}
|
||||
|
||||
$pattern_data_from_dictionary = $this->get_pattern_from_dictionary( $dictionary, $pattern_data['slug'] );
|
||||
|
||||
// The actual pattern content is the output of the file.
|
||||
ob_start();
|
||||
|
||||
/*
|
||||
For patterns that can have AI-generated content, we need to get its content from the dictionary and pass
|
||||
it to the pattern file through the "$content" and "$images" variables.
|
||||
This is to avoid having to access the dictionary for each pattern when it's registered or inserted.
|
||||
Before the "$content" and "$images" variables were populated in each pattern. Since the pattern
|
||||
registration happens in the init hook, the dictionary was being access one for each pattern and
|
||||
for each page load. This way we only do it once on registration.
|
||||
For more context: https://github.com/woocommerce/woocommerce-blocks/pull/11733
|
||||
*/
|
||||
if ( ! is_null( $pattern_data_from_dictionary ) ) {
|
||||
$content = $pattern_data_from_dictionary['content'];
|
||||
$images = $pattern_data_from_dictionary['images'] ?? array();
|
||||
}
|
||||
include $file;
|
||||
$pattern_data['content'] = ob_get_clean();
|
||||
|
||||
if ( ! $pattern_data['content'] ) {
|
||||
continue;
|
||||
}
|
||||
@@ -250,7 +287,7 @@ class BlockPatterns {
|
||||
* @param array $options Array of bulk item update data.
|
||||
*/
|
||||
public function schedule_on_plugin_update( $upgrader_object, $options ) {
|
||||
if ( 'update' === $options['action'] && 'plugin' === $options['type'] ) {
|
||||
if ( 'update' === $options['action'] && 'plugin' === $options['type'] && isset( $options['plugins'] ) ) {
|
||||
foreach ( $options['plugins'] as $plugin ) {
|
||||
if ( str_contains( $plugin, 'woocommerce-gutenberg-products-block.php' ) || str_contains( $plugin, 'woocommerce.php' ) ) {
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
@@ -337,4 +374,22 @@ class BlockPatterns {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the patterns dictionary to get the pattern data corresponding to the pattern slug.
|
||||
*
|
||||
* @param array $dictionary The patterns dictionary.
|
||||
* @param string $slug The pattern slug.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function get_pattern_from_dictionary( $dictionary, $slug ) {
|
||||
foreach ( $dictionary as $pattern_dictionary ) {
|
||||
if ( $pattern_dictionary['slug'] === $slug ) {
|
||||
return $pattern_dictionary;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +183,22 @@ class BlockTemplatesController {
|
||||
* @return object|null
|
||||
*/
|
||||
public function get_block_template_fallback( $template, $id, $template_type ) {
|
||||
$template_name_parts = explode( '//', $id );
|
||||
list( $theme, $slug ) = $template_name_parts;
|
||||
// Add protection against invalid ids.
|
||||
if ( ! is_string( $id ) || ! strstr( $id, '//' ) ) {
|
||||
return null;
|
||||
}
|
||||
// Add protection against invalid template types.
|
||||
if (
|
||||
'wp_template' !== $template_type &&
|
||||
'wp_template_part' !== $template_type
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$template_name_parts = explode( '//', $id );
|
||||
$theme = $template_name_parts[0] ?? '';
|
||||
$slug = $template_name_parts[1] ?? '';
|
||||
|
||||
if ( ! BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $slug ) ) {
|
||||
if ( empty( $theme ) || empty( $slug ) || ! BlockTemplateUtils::template_is_eligible_for_product_archive_fallback( $slug ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class AddToCartForm extends AbstractBlock {
|
||||
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
|
||||
|
||||
$form = sprintf(
|
||||
'<div class="wp-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>',
|
||||
'<div class="wp-block-add-to-cart-form wc-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>',
|
||||
esc_attr( $classes_and_styles['classes'] ),
|
||||
esc_attr( $classname ),
|
||||
esc_attr( $product_classname ),
|
||||
|
||||
@@ -67,7 +67,7 @@ class ClassicShortcode extends AbstractDynamicBlock {
|
||||
* @return string space-separated list of classes.
|
||||
*/
|
||||
protected function get_container_classes( $attributes = array() ) {
|
||||
$classes = array( 'wp-block-group' );
|
||||
$classes = array( 'woocommerce', 'wp-block-group' );
|
||||
|
||||
if ( isset( $attributes['align'] ) ) {
|
||||
$classes[] = "align{$attributes['align']}";
|
||||
|
||||
@@ -15,18 +15,6 @@ final class CollectionFilters extends AbstractBlock {
|
||||
*/
|
||||
protected $block_name = 'collection-filters';
|
||||
|
||||
/**
|
||||
* Mapping inner blocks to CollectionData API parameters.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $collection_data_params_mapping = array(
|
||||
'calculate_price_range' => 'woocommerce/collection-price-filter',
|
||||
'calculate_stock_status_counts' => 'woocommerce/collection-stock-filter',
|
||||
'calculate_attribute_counts' => 'woocommerce/collection-attribute-filter',
|
||||
'calculate_rating_counts' => 'woocommerce/collection-rating-filter',
|
||||
);
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
@@ -43,17 +31,6 @@ final class CollectionFilters extends AbstractBlock {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @param string $key Data to get, or default to everything.
|
||||
*
|
||||
* @return null This block has no frontend script.
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
@@ -98,22 +75,13 @@ final class CollectionFilters extends AbstractBlock {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bail if the current block is not a direct child of CollectionFilters
|
||||
* and the parent block doesn't have our custom context.
|
||||
* When the first direct child of Collection Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" !== $parent_block->name &&
|
||||
empty( $parent_block->context['isCollectionFiltersInnerBlock'] )
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first time we reach here, WP is rendering the first direct child
|
||||
* of CollectionFilters block. We hydrate and cache the collection data
|
||||
* response for other inner blocks to use.
|
||||
*/
|
||||
if ( ! isset( $this->current_response ) ) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
@@ -122,18 +90,10 @@ final class CollectionFilters extends AbstractBlock {
|
||||
}
|
||||
|
||||
/**
|
||||
* We target only filter blocks, but they can be nested inside other
|
||||
* blocks like Group/Row for layout purposes. We pass this custom light
|
||||
* weight context (instead of full CollectionData response) to all inner
|
||||
* blocks of current CollectionFilters to find and iterate inner filter
|
||||
* blocks.
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
$context['isCollectionFiltersInnerBlock'] = true;
|
||||
|
||||
if (
|
||||
isset( $parsed_block['blockName'] ) &&
|
||||
in_array( $parsed_block['blockName'], $this->collection_data_params_mapping, true )
|
||||
) {
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
@@ -148,25 +108,16 @@ final class CollectionFilters extends AbstractBlock {
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$inner_blocks = $this->get_inner_blocks_recursive( $block->inner_blocks );
|
||||
|
||||
$collection_data_params = array_map(
|
||||
function( $block_name ) use ( $inner_blocks ) {
|
||||
return in_array( $block_name, $inner_blocks, true );
|
||||
},
|
||||
$this->collection_data_params_mapping
|
||||
);
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$products_params = $this->get_formatted_products_params( $block->context['query'] );
|
||||
|
||||
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data(
|
||||
add_query_arg(
|
||||
array_merge(
|
||||
$products_params,
|
||||
$this->get_formatted_products_params( $block->context['query'] ),
|
||||
$collection_data_params,
|
||||
),
|
||||
'/wc/store/v1/products/collection-data'
|
||||
@@ -174,7 +125,7 @@ final class CollectionFilters extends AbstractBlock {
|
||||
);
|
||||
|
||||
if ( ! empty( $response['body'] ) ) {
|
||||
return $response['body'];
|
||||
return json_decode( wp_json_encode( $response['body'] ), true );
|
||||
}
|
||||
|
||||
return array();
|
||||
@@ -188,11 +139,13 @@ final class CollectionFilters extends AbstractBlock {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_blocks_recursive( $inner_blocks, &$results = 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 ) {
|
||||
$results[] = $inner_block->name;
|
||||
$this->get_inner_blocks_recursive(
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$results = array_merge( $results, $inner_block->attributes['queryParam'] );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
@@ -219,37 +172,18 @@ final class CollectionFilters extends AbstractBlock {
|
||||
/**
|
||||
* The following params can be passed directly to Store API endpoints.
|
||||
*/
|
||||
$shared_params = array( 'exclude', 'offset', 'order', 'serach' );
|
||||
array_walk(
|
||||
$shared_params,
|
||||
function( $key ) use ( $query, &$params ) {
|
||||
$params[ $key ] = $query[ $key ] ?? '';
|
||||
}
|
||||
);
|
||||
$shared_params = array( 'exclude', 'offset', 'search' );
|
||||
|
||||
/**
|
||||
* The following params just need to transform the key, their value can
|
||||
* be passed as it is to the Store API.
|
||||
*/
|
||||
$mapped_params = array(
|
||||
'orderBy' => 'orderby',
|
||||
'pages' => 'page',
|
||||
'perPage' => 'per_page',
|
||||
'woocommerceStockStatus' => 'stock_status',
|
||||
'woocommerceOnSale' => 'on_sale',
|
||||
'woocommerceHandPickedProducts' => 'include',
|
||||
);
|
||||
array_walk(
|
||||
$mapped_params,
|
||||
function( $mapped_key, $original_key ) use ( $query, &$params ) {
|
||||
$params[ $mapped_key ] = $query[ $original_key ] ?? '';
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* The value of taxQuery and woocommerceAttributes need additional
|
||||
* transformation to the shape that Store API accepts.
|
||||
*/
|
||||
$taxonomy_mapper = function( $key ) {
|
||||
$mapping = array(
|
||||
'product_tag' => 'tag',
|
||||
@@ -259,34 +193,61 @@ final class CollectionFilters extends AbstractBlock {
|
||||
return $mapping[ $key ] ?? '_unstable_tax_' . $key;
|
||||
};
|
||||
|
||||
if ( is_array( $query['taxQuery'] ) ) {
|
||||
array_walk(
|
||||
$query['taxQuery'],
|
||||
function( $terms, $taxonomy ) use ( $taxonomy_mapper, &$params ) {
|
||||
$params[ $taxonomy_mapper( $taxonomy ) ] = implode( ',', $terms );
|
||||
array_walk(
|
||||
$query,
|
||||
function( $value, $key ) use ( $shared_params, $mapped_params, $taxonomy_mapper, &$params ) {
|
||||
if ( in_array( $key, $shared_params, true ) ) {
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_array( $query['woocommerceAttributes'] ) ) {
|
||||
array_walk(
|
||||
$query['woocommerceAttributes'],
|
||||
function( $attribute ) use ( &$params ) {
|
||||
$params['attributes'][] = array(
|
||||
'attribute' => $attribute['taxonomy'],
|
||||
'term_id' => $attribute['termId'],
|
||||
if ( in_array( $key, array_keys( $mapped_params ), true ) ) {
|
||||
$params[ $mapped_params[ $key ] ] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of taxQuery and woocommerceAttributes need additional
|
||||
* transformation to the shape that Store API accepts.
|
||||
*/
|
||||
if ( 'taxQuery' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $terms, $taxonomy ) use ( $taxonomy_mapper, &$params ) {
|
||||
$params[ $taxonomy_mapper( $taxonomy ) ] = implode( ',', $terms );
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'woocommerceAttributes' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $attribute ) use ( &$params ) {
|
||||
$params['attributes'][] = array(
|
||||
'attribute' => $attribute['taxonomy'],
|
||||
'term_id' => $attribute['termId'],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product Collection determines the product visibility based on stock
|
||||
* statuses. We need to pass the catalog_visibility param to the Store
|
||||
* API to make sure the product visibility is correct.
|
||||
*/
|
||||
$params['catalog_visibility'] = is_search() ? 'catalog' : 'visible';
|
||||
$params['catalog_visibility'] = is_search() ? 'search' : 'visible';
|
||||
|
||||
return array_filter( $params );
|
||||
/**
|
||||
* `false` values got removed from `add_query_arg`, so we need to convert
|
||||
* them to numeric.
|
||||
*/
|
||||
return array_map(
|
||||
function( $param ) {
|
||||
return is_bool( $param ) ? +$param : $param;
|
||||
},
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* CollectionPriceFilter class.
|
||||
*/
|
||||
@@ -27,24 +25,31 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
// Short circuit if the collection data isn't ready yet.
|
||||
if ( empty( $block->context['collectionData']['price_range'] ) ) {
|
||||
if (
|
||||
is_admin() ||
|
||||
empty( $block->context['collectionData'] ) ||
|
||||
empty( $block->context['collectionData']['price_range'] )
|
||||
) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$price_range = $block->context['collectionData']['price_range'];
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
$min_range = $price_range->min_price / 10 ** $price_range->currency_minor_unit;
|
||||
$max_range = $price_range->max_price / 10 ** $price_range->currency_minor_unit;
|
||||
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
|
||||
$max_price = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
$min_range = $price_range['min_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$max_range = $price_range['max_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
|
||||
$max_price = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
|
||||
$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
|
||||
$formatted_max_price = wc_price( $max_price, array( 'decimals' => 0 ) );
|
||||
|
||||
$data = array(
|
||||
'minPrice' => $min_price,
|
||||
'maxPrice' => $max_price,
|
||||
'minRange' => $min_range,
|
||||
'maxRange' => $max_range,
|
||||
'minPrice' => $min_price,
|
||||
'maxPrice' => $max_price,
|
||||
'minRange' => $min_range,
|
||||
'maxRange' => $max_range,
|
||||
'formattedMinPrice' => $formatted_min_price,
|
||||
'formattedMaxPrice' => $formatted_max_price,
|
||||
);
|
||||
|
||||
wc_store(
|
||||
@@ -55,55 +60,26 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
)
|
||||
);
|
||||
|
||||
$filter_reset_button = sprintf(
|
||||
' <button class="wc-block-components-filter-reset-button" data-wc-on--click="actions.filters.reset">
|
||||
<span aria-hidden="true">%1$s</span>
|
||||
<span class="screen-reader-text">%2$s</span>
|
||||
</button>',
|
||||
__( 'Reset', 'woocommerce' ),
|
||||
__( 'Reset filter', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="controls">%2$s</div>
|
||||
<div class="actions">
|
||||
%3$s
|
||||
</div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$this->get_price_slider( $data, $attributes ),
|
||||
$filter_reset_button
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price slider HTML.
|
||||
*
|
||||
* @param array $store_data The data passing to Interactivity Store.
|
||||
* @param array $attributes Block attributes.
|
||||
*/
|
||||
private function get_price_slider( $store_data, $attributes ) {
|
||||
list (
|
||||
'showInputFields' => $show_input_fields,
|
||||
'inlineInput' => $inline_input
|
||||
) = $attributes;
|
||||
list (
|
||||
'minPrice' => $min_price,
|
||||
'maxPrice' => $max_price,
|
||||
'minRange' => $min_range,
|
||||
'maxRange' => $max_range,
|
||||
) = $store_data;
|
||||
|
||||
// Max range shouldn't be 0.
|
||||
if ( ! $max_range ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// CSS variables for the range bar style.
|
||||
$__low = 100 * $min_price / $max_range;
|
||||
$__high = 100 * $max_price / $max_range;
|
||||
$__low = 100 * ( $min_price - $min_range ) / ( $max_range - $min_range );
|
||||
$__high = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
|
||||
$range_style = "--low: $__low%; --high: $__high%";
|
||||
|
||||
$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
|
||||
$formatted_max_price = wc_price( $max_price, array( 'decimals' => 0 ) );
|
||||
|
||||
$classes = $show_input_fields && $inline_input ? 'price-slider inline-input' : 'price-slider';
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
)
|
||||
);
|
||||
|
||||
$price_min = $show_input_fields ?
|
||||
sprintf(
|
||||
@@ -139,7 +115,7 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $classes ); ?>">
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<div
|
||||
class="range"
|
||||
style="<?php echo esc_attr( $range_style ); ?>"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
|
||||
/**
|
||||
* CollectionStockFilter class.
|
||||
*/
|
||||
final class CollectionStockFilter extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-stock-filter';
|
||||
|
||||
const STOCK_STATUS_QUERY_VAR = 'filter_stock_status';
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $stock_statuses Any stock statuses 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 $stock_statuses = [] ) {
|
||||
parent::enqueue_data( $stock_statuses );
|
||||
$this->asset_data_registry->add( 'stockStatusOptions', wc_get_product_stock_status_options(), true );
|
||||
$this->asset_data_registry->add( 'hideOutOfStockItems', 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include and render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @param string $content Block content. Default empty string.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$stock_status_counts = $block->context['collectionData']['stock_status_counts'] ?? [];
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
|
||||
wc_store(
|
||||
array(
|
||||
'state' => array(
|
||||
'filters' => array(
|
||||
'stockStatus' => $stock_status_counts,
|
||||
'activeFilters' => '',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-stock-filter__controls">%2$s</div>
|
||||
<div class="wc-block-stock-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$this->get_stock_filter_html( $stock_status_counts, $attributes ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stock filter HTML
|
||||
*
|
||||
* @param array $stock_counts An array of stock counts.
|
||||
* @param array $attributes Block attributes. Default empty array.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
private function get_stock_filter_html( $stock_counts, $attributes ) {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$stock_statuses = wc_get_product_stock_status_options();
|
||||
|
||||
// check the url params to select initial item on page load.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_stock_status = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$list_items = array_map(
|
||||
function( $item ) use ( $stock_statuses ) {
|
||||
return array(
|
||||
'label' => $stock_statuses[ $item['status'] ],
|
||||
'value' => $item['status'],
|
||||
);
|
||||
},
|
||||
$stock_counts
|
||||
);
|
||||
|
||||
$selected_items = array_values(
|
||||
array_filter(
|
||||
$list_items,
|
||||
function( $item ) use ( $selected_stock_status ) {
|
||||
return $item['value'] === $selected_stock_status;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Just for the dropdown, we can only select 1 item.
|
||||
$selected_item = $selected_items[0] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<?php if ( 'list' === $display_style ) : ?>
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $stock_counts as $stock_count ) { ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $stock_count['status'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.filters.updateProducts"
|
||||
value="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
<?php checked( strpos( $selected_stock_status, $stock_count['status'] ) !== false, 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php echo esc_html( $stock_statuses[ $stock_count['status'] ] ); ?>
|
||||
<?php
|
||||
// translators: %s: number of products.
|
||||
$screen_reader_text = sprintf( _n( '%s product', '%s products', $stock_count['count'], 'woocommerce' ), number_format_i18n( $stock_count['count'] ) );
|
||||
?>
|
||||
<span class="wc-filter-element-label-list-count">
|
||||
<span aria-hidden="true">
|
||||
<?php echo esc_html( $stock_count['count'] ); ?>
|
||||
</span>
|
||||
<span class="screen-reader-text">
|
||||
<?php esc_html( $screen_reader_text ); ?>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'dropdown' === $display_style ) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dropdown::render() escapes output.
|
||||
echo Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'actions.filters.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ class MiniCart extends AbstractBlock {
|
||||
public function print_lazy_load_scripts() {
|
||||
$script_data = $this->asset_api->get_script_data( 'build/mini-cart-component-frontend.js' );
|
||||
|
||||
$num_dependencies = count( $script_data['dependencies'] );
|
||||
$num_dependencies = is_countable( $script_data['dependencies'] ) ? count( $script_data['dependencies'] ) : 0;
|
||||
$wp_scripts = wp_scripts();
|
||||
|
||||
for ( $i = 0; $i < $num_dependencies; $i++ ) {
|
||||
@@ -307,7 +307,7 @@ class MiniCart extends AbstractBlock {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( count( $script->deps ) ) {
|
||||
if ( is_countable( $script->deps ) && count( $script->deps ) ) {
|
||||
foreach ( $script->deps as $dep ) {
|
||||
if ( ! array_key_exists( $dep, $this->scripts_to_lazy_load ) ) {
|
||||
$dep_script = $this->get_script_from_handle( $dep );
|
||||
@@ -453,7 +453,7 @@ class MiniCart extends AbstractBlock {
|
||||
|
||||
// Determine if we need to load the template part from the DB, the theme or WooCommerce in that order.
|
||||
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( array( 'mini-cart' ), 'wp_template_part' );
|
||||
if ( count( $templates_from_db ) > 0 ) {
|
||||
if ( is_countable( $templates_from_db ) && count( $templates_from_db ) > 0 ) {
|
||||
$template_slug_to_load = $templates_from_db[0]->theme;
|
||||
} else {
|
||||
$theme_has_mini_cart = BlockTemplateUtils::theme_has_template_part( 'mini-cart' );
|
||||
|
||||
@@ -77,11 +77,25 @@ class ProductButton extends AbstractBlock {
|
||||
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
wc_store(
|
||||
array(
|
||||
'state' => array(
|
||||
'woocommerce' => array(
|
||||
'inTheCartText' => sprintf(
|
||||
/* translators: %s: product number. */
|
||||
__( '%s in cart', 'woocommerce' ),
|
||||
'###'
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $product ) {
|
||||
$number_of_items_in_cart = $this->get_cart_item_quantities_by_product_id( $product->get_id() );
|
||||
$more_than_one_item = $number_of_items_in_cart > 0;
|
||||
$initial_product_text = $more_than_one_item ? sprintf(
|
||||
/* translators: %s: product number. */
|
||||
/* translators: %s: product number. */
|
||||
__( '%s in cart', 'woocommerce' ),
|
||||
$number_of_items_in_cart
|
||||
) : $product->add_to_cart_text();
|
||||
@@ -108,20 +122,6 @@ class ProductButton extends AbstractBlock {
|
||||
)
|
||||
);
|
||||
|
||||
wc_store(
|
||||
array(
|
||||
'state' => array(
|
||||
'woocommerce' => array(
|
||||
'inTheCartText' => sprintf(
|
||||
/* translators: %s: product number. */
|
||||
__( '%s in cart', 'woocommerce' ),
|
||||
'###'
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$default_quantity = 1;
|
||||
|
||||
$context = array(
|
||||
|
||||
@@ -207,7 +207,11 @@ class ProductCollection extends AbstractBlock {
|
||||
$stock_status = $request->get_param( 'woocommerceStockStatus' );
|
||||
$product_attributes = $request->get_param( 'woocommerceAttributes' );
|
||||
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
|
||||
$args['author'] = $request->get_param( 'author' ) ?? '';
|
||||
$featured = $request->get_param( 'featured' );
|
||||
$time_frame = $request->get_param( 'timeFrame' );
|
||||
// This argument is required for the tests to PHP Unit Tests to run correctly.
|
||||
// Most likely this argument is being accessed in the test environment image.
|
||||
$args['author'] = '';
|
||||
|
||||
return $this->get_final_query_args(
|
||||
$args,
|
||||
@@ -217,6 +221,8 @@ class ProductCollection extends AbstractBlock {
|
||||
'stock_status' => $stock_status,
|
||||
'product_attributes' => $product_attributes,
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $featured,
|
||||
'timeFrame' => $time_frame,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -297,12 +303,13 @@ class ProductCollection extends AbstractBlock {
|
||||
'tax_query' => array(),
|
||||
'paged' => $page,
|
||||
's' => $query['search'],
|
||||
'author' => $query['author'] ?? '',
|
||||
);
|
||||
|
||||
$is_on_sale = $query['woocommerceOnSale'] ?? false;
|
||||
$product_attributes = $query['woocommerceAttributes'] ?? [];
|
||||
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
|
||||
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? [];
|
||||
$time_frame = $query['timeFrame'] ?? null;
|
||||
|
||||
$final_query = $this->get_final_query_args(
|
||||
$common_query_values,
|
||||
@@ -310,9 +317,11 @@ class ProductCollection extends AbstractBlock {
|
||||
'on_sale' => $is_on_sale,
|
||||
'stock_status' => $query['woocommerceStockStatus'],
|
||||
'orderby' => $query['orderBy'],
|
||||
'product_attributes' => $query['woocommerceAttributes'],
|
||||
'product_attributes' => $product_attributes,
|
||||
'taxonomies_query' => $taxonomies_query,
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $query['featured'] ?? false,
|
||||
'timeFrame' => $time_frame,
|
||||
),
|
||||
$is_exclude_applied_filters
|
||||
);
|
||||
@@ -333,14 +342,16 @@ class ProductCollection extends AbstractBlock {
|
||||
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
|
||||
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
|
||||
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query ) : [];
|
||||
$featured_query = $this->get_featured_query( $query['featured'] ?? false );
|
||||
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
|
||||
$taxonomies_query = $query['taxonomies_query'] ?? [];
|
||||
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query );
|
||||
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
|
||||
$date_query = $this->get_date_query( $query['timeFrame'] ?? [] );
|
||||
|
||||
// We exclude applied filters to generate product ids for the filter blocks.
|
||||
$applied_filters_query = $is_exclude_applied_filters ? [] : $this->get_queries_by_applied_filters();
|
||||
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query );
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query );
|
||||
|
||||
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||
}
|
||||
@@ -390,6 +401,7 @@ class ProductCollection extends AbstractBlock {
|
||||
*/
|
||||
if (
|
||||
! empty( $merged_query['post__in'] ) &&
|
||||
is_array( $merged_query['post__in'] ) &&
|
||||
count( $merged_query['post__in'] ) > count( array_unique( $merged_query['post__in'] ) )
|
||||
) {
|
||||
$merged_query['post__in'] = array_unique(
|
||||
@@ -626,6 +638,35 @@ class ProductCollection extends AbstractBlock {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tax query to filter products based on their "featured" status.
|
||||
* If the `$featured` parameter is true, the function will return a tax query
|
||||
* that filters products to only those marked as featured.
|
||||
* If `$featured` is false, an empty array is returned, meaning no filtering will be applied.
|
||||
*
|
||||
* @param bool $featured A flag indicating whether to filter products based on featured status.
|
||||
*
|
||||
* @return array A tax query for fetching featured products if `$featured` is true; otherwise, an empty array.
|
||||
*/
|
||||
private function get_featured_query( $featured ) {
|
||||
if ( true !== $featured && 'true' !== $featured ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
'tax_query' => array(
|
||||
array(
|
||||
'taxonomy' => 'product_visibility',
|
||||
'field' => 'name',
|
||||
'terms' => 'featured',
|
||||
'operator' => 'IN',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Merge tax_queries from various queries.
|
||||
*
|
||||
@@ -957,4 +998,38 @@ class ProductCollection extends AbstractBlock {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a date query for product filtering based on a specified time frame.
|
||||
*
|
||||
* @param array $time_frame {
|
||||
* Associative array with 'operator' (in or not-in) and 'value' (date string).
|
||||
*
|
||||
* @type string $operator Determines the inclusion or exclusion of the date range.
|
||||
* @type string $value The date around which the range is applied.
|
||||
* }
|
||||
* @return array Date query array; empty if parameters are invalid.
|
||||
*/
|
||||
private function get_date_query( array $time_frame ) : array {
|
||||
// Validate time_frame elements.
|
||||
if ( empty( $time_frame['operator'] ) || empty( $time_frame['value'] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Determine the query operator based on the 'operator' value.
|
||||
$query_operator = 'in' === $time_frame['operator'] ? 'after' : 'before';
|
||||
|
||||
// Construct and return the date query.
|
||||
return array(
|
||||
'date_query' => array(
|
||||
array(
|
||||
'column' => 'post_date_gmt',
|
||||
$query_operator => $time_frame['value'],
|
||||
'inclusive' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
|
||||
/**
|
||||
* ProductCollectionNoResults class.
|
||||
*/
|
||||
class ProductCollectionNoResults extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'product-collection-no-results';
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @param WP_Block $block Block instance.
|
||||
*
|
||||
* @return string | void Rendered block output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
$content = trim( $content );
|
||||
if ( empty( $content ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$query = ProductCollectionUtils::prepare_and_execute_query( $block );
|
||||
|
||||
// If the query has products, don't render the block.
|
||||
if ( $query->post_count > 0 ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Update the anchor tag URLs.
|
||||
$updated_html_content = $this->modify_anchor_tag_urls( trim( $content ) );
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
return sprintf(
|
||||
'<div %1$s>%2$s</div>',
|
||||
$wrapper_attributes,
|
||||
$updated_html_content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL attributes for "clearing any filters" and "Store's home" links.
|
||||
*
|
||||
* @param string $content Block content.
|
||||
*/
|
||||
protected function modify_anchor_tag_urls( $content ) {
|
||||
$processor = new \WP_HTML_Tag_Processor( trim( $content ) );
|
||||
|
||||
// Set the URL attribute for the "clear any filters" link.
|
||||
if ( $processor->next_tag(
|
||||
array(
|
||||
'tag_name' => 'a',
|
||||
'class_name' => 'wc-link-clear-any-filters',
|
||||
)
|
||||
) ) {
|
||||
$processor->set_attribute( 'href', $this->get_current_url_without_filters() );
|
||||
}
|
||||
|
||||
// Set the URL attribute for the "Store's home" link.
|
||||
if ( $processor->next_tag(
|
||||
array(
|
||||
'tag_name' => 'a',
|
||||
'class_name' => 'wc-link-stores-home',
|
||||
)
|
||||
) ) {
|
||||
$processor->set_attribute( 'href', home_url() );
|
||||
}
|
||||
|
||||
return $processor->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current URL without filter query parameters which will be used
|
||||
* for the "clear any filters" link.
|
||||
*/
|
||||
protected function get_current_url_without_filters() {
|
||||
$protocol = is_ssl() ? 'https' : 'http';
|
||||
|
||||
// Check the existence and sanitize HTTP_HOST and REQUEST_URI in the $_SERVER superglobal.
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$http_host = isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '';
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
|
||||
// Sanitize the host and URI.
|
||||
$http_host = sanitize_text_field( $http_host );
|
||||
$request_uri = esc_url_raw( $request_uri );
|
||||
|
||||
// Construct the full URL.
|
||||
$current_url = $protocol . '://' . $http_host . $request_uri;
|
||||
|
||||
// Parse the URL to extract the query string.
|
||||
$parsed_url = wp_parse_url( $current_url );
|
||||
$query_string = isset( $parsed_url['query'] ) ? $parsed_url['query'] : '';
|
||||
|
||||
// Convert the query string into an associative array.
|
||||
parse_str( $query_string, $query_params );
|
||||
|
||||
// Remove the filter query parameters.
|
||||
$params_to_remove = array( 'min_price', 'max_price', 'rating_filter', 'filter_', 'query_type_' );
|
||||
foreach ( $query_params as $key => $value ) {
|
||||
foreach ( $params_to_remove as $param ) {
|
||||
if ( strpos( $key, $param ) === 0 ) {
|
||||
unset( $query_params[ $key ] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild the query string without the removed parameters.
|
||||
$new_query_string = http_build_query( $query_params );
|
||||
|
||||
// Reconstruct the URL.
|
||||
$new_url = $parsed_url['scheme'] . '://' . $parsed_url['host'];
|
||||
$new_url .= isset( $parsed_url['path'] ) ? $parsed_url['path'] : '';
|
||||
$new_url .= $new_query_string ? '?' . $new_query_string : '';
|
||||
|
||||
return $new_url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -123,9 +123,10 @@ class ProductGallery extends AbstractBlock {
|
||||
|
||||
$number_of_thumbnails = $block->attributes['thumbnailsNumberOfThumbnails'] ?? 0;
|
||||
$classname = $attributes['className'] ?? '';
|
||||
$dialog = ( true === $attributes['fullScreenOnClick'] && isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ) ? $this->render_dialog() : '';
|
||||
$dialog = isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ? $this->render_dialog() : '';
|
||||
$post_id = $block->context['postId'] ?? '';
|
||||
$product = wc_get_product( $post_id );
|
||||
$product_id = strval( $product->get_id() );
|
||||
|
||||
$html = $this->inject_dialog( $content, $dialog );
|
||||
$p = new \WP_HTML_Tag_Processor( $html );
|
||||
@@ -137,13 +138,22 @@ class ProductGallery extends AbstractBlock {
|
||||
wp_json_encode(
|
||||
array(
|
||||
'woocommerce' => array(
|
||||
'selectedImage' => $product->get_image_id(),
|
||||
'visibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, $number_of_thumbnails, true ),
|
||||
'isDialogOpen' => false,
|
||||
'selectedImage' => $product->get_image_id(),
|
||||
'firstMainImageId' => $product->get_image_id(),
|
||||
'visibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, $number_of_thumbnails, true ),
|
||||
'dialogVisibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, null, false ),
|
||||
'mouseIsOverPreviousOrNextButton' => false,
|
||||
'isDialogOpen' => false,
|
||||
'productId' => $product_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $product->is_type( 'variable' ) ) {
|
||||
$p->set_attribute( 'data-wc-init--watch-changes-on-add-to-cart-form', 'effects.woocommerce.watchForChangesOnAddToCartForm' );
|
||||
}
|
||||
|
||||
$p->add_class( $classname );
|
||||
$p->add_class( $classname_single_image );
|
||||
$html = $p->get_updated_html();
|
||||
|
||||
@@ -120,11 +120,11 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
'data-wc-bind--style' => 'selectors.woocommerce.productGalleryLargeImage.styles',
|
||||
'data-wc-effect' => 'effects.woocommerce.scrollInto',
|
||||
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
|
||||
|
||||
'data-wc-class--wc-block-woocommerce-product-gallery-large-image__image--active-image-slide' => 'selectors.woocommerce.isSelected',
|
||||
);
|
||||
|
||||
if ( $context['fullScreenOnClick'] ) {
|
||||
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click';
|
||||
$attributes['class'] .= ' wc-block-woocommerce-product-gallery-large-image__image--full-screen-on-click wc-block-product-gallery-dialog-on-click';
|
||||
}
|
||||
|
||||
if ( $context['hoverZoom'] ) {
|
||||
@@ -136,7 +136,8 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
$product_id,
|
||||
'full',
|
||||
$attributes,
|
||||
'wc-block-product-gallery-large-image__image-element'
|
||||
'wc-block-product-gallery-large-image__image-element',
|
||||
$context['cropImages']
|
||||
);
|
||||
|
||||
$main_image_with_wrapper = array_map(
|
||||
|
||||
@@ -89,6 +89,14 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
|
||||
'data-wc-on--click',
|
||||
'actions.woocommerce.handlePreviousImageButtonClick'
|
||||
);
|
||||
$p->set_attribute(
|
||||
'data-wc-on--mouseleave',
|
||||
'actions.woocommerce.handleMouseLeavePreviousOrNextButton'
|
||||
);
|
||||
$p->set_attribute(
|
||||
'data-wc-on--mouseenter',
|
||||
'actions.woocommerce.handleMouseEnterPreviousOrNextButton'
|
||||
);
|
||||
$prev_button = $p->get_updated_html();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,77 @@ class ProductGalleryThumbnails extends AbstractBlock {
|
||||
* @return string[]
|
||||
*/
|
||||
protected function get_block_type_uses_context() {
|
||||
return [ 'productGalleryClientId', 'postId', 'thumbnailsNumberOfThumbnails', 'thumbnailsPosition' ];
|
||||
return [ 'productGalleryClientId', 'postId', 'thumbnailsNumberOfThumbnails', 'thumbnailsPosition', 'mode' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the View All markup.
|
||||
*
|
||||
* @param int $remaining_thumbnails_count The number of thumbnails that are not displayed.
|
||||
*
|
||||
* @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.woocommerce.handleClick">
|
||||
<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>';
|
||||
|
||||
return sprintf(
|
||||
$view_all_html,
|
||||
esc_html( $remaining_thumbnails_count ),
|
||||
esc_html__( 'View all', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject View All markup into the product thumbnail HTML.
|
||||
*
|
||||
* @param string $thumbnail_html The thumbnail HTML.
|
||||
* @param string $view_all_html The view all HTML.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function inject_view_all( $thumbnail_html, $view_all_html ) {
|
||||
|
||||
// Find the position of the last </div>.
|
||||
$pos = strrpos( $thumbnail_html, '</div>' );
|
||||
|
||||
if ( false !== $pos ) {
|
||||
// Inject the view_all_html at the correct position.
|
||||
$html = substr_replace( $thumbnail_html, $view_all_html, $pos, 0 );
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the thumbnails should be limited.
|
||||
*
|
||||
* @param string $mode Mode of the gallery. Expected values: 'standard'.
|
||||
* @param int $thumbnails_count Current count of processed thumbnails.
|
||||
* @param int $number_of_thumbnails Number of thumbnails configured to display.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_limit_thumbnails( $mode, $thumbnails_count, $number_of_thumbnails ) {
|
||||
return 'standard' === $mode && $thumbnails_count > $number_of_thumbnails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if View All markup should be displayed.
|
||||
*
|
||||
* @param string $mode Mode of the gallery. Expected values: 'standard'.
|
||||
* @param int $thumbnails_count Current count of processed thumbnails.
|
||||
* @param array $product_gallery_images Array of product gallery image HTML strings.
|
||||
* @param int $number_of_thumbnails Number of thumbnails configured to display.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function should_display_view_all( $mode, $thumbnails_count, $product_gallery_images, $number_of_thumbnails ) {
|
||||
return 'standard' === $mode &&
|
||||
$thumbnails_count === $number_of_thumbnails &&
|
||||
count( $product_gallery_images ) > $number_of_thumbnails;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,22 +137,31 @@ class ProductGalleryThumbnails extends AbstractBlock {
|
||||
if ( $product_gallery_images && $post_thumbnail_id ) {
|
||||
$html = '';
|
||||
$number_of_thumbnails = isset( $block->context['thumbnailsNumberOfThumbnails'] ) ? $block->context['thumbnailsNumberOfThumbnails'] : 3;
|
||||
$mode = $block->context['mode'] ?? '';
|
||||
$thumbnails_count = 1;
|
||||
|
||||
foreach ( $product_gallery_images as $product_gallery_image_html ) {
|
||||
if ( $thumbnails_count > $number_of_thumbnails ) {
|
||||
// Limit the number of thumbnails only in the standard mode (and not in dialog).
|
||||
if ( $this->should_limit_thumbnails( $mode, $thumbnails_count, $number_of_thumbnails ) ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$processor = new \WP_HTML_Tag_Processor( $product_gallery_image_html );
|
||||
// If not in dialog and it's the last thumbnail and the number of product gallery images is greater than the number of thumbnails settings output the View All markup.
|
||||
if ( $this->should_display_view_all( $mode, $thumbnails_count, $product_gallery_images, $number_of_thumbnails ) ) {
|
||||
$remaining_thumbnails_count = count( $product_gallery_images ) - $number_of_thumbnails;
|
||||
$product_gallery_image_html = $this->inject_view_all( $product_gallery_image_html, $this->generate_view_all_html( $remaining_thumbnails_count ) );
|
||||
$html .= $product_gallery_image_html;
|
||||
} else {
|
||||
$processor = new \WP_HTML_Tag_Processor( $product_gallery_image_html );
|
||||
|
||||
if ( $processor->next_tag( 'img' ) ) {
|
||||
$processor->set_attribute(
|
||||
'data-wc-on--click',
|
||||
'actions.woocommerce.thumbnails.handleClick'
|
||||
);
|
||||
if ( $processor->next_tag( 'img' ) ) {
|
||||
$processor->set_attribute(
|
||||
'data-wc-on--click',
|
||||
'actions.woocommerce.thumbnails.handleClick'
|
||||
);
|
||||
|
||||
$html .= $processor->get_updated_html();
|
||||
$html .= $processor->get_updated_html();
|
||||
}
|
||||
}
|
||||
|
||||
$thumbnails_count++;
|
||||
|
||||
@@ -298,6 +298,7 @@ class ProductQuery extends AbstractBlock {
|
||||
*/
|
||||
if (
|
||||
! empty( $merged_query['post__in'] ) &&
|
||||
is_array( $merged_query['post__in'] ) &&
|
||||
count( $merged_query['post__in'] ) > count( array_unique( $merged_query['post__in'] ) )
|
||||
) {
|
||||
$merged_query['post__in'] = array_unique(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
use WP_Block;
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* ProductTemplate class.
|
||||
@@ -36,19 +36,7 @@ class ProductTemplate extends AbstractBlock {
|
||||
* @return string | void Rendered block output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
$page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page';
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ];
|
||||
|
||||
// Use global query if needed.
|
||||
$use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] );
|
||||
if ( $use_global_query ) {
|
||||
global $wp_query;
|
||||
$query = clone $wp_query;
|
||||
} else {
|
||||
$query_args = build_query_vars_from_query_block( $block, $page );
|
||||
$query = new WP_Query( $query_args );
|
||||
}
|
||||
$query = ProductCollectionUtils::prepare_and_execute_query( $block );
|
||||
|
||||
if ( ! $query->have_posts() ) {
|
||||
return '';
|
||||
|
||||
@@ -52,6 +52,46 @@ final class BlockTypesController {
|
||||
add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
|
||||
add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
|
||||
add_action( 'woocommerce_delete_product_transients', array( $this, 'delete_product_transients' ) );
|
||||
add_filter(
|
||||
'woocommerce_is_checkout',
|
||||
function( $return ) {
|
||||
return $return || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'checkout' );
|
||||
}
|
||||
);
|
||||
add_filter(
|
||||
'woocommerce_is_cart',
|
||||
function( $return ) {
|
||||
return $return || $this->has_block_variation( 'woocommerce/classic-shortcode', 'shortcode', 'cart' );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current post has a block with a specific attribute value.
|
||||
*
|
||||
* @param string $block_id The block ID to check for.
|
||||
* @param string $attribute The attribute to check.
|
||||
* @param string $value The value to check for.
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_block_variation( $block_id, $attribute, $value ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( has_block( $block_id, $post->ID ) ) {
|
||||
$blocks = (array) parse_blocks( $post->post_content );
|
||||
|
||||
foreach ( $blocks as $block ) {
|
||||
if ( isset( $block['attrs'][ $attribute ] ) && $value === $block['attrs'][ $attribute ] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +239,7 @@ final class BlockTypesController {
|
||||
'ProductCategories',
|
||||
'ProductCategory',
|
||||
'ProductCollection',
|
||||
'ProductCollectionNoResults',
|
||||
'ProductImage',
|
||||
'ProductImageGallery',
|
||||
'ProductNew',
|
||||
@@ -256,6 +297,7 @@ final class BlockTypesController {
|
||||
$block_types[] = 'ProductGalleryPager';
|
||||
$block_types[] = 'ProductGalleryThumbnails';
|
||||
$block_types[] = 'CollectionFilters';
|
||||
$block_types[] = 'CollectionStockFilter';
|
||||
$block_types[] = 'CollectionPriceFilter';
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use Automattic\WooCommerce\Blocks\BlockPatterns;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Blocks\BlockTypesController;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\JetpackWooCommerceAnalytics;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
|
||||
@@ -132,7 +131,6 @@ class Bootstrap {
|
||||
$this->container->get( CreateAccount::class )->init();
|
||||
$this->container->get( ShippingController::class )->init();
|
||||
$this->container->get( TasksController::class )->init();
|
||||
$this->container->get( JetpackWooCommerceAnalytics::class )->init();
|
||||
|
||||
// Load assets in admin and on the frontend.
|
||||
if ( ! $is_rest ) {
|
||||
@@ -366,15 +364,6 @@ class Bootstrap {
|
||||
return new GoogleAnalytics( $asset_api );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
JetpackWooCommerceAnalytics::class,
|
||||
function( Container $container ) {
|
||||
$asset_api = $container->get( AssetApi::class );
|
||||
$asset_data_registry = $container->get( AssetDataRegistry::class );
|
||||
$block_templates_controller = $container->get( BlockTemplatesController::class );
|
||||
return new JetpackWooCommerceAnalytics( $asset_api, $asset_data_registry, $block_templates_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
Notices::class,
|
||||
function( Container $container ) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\InteractivityComponents;
|
||||
|
||||
/**
|
||||
* Dropdown class. This is a component for reuse with interactivity API.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Blocks\InteractivityComponents
|
||||
*/
|
||||
class Dropdown {
|
||||
/**
|
||||
* Render the dropdown.
|
||||
*
|
||||
* @param mixed $props The properties to render the dropdown with.
|
||||
* @return string|false
|
||||
*/
|
||||
public static function render( $props ) {
|
||||
wp_enqueue_script( 'wc-interactivity-dropdown' );
|
||||
|
||||
$selected_item = $props['selected_item'] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
$dropdown_context = array(
|
||||
'woocommerceDropdown' => array(
|
||||
'selectedItem' => $selected_item,
|
||||
'hoveredItem' => array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
),
|
||||
'isOpen' => false,
|
||||
),
|
||||
);
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
|
||||
// Items should be an array of objects with a label and value property.
|
||||
$items = $props['items'] ?? [];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wc-block-stock-filter style-dropdown" data-wc-context='<?php echo wp_json_encode( $dropdown_context ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
data-wc-class--is-active="context.woocommerceDropdown.isOpen"
|
||||
tabindex="-1"
|
||||
data-wc-on--click="actions.woocommerceDropdown.toggleIsOpen"
|
||||
>
|
||||
<input id="components-form-token-input-1" type="text" autocomplete="off" data-wc-bind--placeholder="selectors.woocommerceDropdown.placeholderText" class="components-form-token-field__input" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-describedby="components-form-token-suggestions-howto-1" value="">
|
||||
<ul hidden data-wc-bind--hidden="!context.woocommerceDropdown.isOpen" class="components-form-token-field__suggestions-list" id="components-form-token-suggestions-1" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array(
|
||||
'woocommerceDropdown' => array( 'currentItem' => $item ),
|
||||
JSON_NUMERIC_CHECK,
|
||||
);
|
||||
?>
|
||||
<li
|
||||
role="option"
|
||||
data-wc-on--click--select-item="actions.woocommerceDropdown.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="selectors.woocommerceDropdown.isSelected"
|
||||
data-wc-on--mouseover="actions.woocommerceDropdown.addHoverClass"
|
||||
data-wc-on--mouseout="actions.woocommerceDropdown.removeHoverClass"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="selectors.woocommerceDropdown.isSelected"
|
||||
>
|
||||
<?php echo esc_html( $item['label'] ); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class Package {
|
||||
NewPackage::class,
|
||||
function ( $container ) {
|
||||
// leave for automated version bumping.
|
||||
$version = '11.4.9';
|
||||
$version = '11.6.2';
|
||||
return new NewPackage(
|
||||
$version,
|
||||
dirname( __DIR__ ),
|
||||
|
||||
@@ -10,11 +10,6 @@ use WP_Error;
|
||||
*/
|
||||
class PatternUpdater {
|
||||
|
||||
/**
|
||||
* The patterns content option name.
|
||||
*/
|
||||
const WC_BLOCKS_PATTERNS_CONTENT = 'wc_blocks_patterns_content';
|
||||
|
||||
/**
|
||||
* Creates the patterns content for the given vertical.
|
||||
*
|
||||
@@ -42,13 +37,15 @@ class PatternUpdater {
|
||||
return new WP_Error( 'failed_to_set_pattern_content', __( 'Failed to set the pattern content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( get_option( self::WC_BLOCKS_PATTERNS_CONTENT ) === $patterns_with_images_and_content ) {
|
||||
$patterns_ai_data_post = PatternsHelper::get_patterns_ai_data_post();
|
||||
|
||||
if ( isset( $patterns_ai_data_post->post_content ) && json_decode( $patterns_ai_data_post->post_content ) === $patterns_with_images_and_content ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$updated_content = update_option( self::WC_BLOCKS_PATTERNS_CONTENT, $patterns_with_images_and_content );
|
||||
$updated_content = PatternsHelper::upsert_patterns_ai_data_post( $patterns_with_images_and_content );
|
||||
|
||||
if ( ! $updated_content ) {
|
||||
if ( is_wp_error( $updated_content ) ) {
|
||||
return new WP_Error( 'failed_to_update_patterns_content', __( 'Failed to update patterns content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,48 @@ class PatternsHelper {
|
||||
|
||||
return $image;
|
||||
}
|
||||
/**
|
||||
* Returns the post that has the generated data by the AI for the patterns.
|
||||
*
|
||||
* @return WP_Post|null
|
||||
*/
|
||||
public static function get_patterns_ai_data_post() {
|
||||
$arg = array(
|
||||
'post_type' => 'patterns_ai_data',
|
||||
'posts_per_page' => 1,
|
||||
'no_found_rows' => true,
|
||||
'cache_results' => true,
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $arg );
|
||||
|
||||
$posts = $query->get_posts();
|
||||
return isset( $posts[0] ) ? $posts[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the patterns AI data.
|
||||
*
|
||||
* @param array $patterns_dictionary The patterns dictionary.
|
||||
*
|
||||
* @return WP_Error|null
|
||||
*/
|
||||
public static function upsert_patterns_ai_data_post( $patterns_dictionary ) {
|
||||
$patterns_ai_data_post = self::get_patterns_ai_data_post();
|
||||
|
||||
if ( isset( $patterns_ai_data_post ) ) {
|
||||
$patterns_ai_data_post->post_content = wp_json_encode( $patterns_dictionary );
|
||||
return wp_update_post( $patterns_ai_data_post, true );
|
||||
} else {
|
||||
$patterns_ai_data_post = array(
|
||||
'post_title' => 'Patterns AI Data',
|
||||
'post_content' => wp_json_encode( $patterns_dictionary ),
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'patterns_ai_data',
|
||||
);
|
||||
return wp_insert_post( $patterns_ai_data_post, true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Patterns Dictionary.
|
||||
@@ -82,10 +124,12 @@ class PatternsHelper {
|
||||
*
|
||||
* @return mixed|WP_Error|null
|
||||
*/
|
||||
private static function get_patterns_dictionary( $pattern_slug = null ) {
|
||||
$patterns_dictionary = get_option( PatternUpdater::WC_BLOCKS_PATTERNS_CONTENT );
|
||||
public static function get_patterns_dictionary( $pattern_slug = null ) {
|
||||
|
||||
if ( ! empty( $patterns_dictionary ) ) {
|
||||
$patterns_ai_data_post = self::get_patterns_ai_data_post();
|
||||
|
||||
if ( isset( $patterns_ai_data_post ) ) {
|
||||
$patterns_dictionary = json_decode( $patterns_ai_data_post->post_content, true );
|
||||
if ( empty( $pattern_slug ) ) {
|
||||
return $patterns_dictionary;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class ProductUpdater {
|
||||
* @param array $images The array of images.
|
||||
* @param string $business_description The business description.
|
||||
*
|
||||
* @return bool|WP_Error True if the content was generated successfully, WP_Error otherwise.
|
||||
* @return array|WP_Error The generated content for the products. An error if the content could not be generated.
|
||||
*/
|
||||
public function generate_content( $ai_connection, $token, $images, $business_description ) {
|
||||
if ( empty( $business_description ) ) {
|
||||
@@ -28,16 +28,48 @@ class ProductUpdater {
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
|
||||
return true;
|
||||
return array(
|
||||
'product_content' => array(),
|
||||
);
|
||||
} else {
|
||||
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$ai_selected_products_images = $this->get_images_information( $images );
|
||||
$products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images );
|
||||
|
||||
$response = $this->generate_product_content( $ai_connection, $token, $products_information_list );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$error_msg = $response;
|
||||
} elseif ( empty( $response ) || ! isset( $response['completion'] ) ) {
|
||||
$error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( isset( $error_msg ) ) {
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
$product_content = json_decode( $response['completion'], true );
|
||||
|
||||
return array(
|
||||
'product_content' => $product_content,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all dummy products that were not modified by the store owner.
|
||||
*
|
||||
* @return array|WP_Error An array with the dummy products that need to have their content updated by AI.
|
||||
*/
|
||||
public function fetch_dummy_products_to_update() {
|
||||
$real_products = $this->fetch_product_ids();
|
||||
|
||||
if ( is_array( $real_products ) && count( $real_products ) > 0 ) {
|
||||
return true;
|
||||
return array(
|
||||
'product_content' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
$dummy_products = $this->fetch_product_ids( 'dummy' );
|
||||
@@ -82,62 +114,7 @@ class ProductUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $dummy_products_to_update ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$ai_selected_products_images = $this->get_images_information( $images );
|
||||
$products_information_list = $this->assign_ai_selected_images_to_dummy_products_information_list( $ai_selected_products_images );
|
||||
|
||||
$response = $this->generate_product_content( $ai_connection, $token, $products_information_list );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$error_msg = $response;
|
||||
} elseif ( empty( $response ) || ! isset( $response['completion'] ) ) {
|
||||
$error_msg = new \WP_Error( 'missing_completion_key', __( 'The response from the AI service is empty or missing the completion key.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( isset( $error_msg ) ) {
|
||||
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
|
||||
|
||||
return $error_msg;
|
||||
}
|
||||
|
||||
$product_content = json_decode( $response['completion'], true );
|
||||
|
||||
if ( is_null( $product_content ) ) {
|
||||
$this->update_dummy_products( $dummy_products_to_update, $products_information_list );
|
||||
|
||||
return new \WP_Error( 'invalid_json', __( 'The response from the AI service is not a valid JSON.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
// This is required to allow the usage of the media_sideload_image function outside the context of /wp-admin/.
|
||||
// See https://developer.wordpress.org/reference/functions/media_sideload_image/ for more details.
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
$this->update_dummy_products( $dummy_products_to_update, $product_content );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the dummy products with the content from the information list.
|
||||
*
|
||||
* @param array $dummy_products_to_update The dummy products to update.
|
||||
* @param array $products_information_list The products information list.
|
||||
*/
|
||||
public function update_dummy_products( $dummy_products_to_update, $products_information_list ) {
|
||||
$i = 0;
|
||||
foreach ( $dummy_products_to_update as $dummy_product ) {
|
||||
if ( ! isset( $products_information_list[ $i ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->update_product_content( $dummy_product, $products_information_list[ $i ] );
|
||||
++$i;
|
||||
}
|
||||
return $dummy_products_to_update;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,11 +250,15 @@ class ProductUpdater {
|
||||
if ( ! isset( $ai_generated_product_content['image']['src'] ) || ! isset( $ai_generated_product_content['image']['alt'] ) || ! isset( $ai_generated_product_content['title'] ) || ! isset( $ai_generated_product_content['description'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/media.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
require_once ABSPATH . 'wp-admin/includes/image.php';
|
||||
|
||||
// Since the media_sideload_image function is expensive and can take longer to complete
|
||||
// the process of downloading the external image and uploading it to the media library,
|
||||
// here we are increasing the time limit and the memory limit to avoid any issues.
|
||||
// here we are increasing the time limit to avoid any issues.
|
||||
set_time_limit( 60 );
|
||||
wp_raise_memory_limit();
|
||||
|
||||
$product_image_id = media_sideload_image( $ai_generated_product_content['image']['src'], $product->get_id(), $ai_generated_product_content['image']['alt'], 'id' );
|
||||
|
||||
@@ -394,7 +375,7 @@ class ProductUpdater {
|
||||
return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate the content for your site.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$prompt = sprintf( 'Given the following business description: "%1s" and the assigned value for the alt property in the json bellow, generate new titles and descriptions for each one of the products listed bellow and assign them as the new values for the json: %2s. Each one of the titles should be unique and must be limited to 29 characters. The response should be only a JSON string, with no intro or explanations.', $store_description, wp_json_encode( $products_default_content ) );
|
||||
$prompt = sprintf( 'Given the following business description: "%1s" and the assigned value for the alt property in the JSON below, generate new titles and descriptions for each one of the products listed below and assign them as the new values for the JSON: %2s. Each one of the titles should be unique and must be limited to 29 characters. The response should be only a JSON string, with no intro or explanations.', $store_description, wp_json_encode( $products_default_content ) );
|
||||
|
||||
return $ai_connection->fetch_ai_response( $token, $prompt, 60 );
|
||||
}
|
||||
|
||||
@@ -170,47 +170,31 @@
|
||||
"images_format": "landscape",
|
||||
"content": {
|
||||
"titles": [
|
||||
{
|
||||
"default": "The Fall Collection",
|
||||
"ai_prompt": "An impact phrase that advertises the displayed product: {image.0}"
|
||||
},
|
||||
{
|
||||
"default": "Quality Materials",
|
||||
"ai_prompt": "A title describing the first displayed product feature"
|
||||
"ai_prompt": "A title describing the first displayed product feature. The title must have less than 20 characters."
|
||||
},
|
||||
{
|
||||
"default": "Expert Craftsmanship",
|
||||
"ai_prompt": "A title describing the second displayed product feature"
|
||||
},
|
||||
{
|
||||
"default": "Unique Design",
|
||||
"ai_prompt": "A title describing the third displayed product feature"
|
||||
"ai_prompt": "A title describing the second displayed product feature. The title must have less than 20 characters."
|
||||
},
|
||||
{
|
||||
"default": "Customer Satisfaction",
|
||||
"ai_prompt": "A title describing the fourth displayed product feature"
|
||||
"ai_prompt": "A title describing the fourth displayed product feature. The title must have less than 20 characters."
|
||||
}
|
||||
],
|
||||
"descriptions": [
|
||||
{
|
||||
"default": "With high-quality materials and expert craftsmanship, our products are built to last and exceed your expectations.",
|
||||
"ai_prompt": "A description of the product"
|
||||
},
|
||||
{
|
||||
"default": "We use only the highest-quality materials in our products, ensuring that they look great and last for years to come.",
|
||||
"ai_prompt": "A description of the first displayed product feature"
|
||||
"ai_prompt": "A description of the first displayed product feature. The text must have less than 120 characters."
|
||||
},
|
||||
{
|
||||
"default": "Our products are made with expert craftsmanship and attention to detail, ensuring that every stitch and seam is perfect.",
|
||||
"ai_prompt": "A description of the second displayed product feature"
|
||||
},
|
||||
{
|
||||
"default": "From bold prints and colors to intricate details and textures, our products are a perfect combination of style and function.",
|
||||
"ai_prompt": "A description of the third displayed product feature"
|
||||
"ai_prompt": "A description of the second displayed product feature. The text must have less than 120 characters."
|
||||
},
|
||||
{
|
||||
"default": "Our top priority is customer satisfaction, and we stand behind our products 100%. ",
|
||||
"ai_prompt": "A description of the fourth displayed product feature"
|
||||
"ai_prompt": "A description of the fourth displayed product feature. The text must have less than 120 characters."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
|
||||
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
use WC_Tracks;
|
||||
|
||||
/**
|
||||
* ShippingController class.
|
||||
@@ -81,6 +82,8 @@ class ShippingController {
|
||||
// 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.
|
||||
add_filter( 'option_woocommerce_shipping_cost_requires_address', array( $this, 'override_cost_requires_address_option' ) );
|
||||
|
||||
add_action( 'rest_pre_serve_request', array( $this, 'track_local_pickup' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,4 +469,47 @@ class ShippingController {
|
||||
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track local pickup settings changes via Store API
|
||||
*
|
||||
* @param bool $served Whether the request has already been served.
|
||||
* @param \WP_REST_Response $result The response object.
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return bool
|
||||
*/
|
||||
public function track_local_pickup( $served, $result, $request ) {
|
||||
if ( '/wp/v2/settings' !== $request->get_route() ) {
|
||||
return $served;
|
||||
}
|
||||
// Param name here comes from the show_in_rest['name'] value when registering the setting.
|
||||
if ( ! $request->get_param( 'pickup_location_settings' ) && ! $request->get_param( 'pickup_locations' ) ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
$event_name = 'local_pickup_save_changes';
|
||||
|
||||
$settings = $request->get_param( 'pickup_location_settings' );
|
||||
$locations = $request->get_param( 'pickup_locations' );
|
||||
|
||||
$data = array(
|
||||
'local_pickup_enabled' => 'yes' === $settings['enabled'] ? true : false,
|
||||
'title' => __( 'Local Pickup', 'woocommerce' ) === $settings['title'],
|
||||
'price' => '' === $settings['cost'] ? true : false,
|
||||
'cost' => '' === $settings['cost'] ? 0 : $settings['cost'],
|
||||
'taxes' => $settings['tax_status'],
|
||||
'total_pickup_locations' => count( $locations ),
|
||||
'pickup_locations_enabled' => count(
|
||||
array_filter(
|
||||
$locations,
|
||||
function( $location ) {
|
||||
return $location['enabled']; }
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
WC_Tracks::record_event( $event_name, $data );
|
||||
|
||||
return $served;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
* BusinessDescription class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BusinessDescription extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/business-description';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'ai/business-description';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/ai/business-description';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => [ Middleware::class, 'is_authorized' ],
|
||||
'args' => [
|
||||
'business_description' => [
|
||||
'description' => __( 'The business description for a given store.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
'allow_batch' => [ 'v1' => true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the last business description.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return bool|string|\WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
|
||||
$business_description = $request->get_param( 'business_description' );
|
||||
|
||||
if ( ! $business_description ) {
|
||||
return $this->error_to_response(
|
||||
new \WP_Error(
|
||||
'invalid_business_description',
|
||||
__( 'Invalid business description.', 'woocommerce' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
update_option( 'last_business_description_with_ai_content_generated', $business_description );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'ai_content_generated' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
* Patterns class.
|
||||
*/
|
||||
class Images extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/images';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'ai/images';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/ai/images';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => [ Middleware::class, 'is_authorized' ],
|
||||
'args' => [
|
||||
'business_description' => [
|
||||
'description' => __( 'The business description for a given store.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
'allow_batch' => [ 'v1' => true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Images from Pexels
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return bool|string|\WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
|
||||
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
|
||||
|
||||
if ( empty( $business_description ) ) {
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
return rest_ensure_response(
|
||||
$this->prepare_item_for_response(
|
||||
[
|
||||
'ai_content_generated' => true,
|
||||
'images' => array(),
|
||||
],
|
||||
$request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $this->error_to_response( $site_id );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $this->error_to_response( $token );
|
||||
}
|
||||
|
||||
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
|
||||
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $this->error_to_response( $images );
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
$this->prepare_item_for_response(
|
||||
[
|
||||
'ai_content_generated' => true,
|
||||
'images' => $images,
|
||||
],
|
||||
$request
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
|
||||
|
||||
/**
|
||||
* Middleware class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Middleware {
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that the user is allowed to make this request.
|
||||
*
|
||||
* @throws RouteException If the user is not allowed to make this request.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_authorized() {
|
||||
try {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'You are not allowed to make this request. Please make sure you are logged in.', 'woocommerce' ), 403 );
|
||||
}
|
||||
} catch ( RouteException $error ) {
|
||||
return new \WP_Error(
|
||||
$error->getErrorCode(),
|
||||
$error->getMessage(),
|
||||
array( 'status' => $error->getCode() )
|
||||
);
|
||||
}
|
||||
|
||||
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
|
||||
|
||||
if ( ! $allow_ai_connection ) {
|
||||
try {
|
||||
throw new RouteException( 'ai_connection_not_allowed', __( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' ), 403 );
|
||||
} catch ( RouteException $error ) {
|
||||
return new \WP_Error(
|
||||
$error->getErrorCode(),
|
||||
$error->getMessage(),
|
||||
array( 'status' => $error->getCode() )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
* Patterns class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Patterns extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/patterns';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'ai/patterns';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/ai/patterns';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => [ Middleware::class, 'is_authorized' ],
|
||||
'args' => [
|
||||
'business_description' => [
|
||||
'description' => __( 'The business description for a given store.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'images' => [
|
||||
'description' => __( 'The images for a given store.', 'woocommerce' ),
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
'allow_batch' => [ 'v1' => true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update patterns with the content and images powered by AI.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return bool|string|\WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
|
||||
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $this->error_to_response( $site_id );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
$images = $request['images'];
|
||||
|
||||
try {
|
||||
( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
return rest_ensure_response( array( 'ai_content_generated' => true ) );
|
||||
} catch ( \WP_Error $e ) {
|
||||
return $this->error_to_response( $e );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
* Product class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Product extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/product';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'ai/product';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/ai/product';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => [ Middleware::class, 'is_authorized' ],
|
||||
'args' => [
|
||||
'index' => [
|
||||
'description' => __( 'The business description for a given store.', 'woocommerce' ),
|
||||
'type' => 'integer',
|
||||
],
|
||||
'products_information' => [
|
||||
'description' => __( 'Data generated by AI for updating dummy products.', 'woocommerce' ),
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
'allow_batch' => [ 'v1' => true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update product with the content and image powered by AI.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return bool|string|\WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$product_updater = new ProductUpdater();
|
||||
$dummy_products = $product_updater->fetch_dummy_products_to_update();
|
||||
|
||||
if ( empty( $dummy_products ) ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'ai_content_generated' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$index = $request['index'];
|
||||
if ( ! is_numeric( $index ) ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'ai_content_generated' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$products_information = $request['products_information'] ?? array();
|
||||
|
||||
if ( ! isset( $dummy_products[ $index ] ) ) {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'ai_content_generated' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$product_updater->update_product_content( $dummy_products[ $index ], $products_information );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'ai_content_generated' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\StoreApi\Routes\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
* Products class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Products extends AbstractRoute {
|
||||
/**
|
||||
* The route identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/products';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEMA_TYPE = 'ai/products';
|
||||
|
||||
/**
|
||||
* Get the path of this REST route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_path() {
|
||||
return '/ai/products';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method arguments for this REST route.
|
||||
*
|
||||
* @return array An array of endpoints.
|
||||
*/
|
||||
public function get_args() {
|
||||
return [
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => [ $this, 'get_response' ],
|
||||
'permission_callback' => [ Middleware::class, 'is_authorized' ],
|
||||
'args' => [
|
||||
'business_description' => [
|
||||
'description' => __( 'The business description for a given store.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
],
|
||||
'images' => [
|
||||
'description' => __( 'The images for a given store.', 'woocommerce' ),
|
||||
'type' => 'object',
|
||||
],
|
||||
],
|
||||
],
|
||||
'schema' => [ $this->schema, 'get_public_item_schema' ],
|
||||
'allow_batch' => [ 'v1' => true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the content for the products.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
*
|
||||
* @return bool|string|\WP_Error|\WP_REST_Response
|
||||
*/
|
||||
protected function get_route_post_response( \WP_REST_Request $request ) {
|
||||
$allow_ai_connection = get_option( 'woocommerce_blocks_allow_ai_connection' );
|
||||
|
||||
if ( ! $allow_ai_connection ) {
|
||||
return rest_ensure_response(
|
||||
$this->error_to_response(
|
||||
new \WP_Error(
|
||||
'ai_connection_not_allowed',
|
||||
__( 'AI content generation is not allowed on this store. Update your store settings if you wish to enable this feature.', 'woocommerce' )
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$business_description = sanitize_text_field( wp_unslash( $request['business_description'] ) );
|
||||
|
||||
if ( empty( $business_description ) ) {
|
||||
$business_description = get_option( 'woo_ai_describe_store_description' );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
return rest_ensure_response(
|
||||
$this->prepare_item_for_response(
|
||||
[
|
||||
'ai_content_generated' => true,
|
||||
'product_content' => null,
|
||||
],
|
||||
$request
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$ai_connection = new Connection();
|
||||
|
||||
$site_id = $ai_connection->get_site_id();
|
||||
|
||||
if ( is_wp_error( $site_id ) ) {
|
||||
return $this->error_to_response( $site_id );
|
||||
}
|
||||
|
||||
$token = $ai_connection->get_jwt_token( $site_id );
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $this->error_to_response( $token );
|
||||
}
|
||||
|
||||
$images = $request['images'];
|
||||
|
||||
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_products ) ) {
|
||||
return $this->error_to_response( $populate_products );
|
||||
}
|
||||
|
||||
if ( ! isset( $populate_products['product_content'] ) ) {
|
||||
return $this->error_to_response( new \WP_Error( 'product_content_not_found', __( 'Product content not found.', 'woocommerce' ) ) );
|
||||
}
|
||||
|
||||
$product_content = $populate_products['product_content'];
|
||||
|
||||
$item = array(
|
||||
'ai_content_generated' => true,
|
||||
'product_content' => $product_content,
|
||||
);
|
||||
|
||||
return rest_ensure_response( $item );
|
||||
}
|
||||
}
|
||||
@@ -654,7 +654,7 @@ class Checkout extends AbstractCartRoute {
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'role' => 'customer',
|
||||
'source' => 'store-api,',
|
||||
'source' => 'store-api',
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\StoreApi\Routes\V1\AbstractRoute;
|
||||
|
||||
/**
|
||||
@@ -30,7 +29,7 @@ class RoutesController {
|
||||
public function __construct( SchemaController $schema_controller ) {
|
||||
$this->schema_controller = $schema_controller;
|
||||
$this->routes = [
|
||||
'v1' => [
|
||||
'v1' => [
|
||||
Routes\V1\Batch::IDENTIFIER => Routes\V1\Batch::class,
|
||||
Routes\V1\Cart::IDENTIFIER => Routes\V1\Cart::class,
|
||||
Routes\V1\CartAddItem::IDENTIFIER => Routes\V1\CartAddItem::class,
|
||||
@@ -48,7 +47,6 @@ class RoutesController {
|
||||
Routes\V1\Checkout::IDENTIFIER => Routes\V1\Checkout::class,
|
||||
Routes\V1\CheckoutOrder::IDENTIFIER => Routes\V1\CheckoutOrder::class,
|
||||
Routes\V1\Order::IDENTIFIER => Routes\V1\Order::class,
|
||||
Routes\V1\Patterns::IDENTIFIER => Routes\V1\Patterns::class,
|
||||
Routes\V1\ProductAttributes::IDENTIFIER => Routes\V1\ProductAttributes::class,
|
||||
Routes\V1\ProductAttributesById::IDENTIFIER => Routes\V1\ProductAttributesById::class,
|
||||
Routes\V1\ProductAttributeTerms::IDENTIFIER => Routes\V1\ProductAttributeTerms::class,
|
||||
@@ -61,6 +59,14 @@ class RoutesController {
|
||||
Routes\V1\ProductsById::IDENTIFIER => Routes\V1\ProductsById::class,
|
||||
Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class,
|
||||
],
|
||||
// @todo Migrate internal AI routes to WooCommerce Core codebase.
|
||||
'private' => [
|
||||
Routes\V1\AI\Images::IDENTIFIER => Routes\V1\AI\Images::class,
|
||||
Routes\V1\AI\Patterns::IDENTIFIER => Routes\V1\AI\Patterns::class,
|
||||
Routes\V1\AI\Product::IDENTIFIER => Routes\V1\AI\Product::class,
|
||||
Routes\V1\AI\Products::IDENTIFIER => Routes\V1\AI\Products::class,
|
||||
Routes\V1\AI\BusinessDescription::IDENTIFIER => Routes\V1\AI\BusinessDescription::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -70,6 +76,7 @@ class RoutesController {
|
||||
public function register_all_routes() {
|
||||
$this->register_routes( 'v1', 'wc/store' );
|
||||
$this->register_routes( 'v1', 'wc/store/v1' );
|
||||
$this->register_routes( 'private', 'wc/private' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,12 +49,16 @@ class SchemaController {
|
||||
Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class,
|
||||
Schemas\V1\OrderFeeSchema::IDENTIFIER => Schemas\V1\OrderFeeSchema::class,
|
||||
Schemas\V1\OrderSchema::IDENTIFIER => Schemas\V1\OrderSchema::class,
|
||||
Schemas\V1\PatternsSchema::IDENTIFIER => Schemas\V1\PatternsSchema::class,
|
||||
Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class,
|
||||
Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class,
|
||||
Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class,
|
||||
Schemas\V1\ProductCollectionDataSchema::IDENTIFIER => Schemas\V1\ProductCollectionDataSchema::class,
|
||||
Schemas\V1\ProductReviewSchema::IDENTIFIER => Schemas\V1\ProductReviewSchema::class,
|
||||
Schemas\V1\AI\ImagesSchema::IDENTIFIER => Schemas\V1\AI\ImagesSchema::class,
|
||||
Schemas\V1\AI\PatternsSchema::IDENTIFIER => Schemas\V1\AI\PatternsSchema::class,
|
||||
Schemas\V1\AI\ProductSchema::IDENTIFIER => Schemas\V1\AI\ProductSchema::class,
|
||||
Schemas\V1\AI\ProductsSchema::IDENTIFIER => Schemas\V1\AI\ProductsSchema::class,
|
||||
Schemas\V1\AI\BusinessDescriptionSchema::IDENTIFIER => Schemas\V1\AI\BusinessDescriptionSchema::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
|
||||
|
||||
/**
|
||||
* BusinessDescriptionSchema class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BusinessDescriptionSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'ai/business-description';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/business-description';
|
||||
|
||||
/**
|
||||
* Business Description schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Business Description response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return [
|
||||
'ai_content_generated' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
|
||||
|
||||
/**
|
||||
* ImagesSchema class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ImagesSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'ai/images';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/images';
|
||||
|
||||
/**
|
||||
* Images schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Images response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
|
||||
|
||||
/**
|
||||
* PatternsSchema class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PatternsSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'ai/patterns';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/patterns';
|
||||
|
||||
/**
|
||||
* Patterns schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Patterns response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return [
|
||||
'ai_content_generated' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
|
||||
|
||||
/**
|
||||
* ProductSchema class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ProductSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'ai/product';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/product';
|
||||
|
||||
/**
|
||||
* Patterns schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Product response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return [
|
||||
'ai_content_generated' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\StoreApi\Schemas\V1\AI;
|
||||
|
||||
use Automattic\WooCommerce\StoreApi\Schemas\V1\AbstractSchema;
|
||||
|
||||
/**
|
||||
* ProductsSchema class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ProductsSchema extends AbstractSchema {
|
||||
/**
|
||||
* The schema item name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title = 'ai/products';
|
||||
|
||||
/**
|
||||
* The schema item identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const IDENTIFIER = 'ai/products';
|
||||
|
||||
/**
|
||||
* Products schema properties.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_properties() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Products response.
|
||||
*
|
||||
* @param array $item Item to get response for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $item ) {
|
||||
return [
|
||||
'ai_content_generated' => $item['ai_content_generated'],
|
||||
'product_content' => $item['product_content'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -173,12 +173,18 @@ abstract class AbstractSchema {
|
||||
}
|
||||
|
||||
if ( ! $result || is_wp_error( $result ) ) {
|
||||
// If schema validation fails, we return here as we don't need to validate any deeper.
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ( isset( $property_value['properties'] ) ) {
|
||||
$validate_callback = $this->get_recursive_validate_callback( $property_value['properties'] );
|
||||
return $validate_callback( $current_value, $request, $param . ' > ' . $property_key );
|
||||
$result = $validate_callback( $current_value, $request, $param . ' > ' . $property_key );
|
||||
|
||||
if ( ! $result || is_wp_error( $result ) ) {
|
||||
// If schema validation fails, we return here as we don't need to validate any deeper.
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -380,7 +380,7 @@ class OrderSchema extends AbstractSchema {
|
||||
function( $item ) {
|
||||
return [
|
||||
'name' => $item->get_name(),
|
||||
'price' => $item->get_tax_total(),
|
||||
'price' => $this->prepare_money_response( $item->get_tax_total() ),
|
||||
'rate' => strval( $item->get_rate_percent() ),
|
||||
];
|
||||
},
|
||||
|
||||
@@ -159,9 +159,8 @@ class ProductReviewSchema extends AbstractSchema {
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_response( $review ) {
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true );
|
||||
$data = [
|
||||
$rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true );
|
||||
return [
|
||||
'id' => (int) $review->comment_ID,
|
||||
'date_created' => wc_rest_prepare_date_response( $review->comment_date ),
|
||||
'formatted_date_created' => get_comment_date( 'F j, Y', $review->comment_ID ),
|
||||
@@ -171,16 +170,10 @@ class ProductReviewSchema extends AbstractSchema {
|
||||
'product_permalink' => get_permalink( (int) $review->comment_post_ID ),
|
||||
'product_image' => $this->image_attachment_schema->get_item_response( get_post_thumbnail_id( (int) $review->comment_post_ID ) ),
|
||||
'reviewer' => $review->comment_author,
|
||||
'review' => $review->comment_content,
|
||||
'review' => wpautop( $review->comment_content ),
|
||||
'rating' => $rating,
|
||||
'verified' => wc_review_is_from_verified_owner( $review->comment_ID ),
|
||||
'reviewer_avatar_urls' => rest_get_avatar_urls( $review->comment_author_email ),
|
||||
];
|
||||
|
||||
if ( 'view' === $context ) {
|
||||
$data['review'] = wpautop( $data['review'] );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
|
||||
private static function group_blocks( $parsed_blocks ) {
|
||||
return array_reduce(
|
||||
$parsed_blocks,
|
||||
function( $carry, $block ) {
|
||||
function( array $carry, array $block ) {
|
||||
if ( 'core/template-part' === $block['blockName'] ) {
|
||||
$carry[] = array( $block );
|
||||
return $carry;
|
||||
|
||||
@@ -183,7 +183,8 @@ class BlockTemplateUtils {
|
||||
|
||||
/**
|
||||
* Build a unified template object based on a theme file.
|
||||
* Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
|
||||
*
|
||||
* @internal Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
|
||||
*
|
||||
* @param array|object $template_file Theme file.
|
||||
* @param string $template_type wp_template or wp_template_part.
|
||||
@@ -223,8 +224,16 @@ class BlockTemplateUtils {
|
||||
$template->area = 'uncategorized';
|
||||
|
||||
// Force the Mini-Cart template part to be in the Mini-Cart template part area.
|
||||
if ( 'wp_template_part' === $template_type && 'mini-cart' === $template_file->slug ) {
|
||||
$template->area = 'mini-cart';
|
||||
// @todo When this class is refactored, move title, description, and area definition to the template classes (CheckoutHeaderTemplate, MiniCartTemplate, etc).
|
||||
if ( 'wp_template_part' === $template_type ) {
|
||||
switch ( $template_file->slug ) {
|
||||
case 'mini-cart':
|
||||
$template->area = 'mini-cart';
|
||||
break;
|
||||
case 'checkout-header':
|
||||
$template->area = 'header';
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $template;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Utils;
|
||||
|
||||
use WP_Query;
|
||||
|
||||
/**
|
||||
* Utility methods used for the Product Collection block.
|
||||
* {@internal This class and its methods are not intended for public use.}
|
||||
*/
|
||||
class ProductCollectionUtils {
|
||||
/**
|
||||
* Prepare and execute a query for the Product Collection block.
|
||||
* This method is used by the Product Collection block and the No Results block.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
*/
|
||||
public static function prepare_and_execute_query( $block ) {
|
||||
$page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page';
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ];
|
||||
|
||||
// Use global query if needed.
|
||||
$use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] );
|
||||
if ( $use_global_query ) {
|
||||
global $wp_query;
|
||||
$query = clone $wp_query;
|
||||
} else {
|
||||
$query_args = build_query_vars_from_query_block( $block, $page );
|
||||
$query = new WP_Query( $query_args );
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Blocks\Utils;
|
||||
* {@internal This class and its methods are not intended for public use.}
|
||||
*/
|
||||
class ProductGalleryUtils {
|
||||
const CROP_IMAGE_SIZE_NAME = '_woo_blocks_product_gallery_crop_full';
|
||||
|
||||
/**
|
||||
* When requesting a full-size image, this function may return an array with a single image.
|
||||
@@ -18,9 +19,10 @@ class ProductGalleryUtils {
|
||||
* @param string $size Image size.
|
||||
* @param array $attributes Attributes.
|
||||
* @param string $wrapper_class Wrapper class.
|
||||
* @param bool $crop_images Whether to crop images.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_product_gallery_images( $post_id, $size = 'full', $attributes = array(), $wrapper_class = '' ) {
|
||||
public static function get_product_gallery_images( $post_id, $size = 'full', $attributes = array(), $wrapper_class = '', $crop_images = false ) {
|
||||
$product_gallery_images = array();
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
@@ -28,7 +30,13 @@ class ProductGalleryUtils {
|
||||
$all_product_gallery_image_ids = self::get_product_gallery_image_ids( $product );
|
||||
|
||||
if ( 'full' === $size || 'full' !== $size && count( $all_product_gallery_image_ids ) > 1 ) {
|
||||
$size = $crop_images ? self::CROP_IMAGE_SIZE_NAME : $size;
|
||||
|
||||
foreach ( $all_product_gallery_image_ids as $product_gallery_image_id ) {
|
||||
if ( $crop_images ) {
|
||||
self::maybe_generate_intermediate_image( $product_gallery_image_id, self::CROP_IMAGE_SIZE_NAME );
|
||||
}
|
||||
|
||||
$product_image_html = wp_get_attachment_image(
|
||||
$product_gallery_image_id,
|
||||
$size,
|
||||
@@ -93,4 +101,47 @@ class ProductGalleryUtils {
|
||||
|
||||
return $unique_image_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the intermediate image sizes only when needed.
|
||||
*
|
||||
* @param int $attachment_id Attachment ID.
|
||||
* @param string $size Image size.
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_generate_intermediate_image( $attachment_id, $size ) {
|
||||
$metadata = image_get_intermediate_size( $attachment_id, $size );
|
||||
$upload_dir = wp_upload_dir();
|
||||
$image_path = '';
|
||||
|
||||
if ( $metadata ) {
|
||||
$image_path = $upload_dir['basedir'] . '/' . $metadata['path'];
|
||||
}
|
||||
|
||||
/*
|
||||
* We need to check both if the size metadata exists and if the file exists.
|
||||
* Sometimes we can have orphaned image file and no metadata or vice versa.
|
||||
*/
|
||||
if ( $metadata && file_exists( $image_path ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image_path = wp_get_original_image_path( $attachment_id );
|
||||
$image_metadata = wp_get_attachment_metadata( $attachment_id );
|
||||
|
||||
// If image sizes are not available. Bail.
|
||||
if ( ! isset( $image_metadata['width'], $image_metadata['height'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* We want to take the minimum dimension of the image and
|
||||
* use that size as the crop size for the new image.
|
||||
*/
|
||||
$min_size = min( $image_metadata['width'], $image_metadata['height'] );
|
||||
$new_image_metadata = image_make_intermediate_size( $image_path, $min_size, $min_size, true );
|
||||
$image_metadata['sizes'][ $size ] = $new_image_metadata;
|
||||
|
||||
wp_update_attachment_metadata( $attachment_id, $image_metadata );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user