plugin updates
This commit is contained in:
@@ -93,6 +93,7 @@ class Init {
|
||||
'Automattic\WooCommerce\Admin\API\OnboardingTasks',
|
||||
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
|
||||
'Automattic\WooCommerce\Admin\API\OnboardingPlugins',
|
||||
'Automattic\WooCommerce\Admin\API\OnboardingProducts',
|
||||
'Automattic\WooCommerce\Admin\API\NavigationFavorites',
|
||||
'Automattic\WooCommerce\Admin\API\Taxes',
|
||||
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Automattic\WooCommerce\Admin\API;
|
||||
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs;
|
||||
use Automattic\WooCommerce\Admin\Features\MarketingRecommendations\Init as MarketingRecommendationsInit;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -103,16 +104,9 @@ class Marketing extends \WC_REST_Data_Controller {
|
||||
* @return \WP_Error|\WP_REST_Response
|
||||
*/
|
||||
public function get_recommended_plugins( $request ) {
|
||||
/**
|
||||
* MarketingSpecs class.
|
||||
*
|
||||
* @var MarketingSpecs $marketing_specs
|
||||
*/
|
||||
$marketing_specs = wc_get_container()->get( MarketingSpecs::class );
|
||||
|
||||
// Default to marketing category (if no category set).
|
||||
$category = ( ! empty( $request->get_param( 'category' ) ) ) ? $request->get_param( 'category' ) : 'marketing';
|
||||
$all_plugins = $marketing_specs->get_recommended_plugins();
|
||||
$all_plugins = MarketingRecommendationsInit::get_recommended_plugins();
|
||||
$valid_plugins = [];
|
||||
$per_page = $request->get_param( 'per_page' );
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class MarketingCampaigns extends WC_REST_Controller {
|
||||
$marketing_channels_service = wc_get_container()->get( MarketingChannelsService::class );
|
||||
|
||||
// Aggregate the campaigns from all registered marketing channels.
|
||||
$responses = [];
|
||||
$responses = array();
|
||||
foreach ( $marketing_channels_service->get_registered_channels() as $channel ) {
|
||||
foreach ( $channel->get_campaigns() as $campaign ) {
|
||||
$response = $this->prepare_item_for_response( $campaign, $request );
|
||||
@@ -141,18 +141,25 @@ class MarketingCampaigns extends WC_REST_Controller {
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function prepare_item_for_response( $item, $request ) {
|
||||
$data = [
|
||||
$data = array(
|
||||
'id' => $item->get_id(),
|
||||
'channel' => $item->get_type()->get_channel()->get_slug(),
|
||||
'title' => $item->get_title(),
|
||||
'manage_url' => $item->get_manage_url(),
|
||||
];
|
||||
);
|
||||
|
||||
if ( $item->get_cost() instanceof Price ) {
|
||||
$data['cost'] = [
|
||||
$data['cost'] = array(
|
||||
'value' => wc_format_decimal( $item->get_cost()->get_value() ),
|
||||
'currency' => $item->get_cost()->get_currency(),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
if ( $item->get_sales() instanceof Price ) {
|
||||
$data['sales'] = array(
|
||||
'value' => wc_format_decimal( $item->get_sales()->get_value() ),
|
||||
'currency' => $item->get_sales()->get_currency(),
|
||||
);
|
||||
}
|
||||
|
||||
$context = $request['context'] ?? 'view';
|
||||
@@ -168,55 +175,73 @@ class MarketingCampaigns extends WC_REST_Controller {
|
||||
* @return array Item schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$schema = [
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'marketing_campaign',
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'id' => [
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'The unique identifier for the marketing campaign.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
'channel' => [
|
||||
),
|
||||
'channel' => array(
|
||||
'description' => __( 'The unique identifier for the marketing channel that this campaign belongs to.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
'title' => [
|
||||
),
|
||||
'title' => array(
|
||||
'description' => __( 'Title of the marketing campaign.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
'manage_url' => [
|
||||
),
|
||||
'manage_url' => array(
|
||||
'description' => __( 'URL to the campaign management page.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
'cost' => [
|
||||
),
|
||||
'cost' => array(
|
||||
'description' => __( 'Cost of the marketing campaign.', 'woocommerce' ),
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
'type' => 'object',
|
||||
'properties' => [
|
||||
'value' => [
|
||||
'properties' => array(
|
||||
'value' => array(
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
'currency' => [
|
||||
),
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'context' => [ 'view' ],
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
),
|
||||
),
|
||||
),
|
||||
'sales' => array(
|
||||
'description' => __( 'Sales of the marketing campaign.', 'woocommerce' ),
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'value' => array(
|
||||
'type' => 'string',
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'context' => array( 'view' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs;
|
||||
use Automattic\WooCommerce\Admin\Features\MarketingRecommendations\Init as MarketingRecommendationsInit;
|
||||
use WC_REST_Controller;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
@@ -88,18 +88,11 @@ class MarketingRecommendations extends WC_REST_Controller {
|
||||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
/**
|
||||
* MarketingSpecs class.
|
||||
*
|
||||
* @var MarketingSpecs $marketing_specs
|
||||
*/
|
||||
$marketing_specs = wc_get_container()->get( MarketingSpecs::class );
|
||||
|
||||
$category = $request->get_param( 'category' );
|
||||
if ( 'channels' === $category ) {
|
||||
$items = $marketing_specs->get_recommended_marketing_channels();
|
||||
$items = MarketingRecommendationsInit::get_recommended_marketing_channels();
|
||||
} elseif ( 'extensions' === $category ) {
|
||||
$items = $marketing_specs->get_recommended_marketing_extensions_excluding_channels();
|
||||
$items = MarketingRecommendationsInit::get_recommended_marketing_extensions_excluding_channels();
|
||||
} else {
|
||||
return new WP_Error( 'woocommerce_rest_invalid_category', __( 'The specified category for recommendations is invalid. Allowed values: "channels", "extensions".', 'woocommerce' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API Onboarding Themes Controller
|
||||
*
|
||||
* Handles requests to install and activate themes.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\API;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Onboarding Themes Controller.
|
||||
*
|
||||
* @internal
|
||||
* @extends WC_REST_Data_Controller
|
||||
*/
|
||||
class OnboardingProducts extends \WC_REST_Data_Controller {
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc-admin';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'onboarding';
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/products',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_products' ),
|
||||
'permission_callback' => array( $this, 'update_item_permissions_check' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create products.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error|WP_REST_Response
|
||||
*/
|
||||
public function create_products( $request ) {
|
||||
$update_products = new UpdateProducts();
|
||||
|
||||
$products = $update_products->fetch_dummy_products_to_update();
|
||||
|
||||
if ( is_wp_error( $products ) ) {
|
||||
return rest_ensure_response( array( 'success' => false ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( array( 'success' => true ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given request has access to manage themes.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error|boolean
|
||||
*/
|
||||
public function update_item_permissions_check( $request ) {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot create dummy products.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -389,21 +389,6 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||
'sanitize_callback' => 'wp_parse_slug_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'enum' => array(
|
||||
'jetpack',
|
||||
'jetpack-boost',
|
||||
'woocommerce-services',
|
||||
'woocommerce-payments',
|
||||
'mailchimp-for-woocommerce',
|
||||
'creative-mail-by-constant-contact',
|
||||
'facebook-for-woocommerce',
|
||||
'google-listings-and-ads',
|
||||
'pinterest-for-woocommerce',
|
||||
'mailpoet',
|
||||
'codistoconnect',
|
||||
'tiktok-for-business',
|
||||
'tiktok-for-business:alt',
|
||||
),
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
|
||||
@@ -304,6 +304,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
$cache_key = $this->get_cache_key( $query_args );
|
||||
$data = $this->get_cached_data( $cache_key );
|
||||
|
||||
if ( isset( $query_args['date_type'] ) ) {
|
||||
$this->date_column_name = $query_args['date_type'];
|
||||
}
|
||||
|
||||
if ( false === $data ) {
|
||||
$this->initialize_queries();
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class Controller extends GenericStatsController implements ExportableInterface {
|
||||
$args['segmentby'] = $request['segmentby'];
|
||||
$args['fields'] = $request['fields'];
|
||||
$args['force_cache_refresh'] = $request['force_cache_refresh'];
|
||||
$args['date_type'] = $request['date_type'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
@@ -268,6 +269,16 @@ class Controller extends GenericStatsController implements ExportableInterface {
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['date_type'] = array(
|
||||
'description' => __( 'Override the "woocommerce_date_type" option that is used for the database date field considered for revenue reports.', 'woocommerce' ),
|
||||
'type' => 'string',
|
||||
'enum' => array(
|
||||
'date_paid',
|
||||
'date_created',
|
||||
'date_completed',
|
||||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
@@ -33,4 +33,11 @@ interface BlockTemplateInterface extends ContainerInterface {
|
||||
* @return string
|
||||
*/
|
||||
public function generate_block_id( string $id_base ): string;
|
||||
|
||||
/**
|
||||
* Get the template as JSON like array.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
public function to_json(): array;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
/**
|
||||
* Gets a list of fallback methods if remote fetching is disabled.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Default Marketing Recommendations
|
||||
*/
|
||||
class DefaultMarketingRecommendations {
|
||||
/**
|
||||
* Get default specs.
|
||||
*
|
||||
* @return array Default specs.
|
||||
*/
|
||||
public static function get_all() {
|
||||
// Icon directory URL.
|
||||
$icon_dir_url = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing';
|
||||
|
||||
$utm_string = '?utm_source=marketingtab&utm_medium=product&utm_campaign=wcaddons';
|
||||
|
||||
// Categories. Note that these are keys used in code, not texts to be displayed in the UI.
|
||||
$marketing = 'marketing';
|
||||
$coupons = 'coupons';
|
||||
|
||||
// Subcategories.
|
||||
$sales_channels = array(
|
||||
'slug' => 'sales-channels',
|
||||
'name' => __( 'Sales channels', 'woocommerce' ),
|
||||
);
|
||||
$email = array(
|
||||
'slug' => 'email',
|
||||
'name' => __( 'Email', 'woocommerce' ),
|
||||
);
|
||||
$automations = array(
|
||||
'slug' => 'automations',
|
||||
'name' => __( 'Automations', 'woocommerce' ),
|
||||
);
|
||||
$conversion = array(
|
||||
'slug' => 'conversion',
|
||||
'name' => __( 'Conversion', 'woocommerce' ),
|
||||
);
|
||||
$crm = array(
|
||||
'slug' => 'crm',
|
||||
'name' => __( 'CRM', 'woocommerce' ),
|
||||
);
|
||||
|
||||
// Tags.
|
||||
$built_by_woocommerce = array(
|
||||
'slug' => 'built-by-woocommerce',
|
||||
'name' => __( 'Built by WooCommerce', 'woocommerce' ),
|
||||
);
|
||||
|
||||
return array(
|
||||
array(
|
||||
'title' => 'Google Listings and Ads',
|
||||
'description' => __( 'Get in front of shoppers and drive traffic so you can grow your business with Smart Shopping Campaigns and free listings.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/google-listings-and-ads/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/google.svg",
|
||||
'product' => 'google-listings-and-ads',
|
||||
'plugin' => 'google-listings-and-ads/google-listings-and-ads.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$sales_channels,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'Pinterest for WooCommerce',
|
||||
'description' => __( 'Grow your business on Pinterest! Use this official plugin to allow shoppers to Pin products while browsing your store, track conversions, and advertise on Pinterest.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/pinterest-for-woocommerce/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/pinterest.svg",
|
||||
'product' => 'pinterest-for-woocommerce',
|
||||
'plugin' => 'pinterest-for-woocommerce/pinterest-for-woocommerce.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$sales_channels,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'TikTok for WooCommerce',
|
||||
'description' => __( 'Create advertising campaigns and reach one billion global users with TikTok for WooCommerce.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/tiktok-for-woocommerce/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/tiktok.jpg",
|
||||
'product' => 'tiktok-for-business',
|
||||
'plugin' => 'tiktok-for-business/tiktok-for-woocommerce.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$sales_channels,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'MailPoet',
|
||||
'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/mailpoet/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/mailpoet.svg",
|
||||
'product' => 'mailpoet',
|
||||
'plugin' => 'mailpoet/mailpoet.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$email,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'Mailchimp for WooCommerce',
|
||||
'description' => __( 'Send targeted campaigns, recover abandoned carts and more with Mailchimp.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/mailchimp-for-woocommerce/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/mailchimp.svg",
|
||||
'product' => 'mailchimp-for-woocommerce',
|
||||
'plugin' => 'mailchimp-for-woocommerce/mailchimp-woocommerce.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$email,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Klaviyo for WooCommerce',
|
||||
'description' => __( 'Grow and retain customers with intelligent, impactful email and SMS marketing automation and a consolidated view of customer interactions.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/klaviyo-for-woocommerce/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/klaviyo.png",
|
||||
'product' => 'klaviyo',
|
||||
'plugin' => 'klaviyo/klaviyo.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$email,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'AutomateWoo',
|
||||
'description' => __( 'Convert and retain customers with automated marketing that does the hard work for you.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/automatewoo/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/automatewoo.svg",
|
||||
'product' => 'automatewoo',
|
||||
'plugin' => 'automatewoo/automatewoo.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$automations,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'AutomateWoo Refer a Friend',
|
||||
'description' => __( 'Boost your organic sales by adding a customer referral program to your WooCommerce store.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/automatewoo-refer-a-friend/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/automatewoo.svg",
|
||||
'product' => 'automatewoo-referrals',
|
||||
'plugin' => 'automatewoo-referrals/automatewoo-referrals.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$automations,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'AutomateWoo Birthdays',
|
||||
'description' => __( 'Delight customers and boost organic sales with a special WooCommerce birthday email (and coupon!) on their special day.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/automatewoo-birthdays/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/automatewoo.svg",
|
||||
'product' => 'automatewoo-birthdays',
|
||||
'plugin' => 'automatewoo-birthdays/automatewoo-birthdays.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$automations,
|
||||
),
|
||||
'tags' => array(
|
||||
$built_by_woocommerce,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'title' => 'Trustpilot Reviews',
|
||||
'description' => __( 'Collect and showcase verified reviews that consumers trust.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/trustpilot-reviews/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/trustpilot.png",
|
||||
'product' => 'trustpilot-reviews',
|
||||
'plugin' => 'trustpilot-reviews/wc_trustpilot.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$conversion,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Vimeo for WooCommerce',
|
||||
'description' => __( 'Turn your product images into stunning videos that engage and convert audiences - no video experience required.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/vimeo/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/vimeo.png",
|
||||
'product' => 'vimeo',
|
||||
'plugin' => 'vimeo/Core.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$conversion,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Jetpack CRM for WooCommerce',
|
||||
'description' => __( 'Harness data from WooCommerce to grow your business. Manage leads, customers, and segments, through automation, quotes, invoicing, billing, and email marketing. Power up your store with CRM.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/jetpack-crm/{$utm_string}",
|
||||
'direct_install' => true,
|
||||
'icon' => "{$icon_dir_url}/jetpack-crm.svg",
|
||||
'product' => 'zero-bs-crm',
|
||||
'plugin' => 'zero-bs-crm/ZeroBSCRM.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$crm,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'WooCommerce Zapier',
|
||||
'description' => __( 'Integrate your WooCommerce store with 5000+ cloud apps and services today. Trusted by 11,000+ users.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/woocommerce-zapier/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/zapier.png",
|
||||
'product' => 'woocommerce-zapier',
|
||||
'plugin' => 'woocommerce-zapier/woocommerce-zapier.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$crm,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Salesforce',
|
||||
'description' => __( 'Sync your website\'s data like contacts, products, and orders over Salesforce CRM with Salesforce Integration for WooCommerce.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/integration-with-salesforce-for-woocommerce/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/salesforce.jpg",
|
||||
'product' => 'integration-with-salesforce',
|
||||
'plugin' => 'integration-with-salesforce/integration-with-salesforce.php',
|
||||
'categories' => array(
|
||||
$marketing,
|
||||
),
|
||||
'subcategories' => array(
|
||||
$crm,
|
||||
),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Personalized Coupons',
|
||||
'description' => __( 'Generate dynamic personalized coupons for your customers that increase purchase rates.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/automatewoo/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/automatewoo-personalized-coupons.svg",
|
||||
'product' => 'automatewoo',
|
||||
'plugin' => 'automatewoo/automatewoo.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Smart Coupons',
|
||||
'description' => __( 'Powerful, "all in one" solution for gift certificates, store credits, discount coupons and vouchers.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/smart-coupons/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/woocommerce-smart-coupons.svg",
|
||||
'product' => 'woocommerce-smart-coupons',
|
||||
'plugin' => 'woocommerce-smart-coupons/woocommerce-smart-coupons.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'URL Coupons',
|
||||
'description' => __( 'Create a unique URL that applies a discount and optionally adds one or more products to the customer\'s cart.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/url-coupons/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/woocommerce-url-coupons.svg",
|
||||
'product' => 'woocommerce-url-coupons',
|
||||
'plugin' => 'woocommerce-url-coupons/woocommerce-url-coupons.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'WooCommerce Store Credit',
|
||||
'description' => __( 'Create "store credit" coupons for customers which are redeemable at checkout.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/store-credit/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/woocommerce-store-credit.svg",
|
||||
'product' => 'woocommerce-store-credit',
|
||||
'plugin' => 'woocommerce-store-credit/woocommerce-store-credit.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Free Gift Coupons',
|
||||
'description' => __( 'Give away a free item to any customer with the coupon code.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/free-gift-coupons/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/woocommerce-free-gift-coupons.svg",
|
||||
'product' => 'woocommerce-free-gift-coupons',
|
||||
'plugin' => 'woocommerce-free-gift-coupons/woocommerce-free-gift-coupons.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
array(
|
||||
'title' => 'Group Coupons',
|
||||
'description' => __( 'Coupons for groups. Provides the option to have coupons that are restricted to group members or roles. Works with the free Groups plugin.', 'woocommerce' ),
|
||||
'url' => "https://woo.com/products/group-coupons/{$utm_string}",
|
||||
'direct_install' => false,
|
||||
'icon' => "{$icon_dir_url}/woocommerce-group-coupons.svg",
|
||||
'product' => 'woocommerce-group-coupons',
|
||||
'plugin' => 'woocommerce-group-coupons/woocommerce-group-coupons.php',
|
||||
'categories' => array(
|
||||
$coupons,
|
||||
),
|
||||
'subcategories' => array(),
|
||||
'tags' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Marketing Recommendations engine.
|
||||
* This goes through the specs and gets marketing recommendations.
|
||||
*/
|
||||
class Init {
|
||||
/**
|
||||
* Slug of the category specifying marketing extensions on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing';
|
||||
|
||||
/**
|
||||
* Slug of the subcategory specifying marketing channels on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'woocommerce_updated', array( __CLASS__, 'delete_specs_transient' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specs transient.
|
||||
*/
|
||||
public static function delete_specs_transient() {
|
||||
MarketingRecommendationsDataSourcePoller::get_instance()->delete_specs_transient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specs or fetch remotely if they don't exist.
|
||||
*/
|
||||
public static function get_specs() {
|
||||
if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
|
||||
return DefaultMarketingRecommendations::get_all();
|
||||
}
|
||||
$specs = MarketingRecommendationsDataSourcePoller::get_instance()->get_specs_from_data_sources();
|
||||
|
||||
// Fetch specs if they don't yet exist.
|
||||
if ( ! is_array( $specs ) || 0 === count( $specs ) ) {
|
||||
return DefaultMarketingRecommendations::get_all();
|
||||
}
|
||||
|
||||
return $specs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load recommended plugins from Woo.com
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recommended_plugins(): array {
|
||||
$specs = self::get_specs();
|
||||
$result = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
$result[] = self::object_to_array( $spec );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the recommended marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recommended_marketing_channels(): array {
|
||||
return array_filter(
|
||||
self::get_recommended_plugins(),
|
||||
function ( array $plugin_data ) {
|
||||
return self::is_marketing_channel_plugin( $plugin_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all recommended marketing extensions EXCEPT the marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recommended_marketing_extensions_excluding_channels(): array {
|
||||
return array_filter(
|
||||
self::get_recommended_plugins(),
|
||||
function ( array $plugin_data ) {
|
||||
return self::is_marketing_plugin( $plugin_data ) && ! self::is_marketing_channel_plugin( $plugin_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing extension.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_marketing_plugin( array $plugin_data ): bool {
|
||||
$categories = $plugin_data['categories'] ?? array();
|
||||
|
||||
return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing channel.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function is_marketing_channel_plugin( array $plugin_data ): bool {
|
||||
if ( ! self::is_marketing_plugin( $plugin_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subcategories = $plugin_data['subcategories'] ?? array();
|
||||
foreach ( $subcategories as $subcategory ) {
|
||||
if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object to an array.
|
||||
* This is used to convert the specs to an array so that they can be returned by the API.
|
||||
*
|
||||
* @param mixed $obj Object to convert.
|
||||
* @return array
|
||||
*/
|
||||
protected static function object_to_array( $obj ) {
|
||||
if ( is_object( $obj ) ) {
|
||||
$obj = (array) $obj;
|
||||
}
|
||||
if ( is_array( $obj ) ) {
|
||||
$new = array();
|
||||
foreach ( $obj as $key => $val ) {
|
||||
$new[ $key ] = self::object_to_array( $val );
|
||||
}
|
||||
} else {
|
||||
$new = $obj;
|
||||
}
|
||||
return $new;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;
|
||||
|
||||
use Automattic\WooCommerce\Admin\DataSourcePoller;
|
||||
|
||||
/**
|
||||
* Specs data source poller class for marketing recommendations.
|
||||
*/
|
||||
class MarketingRecommendationsDataSourcePoller extends DataSourcePoller {
|
||||
|
||||
/**
|
||||
* Data Source Poller ID.
|
||||
*/
|
||||
const ID = 'marketing_recommendations';
|
||||
|
||||
/**
|
||||
* Default data sources array.
|
||||
*/
|
||||
const DATA_SOURCES = array(
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json',
|
||||
);
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var Analytics instance
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self(
|
||||
self::ID,
|
||||
self::DATA_SOURCES,
|
||||
array(
|
||||
'spec_key' => 'product',
|
||||
)
|
||||
);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class CustomizeStore extends Task {
|
||||
global $_GET;
|
||||
$theme_switch_via_cys_ai_loader = isset( $_GET['theme_switch_via_cys_ai_loader'] ) ? 1 === absint( $_GET['theme_switch_via_cys_ai_loader'] ) : false;
|
||||
if ( ! $theme_switch_via_cys_ai_loader ) {
|
||||
add_action( 'switch_theme', array( $this, 'mark_task_as_complete' ) );
|
||||
add_action( 'switch_theme', array( $this, 'mark_task_as_complete' ) );
|
||||
}
|
||||
|
||||
// Hook to remove unwanted UI elements when users are viewing with ?cys-hide-admin-bar=true.
|
||||
|
||||
@@ -88,15 +88,8 @@ class WooCommercePayments extends Task {
|
||||
* @return string
|
||||
*/
|
||||
public function get_additional_info() {
|
||||
if ( WCPayPromotionInit::is_woopay_eligible() ) {
|
||||
return __(
|
||||
'By using WooPayments you agree to be bound by our <a href="https://wordpress.com/tos/" target="_blank">Terms of Service</a> (including WooPay <a href="https://wordpress.com/tos/#more-woopay-specifically" target="_blank">merchant terms</a>) and acknowledge that you have read our <a href="https://automattic.com/privacy/" target="_blank">Privacy Policy</a>',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
return __(
|
||||
'By using WooPayments you agree to be bound by our <a href="https://wordpress.com/tos/" target="_blank">Terms of Service</a> and acknowledge that you have read our <a href="https://automattic.com/privacy/" target="_blank">Privacy Policy</a>',
|
||||
'Accept credit/debit cards and other popular payment methods with no setup or monthly fees — and manage payments right from your store dashboard.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,19 @@ class DefaultPaymentGateways {
|
||||
'AU',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'id' => 'airwallex_main',
|
||||
'title' => __( 'Airwallex Payments', 'woocommerce' ),
|
||||
'content' => __( 'Boost international sales and save on FX fees. Accept 60+ local payment methods including Apple Pay and Google Pay.', 'woocommerce' ),
|
||||
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/airwallex.png',
|
||||
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/airwallex.png',
|
||||
'plugins' => array( 'airwallex-online-payments-gateway' ),
|
||||
'is_visible' => array(
|
||||
self::get_rules_for_countries( array( 'GB', 'AT', 'BE', 'EE', 'FR', 'DE', 'GR', 'IE', 'IT', 'NL', 'PL', 'PT', 'AU', 'NZ', 'HK', 'SG', 'CN' ) ),
|
||||
),
|
||||
'category_other' => array( 'GB', 'AT', 'BE', 'EE', 'FR', 'DE', 'GR', 'IE', 'IT', 'NL', 'PL', 'PT', 'AU', 'NZ', 'HK', 'SG', 'CN' ),
|
||||
'category_additional' => array(),
|
||||
),
|
||||
array(
|
||||
'id' => 'amazon_payments_advanced',
|
||||
'title' => __( 'Amazon Pay', 'woocommerce' ),
|
||||
@@ -177,23 +190,8 @@ class DefaultPaymentGateways {
|
||||
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png',
|
||||
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/eway.png',
|
||||
'plugins' => array( 'woocommerce-gateway-eway' ),
|
||||
'is_visible' => array(
|
||||
self::get_rules_for_countries(
|
||||
array(
|
||||
'NZ',
|
||||
'HK',
|
||||
'SG',
|
||||
'AU',
|
||||
)
|
||||
),
|
||||
self::get_rules_for_cbd( false ),
|
||||
),
|
||||
'category_other' => array(
|
||||
'NZ',
|
||||
'HK',
|
||||
'SG',
|
||||
'AU',
|
||||
),
|
||||
'is_visible' => false,
|
||||
'category_other' => array(),
|
||||
'category_additional' => array(),
|
||||
),
|
||||
array(
|
||||
@@ -778,12 +776,12 @@ class DefaultPaymentGateways {
|
||||
'operands' => (object) array(
|
||||
(object) array(
|
||||
'type' => 'not',
|
||||
'operand' => [
|
||||
'operand' => array(
|
||||
(object) array(
|
||||
'type' => 'plugins_activated',
|
||||
'plugins' => [ 'woocommerce-admin' ],
|
||||
'plugins' => array( 'woocommerce-admin' ),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
(object) array(
|
||||
'type' => 'plugin_version',
|
||||
@@ -871,21 +869,9 @@ class DefaultPaymentGateways {
|
||||
'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/zipco.png',
|
||||
'image_72x72' => WC_ADMIN_IMAGES_FOLDER_URL . '/payment_methods/72x72/zipco.png',
|
||||
'plugins' => array( 'zipmoney-payments-woocommerce' ),
|
||||
'is_visible' => array(
|
||||
self::get_rules_for_countries(
|
||||
array(
|
||||
'US',
|
||||
'NZ',
|
||||
'AU',
|
||||
)
|
||||
),
|
||||
),
|
||||
'is_visible' => false,
|
||||
'category_other' => array(),
|
||||
'category_additional' => array(
|
||||
'US',
|
||||
'NZ',
|
||||
'AU',
|
||||
),
|
||||
'category_additional' => array(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -980,9 +966,9 @@ class DefaultPaymentGateways {
|
||||
),
|
||||
),
|
||||
'option_name' => 'woocommerce_onboarding_profile',
|
||||
'operation' => 'contains',
|
||||
'value' => 'no_im_selling_offline',
|
||||
'default' => array(),
|
||||
'operation' => 'in',
|
||||
'value' => array( 'no_im_selling_offline', 'im_selling_both_online_and_offline' ),
|
||||
'default' => '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1026,7 +1012,7 @@ class DefaultPaymentGateways {
|
||||
*/
|
||||
private static function get_recommendation_priority( $gateway_id, $country_code ) {
|
||||
$recommendation_priority_map = array(
|
||||
'US' => [
|
||||
'US' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
@@ -1035,9 +1021,8 @@ class DefaultPaymentGateways {
|
||||
'affirm',
|
||||
'afterpay',
|
||||
'klarna_payments',
|
||||
'zipmoney',
|
||||
],
|
||||
'CA' => [
|
||||
),
|
||||
'CA' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
@@ -1045,42 +1030,44 @@ class DefaultPaymentGateways {
|
||||
'affirm',
|
||||
'afterpay',
|
||||
'klarna_payments',
|
||||
],
|
||||
'AT' => [
|
||||
),
|
||||
'AT' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
),
|
||||
'BE' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
),
|
||||
'BG' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'HR' => array( 'ppcp-gateway' ),
|
||||
'CH' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'BE' => [
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'BG' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'HR' => [ 'ppcp-gateway' ],
|
||||
'CH' => [
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
],
|
||||
'CY' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
|
||||
'CZ' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'DK' => [
|
||||
),
|
||||
'CY' => array( 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
|
||||
'CZ' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'DK' => array(
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'EE' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'ES' => [
|
||||
),
|
||||
'EE' => array( 'stripe', 'ppcp-gateway', 'airwallex_main' ),
|
||||
'ES' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
@@ -1088,153 +1075,158 @@ class DefaultPaymentGateways {
|
||||
'square_credit_card',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'FI' => [
|
||||
),
|
||||
'FI' => array(
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'kco',
|
||||
'klarna_payments',
|
||||
],
|
||||
'FR' => [
|
||||
),
|
||||
'FR' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'square_credit_card',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'DE' => [
|
||||
),
|
||||
'DE' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'GB' => [
|
||||
),
|
||||
'GB' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'square_credit_card',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'GR' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'HU' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
|
||||
'IE' => [
|
||||
),
|
||||
'GR' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'HU' => array( 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
|
||||
'IE' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'square_credit_card',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'IT' => [
|
||||
),
|
||||
'IT' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'LV' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'LT' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'LU' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
|
||||
'MT' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'NL' => [
|
||||
),
|
||||
'LV' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'LT' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'LU' => array( 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
|
||||
'MT' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'NL' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'NO' => [ 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ],
|
||||
'PL' => [
|
||||
),
|
||||
'NO' => array( 'stripe', 'ppcp-gateway', 'kco', 'klarna_payments' ),
|
||||
'PL' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'airwallex_main',
|
||||
'mollie_wc_gateway_banktransfer',
|
||||
'klarna_payments',
|
||||
],
|
||||
'PT' => [
|
||||
),
|
||||
'PT' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'RO' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'SK' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'SL' => [ 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ],
|
||||
'SE' => [
|
||||
),
|
||||
'RO' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'SK' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'SL' => array( 'stripe', 'ppcp-gateway', 'amazon_payments_advanced' ),
|
||||
'SE' => array(
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'kco',
|
||||
'klarna_payments',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'MX' => [
|
||||
),
|
||||
'MX' => array(
|
||||
'stripe',
|
||||
'woo-mercado-pago-custom',
|
||||
'ppcp-gateway',
|
||||
'klarna_payments',
|
||||
],
|
||||
'BR' => [ 'stripe', 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'AR' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'BO' => [],
|
||||
'CL' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'CO' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'EC' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'FK' => [],
|
||||
'GF' => [],
|
||||
'GY' => [],
|
||||
'PY' => [],
|
||||
'PE' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'SR' => [],
|
||||
'UY' => [ 'woo-mercado-pago-custom', 'ppcp-gateway' ],
|
||||
'VE' => [ 'ppcp-gateway' ],
|
||||
'AU' => [
|
||||
),
|
||||
'BR' => array( 'stripe', 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'AR' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'BO' => array(),
|
||||
'CL' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'CO' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'EC' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'FK' => array(),
|
||||
'GF' => array(),
|
||||
'GY' => array(),
|
||||
'PY' => array(),
|
||||
'PE' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'SR' => array(),
|
||||
'UY' => array( 'woo-mercado-pago-custom', 'ppcp-gateway' ),
|
||||
'VE' => array( 'ppcp-gateway' ),
|
||||
'AU' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'airwallex_main',
|
||||
'ppcp-gateway',
|
||||
'square_credit_card',
|
||||
'eway',
|
||||
'afterpay',
|
||||
'klarna_payments',
|
||||
'zipmoney',
|
||||
],
|
||||
'NZ' => [
|
||||
),
|
||||
'NZ' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'airwallex_main',
|
||||
'ppcp-gateway',
|
||||
'eway',
|
||||
'klarna_payments',
|
||||
'zipmoney',
|
||||
],
|
||||
'HK' => [
|
||||
),
|
||||
'HK' => array(
|
||||
'woocommerce_payments',
|
||||
'stripe',
|
||||
'airwallex_main',
|
||||
'ppcp-gateway',
|
||||
'eway',
|
||||
'payoneer-checkout',
|
||||
],
|
||||
'JP' => [
|
||||
),
|
||||
'JP' => array(
|
||||
'stripe',
|
||||
'ppcp-gateway',
|
||||
'square_credit_card',
|
||||
'amazon_payments_advanced',
|
||||
],
|
||||
'SG' => [ 'woocommerce_payments', 'stripe', 'ppcp-gateway', 'eway' ],
|
||||
'CN' => [ 'ppcp-gateway', 'payoneer-checkout' ],
|
||||
'FJ' => [],
|
||||
'GU' => [],
|
||||
'ID' => [ 'stripe', 'ppcp-gateway' ],
|
||||
'IN' => [ 'stripe', 'razorpay', 'payubiz', 'ppcp-gateway' ],
|
||||
'ZA' => [ 'payfast', 'paystack' ],
|
||||
'NG' => [ 'paystack' ],
|
||||
'GH' => [ 'paystack' ],
|
||||
),
|
||||
'SG' => array( 'woocommerce_payments', 'stripe', 'airwallex_main', 'ppcp-gateway' ),
|
||||
'CN' => array( 'airwallex_main', 'ppcp-gateway', 'payoneer-checkout' ),
|
||||
'FJ' => array(),
|
||||
'GU' => array(),
|
||||
'ID' => array( 'stripe', 'ppcp-gateway' ),
|
||||
'IN' => array( 'stripe', 'razorpay', 'payubiz', 'ppcp-gateway' ),
|
||||
'ZA' => array( 'payfast', 'paystack' ),
|
||||
'NG' => array( 'paystack' ),
|
||||
'GH' => array( 'paystack' ),
|
||||
);
|
||||
|
||||
// If the country code is not in the list, return default priority.
|
||||
|
||||
@@ -31,11 +31,13 @@ class BlockRegistry {
|
||||
'woocommerce/product-pricing-field',
|
||||
'woocommerce/product-section',
|
||||
'woocommerce/product-section-description',
|
||||
'woocommerce/product-details-section-description',
|
||||
'woocommerce/product-tab',
|
||||
'woocommerce/product-toggle-field',
|
||||
'woocommerce/product-taxonomy-field',
|
||||
'woocommerce/product-text-field',
|
||||
'woocommerce/product-number-field',
|
||||
'woocommerce/product-linked-list-field',
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplate;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\LayoutTemplates\LayoutTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
|
||||
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\ProductVariationTemplate;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\Block;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\BlockTemplateLogger;
|
||||
|
||||
use WP_Block_Editor_Context;
|
||||
|
||||
/**
|
||||
@@ -24,11 +25,18 @@ class Init {
|
||||
const EDITOR_CONTEXT_NAME = 'woocommerce/edit-product';
|
||||
|
||||
/**
|
||||
* Supported post types.
|
||||
* Supported product types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $supported_post_types = array( 'simple' );
|
||||
private $supported_product_types = array( 'simple' );
|
||||
|
||||
/**
|
||||
* Registered product templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $product_templates = array();
|
||||
|
||||
/**
|
||||
* Redirection controller.
|
||||
@@ -42,18 +50,18 @@ class Init {
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
array_push( $this->supported_post_types, 'variable' );
|
||||
array_push( $this->supported_product_types, 'variable' );
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'product-external-affiliate' ) ) {
|
||||
array_push( $this->supported_post_types, 'external' );
|
||||
array_push( $this->supported_product_types, 'external' );
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'product-grouped' ) ) {
|
||||
array_push( $this->supported_post_types, 'grouped' );
|
||||
array_push( $this->supported_product_types, 'grouped' );
|
||||
}
|
||||
|
||||
$this->redirection_controller = new RedirectionController( $this->supported_post_types );
|
||||
$this->redirection_controller = new RedirectionController();
|
||||
|
||||
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||
if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
@@ -67,14 +75,15 @@ class Init {
|
||||
|
||||
add_action( 'current_screen', array( $this, 'set_current_screen_to_block_editor_if_wc_admin' ) );
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_layout_templates' ) );
|
||||
|
||||
// Make sure the block registry is initialized so that core blocks are registered.
|
||||
BlockRegistry::get_instance();
|
||||
|
||||
$tracks = new Tracks();
|
||||
$tracks->init();
|
||||
|
||||
// Make sure the block template logger is initialized before any templates are created.
|
||||
BlockTemplateLogger::get_instance();
|
||||
$this->register_product_templates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +95,6 @@ class Init {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->register_product_editor_templates();
|
||||
$editor_settings = $this->get_product_editor_settings();
|
||||
|
||||
$script_handle = 'wc-admin-edit-product';
|
||||
@@ -181,6 +189,7 @@ class Init {
|
||||
'variable_product_block_tour_shown',
|
||||
'local_attributes_notice_dismissed_ids',
|
||||
'variable_items_without_price_notice_dismissed',
|
||||
'product_advice_card_dismissed',
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -210,22 +219,11 @@ class Init {
|
||||
* Get the product editor settings.
|
||||
*/
|
||||
private function get_product_editor_settings() {
|
||||
$editor_settings = array();
|
||||
|
||||
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
|
||||
$block_template_logger = BlockTemplateLogger::get_instance();
|
||||
|
||||
$block_template_logger->log_template_events_to_file( 'simple-product' );
|
||||
$block_template_logger->log_template_events_to_file( 'product-variation' );
|
||||
|
||||
$editor_settings['templates'] = array(
|
||||
'product' => $template_registry->get_registered( 'simple-product' )->get_formatted_template(),
|
||||
'product_variation' => $template_registry->get_registered( 'product-variation' )->get_formatted_template(),
|
||||
);
|
||||
|
||||
$editor_settings['templateEvents'] = array(
|
||||
'product' => $block_template_logger->get_formatted_template_events( 'simple-product' ),
|
||||
'product_variation' => $block_template_logger->get_formatted_template_events( 'product-variation' ),
|
||||
$editor_settings['productTemplates'] = array_map(
|
||||
function ( $product_template ) {
|
||||
return $product_template->to_json();
|
||||
},
|
||||
$this->product_templates
|
||||
);
|
||||
|
||||
$block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );
|
||||
@@ -234,12 +232,151 @@ class Init {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register product editor templates.
|
||||
* Get default product templates.
|
||||
*
|
||||
* @return array The default templates.
|
||||
*/
|
||||
private function register_product_editor_templates() {
|
||||
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
|
||||
private function get_default_product_templates() {
|
||||
$templates = array();
|
||||
$templates[] = new ProductTemplate(
|
||||
array(
|
||||
'id' => 'standard-product-template',
|
||||
'title' => __( 'Standard product', 'woocommerce' ),
|
||||
'description' => __( 'A single physical or virtual product, e.g. a t-shirt or an eBook.', 'woocommerce' ),
|
||||
'order' => 10,
|
||||
'icon' => 'shipping',
|
||||
'layout_template_id' => 'simple-product',
|
||||
'product_data' => array(
|
||||
'type' => 'simple',
|
||||
),
|
||||
)
|
||||
);
|
||||
$templates[] = new ProductTemplate(
|
||||
array(
|
||||
'id' => 'grouped-product-template',
|
||||
'title' => __( 'Grouped product', 'woocommerce' ),
|
||||
'description' => __( 'A set of products that go well together, e.g. camera kit.', 'woocommerce' ),
|
||||
'order' => 20,
|
||||
'icon' => 'group',
|
||||
'layout_template_id' => 'simple-product',
|
||||
'product_data' => array(
|
||||
'type' => 'grouped',
|
||||
),
|
||||
)
|
||||
);
|
||||
$templates[] = new ProductTemplate(
|
||||
array(
|
||||
'id' => 'affiliate-product-template',
|
||||
'title' => __( 'Affiliate product', 'woocommerce' ),
|
||||
'description' => __( 'A link to a product sold on a different website, e.g. brand collab.', 'woocommerce' ),
|
||||
'order' => 30,
|
||||
'icon' => 'link',
|
||||
'layout_template_id' => 'simple-product',
|
||||
'product_data' => array(
|
||||
'type' => 'external',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$template_registry->register( new SimpleProductTemplate() );
|
||||
$template_registry->register( new ProductVariationTemplate() );
|
||||
return $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default product template by custom product type if it does not have a
|
||||
* template associated yet.
|
||||
*
|
||||
* @param array $templates The registered product templates.
|
||||
* @return array The new templates.
|
||||
*/
|
||||
private function create_default_product_template_by_custom_product_type( array $templates ) {
|
||||
// Getting the product types registered via the classic editor.
|
||||
$registered_product_types = wc_get_product_types();
|
||||
|
||||
$custom_product_types = array_filter(
|
||||
$registered_product_types,
|
||||
function ( $product_type ) {
|
||||
return ! in_array( $product_type, $this->supported_product_types, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
$templates_with_product_type = array_filter(
|
||||
$templates,
|
||||
function ( $template ) {
|
||||
$product_data = $template->get_product_data();
|
||||
return ! is_null( $product_data ) && array_key_exists( 'type', $product_data );
|
||||
}
|
||||
);
|
||||
|
||||
$custom_product_types_on_templates = array_map(
|
||||
function ( $template ) {
|
||||
$product_data = $template->get_product_data();
|
||||
return $product_data['type'];
|
||||
},
|
||||
$templates_with_product_type
|
||||
);
|
||||
|
||||
foreach ( $custom_product_types as $product_type => $title ) {
|
||||
if ( in_array( $product_type, $custom_product_types_on_templates, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$templates[] = new ProductTemplate(
|
||||
array(
|
||||
'id' => $product_type . '-product-template',
|
||||
'title' => $title,
|
||||
'product_data' => array(
|
||||
'type' => $product_type,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register layout templates.
|
||||
*/
|
||||
public function register_layout_templates() {
|
||||
$layout_template_registry = wc_get_container()->get( LayoutTemplateRegistry::class );
|
||||
|
||||
if ( ! $layout_template_registry->is_registered( 'simple-product' ) ) {
|
||||
$layout_template_registry->register(
|
||||
'simple-product',
|
||||
'product-form',
|
||||
SimpleProductTemplate::class
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! $layout_template_registry->is_registered( 'product-variation' ) ) {
|
||||
$layout_template_registry->register(
|
||||
'product-variation',
|
||||
'product-form',
|
||||
ProductVariationTemplate::class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register product templates.
|
||||
*/
|
||||
public function register_product_templates() {
|
||||
/**
|
||||
* Allows for new product template registration.
|
||||
*
|
||||
* @since 8.5.0
|
||||
*/
|
||||
$this->product_templates = apply_filters( 'woocommerce_product_editor_product_templates', $this->get_default_product_templates() );
|
||||
$this->product_templates = $this->create_default_product_template_by_custom_product_type( $this->product_templates );
|
||||
|
||||
usort(
|
||||
$this->product_templates,
|
||||
function ( $a, $b ) {
|
||||
return $a->get_order() - $b->get_order();
|
||||
}
|
||||
);
|
||||
|
||||
$this->redirection_controller->set_product_templates( $this->product_templates );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Block Editor
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
/**
|
||||
* The Product Template that represents the relation between the Product and
|
||||
* the LayoutTemplate (ProductFormTemplateInterface)
|
||||
*
|
||||
* @see ProductFormTemplateInterface
|
||||
*/
|
||||
class ProductTemplate {
|
||||
/**
|
||||
* The template id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The template title.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* The product data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $product_data;
|
||||
|
||||
/**
|
||||
* The template order.
|
||||
*
|
||||
* @var Integer
|
||||
*/
|
||||
private $order = 999;
|
||||
|
||||
/**
|
||||
* The layout template id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $layout_template_id = null;
|
||||
|
||||
/**
|
||||
* The template description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $description = null;
|
||||
|
||||
/**
|
||||
* The template icon.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $icon = null;
|
||||
|
||||
/**
|
||||
* ProductTemplate constructor
|
||||
*
|
||||
* @param array $data The data.
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->id = $data['id'];
|
||||
$this->title = $data['title'];
|
||||
$this->product_data = $data['product_data'];
|
||||
|
||||
if ( isset( $data['order'] ) ) {
|
||||
$this->order = $data['order'];
|
||||
}
|
||||
|
||||
if ( isset( $data['layout_template_id'] ) ) {
|
||||
$this->layout_template_id = $data['layout_template_id'];
|
||||
}
|
||||
|
||||
if ( isset( $data['description'] ) ) {
|
||||
$this->description = $data['description'];
|
||||
}
|
||||
|
||||
if ( isset( $data['icon'] ) ) {
|
||||
$this->icon = $data['icon'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*
|
||||
* @return string The ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*
|
||||
* @return string The title.
|
||||
*/
|
||||
public function get_title() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the layout template ID.
|
||||
*
|
||||
* @return string The layout template ID.
|
||||
*/
|
||||
public function get_layout_template_id() {
|
||||
return $this->layout_template_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the layout template ID.
|
||||
*
|
||||
* @param string $layout_template_id The layout template ID.
|
||||
*/
|
||||
public function set_layout_template_id( string $layout_template_id ) {
|
||||
$this->layout_template_id = $layout_template_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product data.
|
||||
*
|
||||
* @return array The product data.
|
||||
*/
|
||||
public function get_product_data() {
|
||||
return $this->product_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*
|
||||
* @return string The description.
|
||||
*/
|
||||
public function get_description() {
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the template description.
|
||||
*
|
||||
* @param string $description The template description.
|
||||
*/
|
||||
public function set_description( string $description ) {
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template icon.
|
||||
*
|
||||
* @return string The icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the template icon.
|
||||
*
|
||||
* @see https://github.com/WordPress/gutenberg/tree/trunk/packages/icons.
|
||||
*
|
||||
* @param string $icon The icon name from the @wordpress/components or a url for an external image resource.
|
||||
*/
|
||||
public function set_icon( string $icon ) {
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template order.
|
||||
*
|
||||
* @return int The order.
|
||||
*/
|
||||
public function get_order() {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the template order.
|
||||
*
|
||||
* @param int $order The template order.
|
||||
*/
|
||||
public function set_order( int $order ) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product template as JSON like.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
public function to_json() {
|
||||
return array(
|
||||
'id' => $this->get_id(),
|
||||
'title' => $this->get_title(),
|
||||
'description' => $this->get_description(),
|
||||
'icon' => $this->get_icon(),
|
||||
'order' => $this->get_order(),
|
||||
'layoutTemplateId' => $this->get_layout_template_id(),
|
||||
'productData' => $this->get_product_data(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,22 +12,17 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
* Handle redirecting to the old or new editor based on features and support.
|
||||
*/
|
||||
class RedirectionController {
|
||||
|
||||
/**
|
||||
* Supported post types.
|
||||
* Registered product templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $supported_post_types;
|
||||
private $product_templates = array();
|
||||
|
||||
/**
|
||||
* Set up the hooks used for redirection.
|
||||
*
|
||||
* @param array $supported_post_types Array of supported post types.
|
||||
*/
|
||||
public function __construct( $supported_post_types ) {
|
||||
$this->supported_post_types = $supported_post_types;
|
||||
|
||||
public function __construct() {
|
||||
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||
add_action( 'current_screen', array( $this, 'maybe_redirect_to_new_editor' ), 30, 0 );
|
||||
add_action( 'current_screen', array( $this, 'redirect_non_supported_product_types' ), 30, 0 );
|
||||
@@ -63,18 +58,53 @@ class RedirectionController {
|
||||
*/
|
||||
protected function is_product_supported( $product_id ): bool {
|
||||
$product = $product_id ? wc_get_product( $product_id ) : null;
|
||||
$digital_product = $product->is_downloadable() || $product->is_virtual();
|
||||
|
||||
if ( $product && in_array( $product->get_type(), $this->supported_post_types, true ) ) {
|
||||
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
|
||||
if ( is_null( $product ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$digital_product = $product->is_downloadable() || $product->is_virtual();
|
||||
$product_template_id = $product->get_meta( '_product_template_id' );
|
||||
|
||||
foreach ( $this->product_templates as $product_template ) {
|
||||
if ( is_null( $product_template->get_layout_template_id() ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product_data = $product_template->get_product_data();
|
||||
$product_data_type = $product_data['type'];
|
||||
// Treat a variable product as a simple product since there is not a product template
|
||||
// for variable products.
|
||||
$product_type = $product->get_type() === 'variable' ? 'simple' : $product->get_type();
|
||||
|
||||
if ( isset( $product_data_type ) && $product_data_type !== $product_type ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $product_template_id ) && $product_template_id === $product_template->get_id() ) {
|
||||
return true;
|
||||
}
|
||||
return ! $digital_product;
|
||||
|
||||
if ( isset( $product_data_type ) ) {
|
||||
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
|
||||
return true;
|
||||
}
|
||||
return ! $digital_product;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a product is supported by the new experience.
|
||||
*
|
||||
* @param array $product_templates The registered product teamplates.
|
||||
*/
|
||||
public function set_product_templates( array $product_templates ): void {
|
||||
$this->product_templates = $product_templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects from old product form to the new product form if the
|
||||
* feature `product_block_editor` is enabled.
|
||||
|
||||
@@ -48,6 +48,13 @@ class MarketingCampaign {
|
||||
*/
|
||||
protected $cost;
|
||||
|
||||
/**
|
||||
* The sales of the marketing campaign with the currency.
|
||||
*
|
||||
* @var Price
|
||||
*/
|
||||
protected $sales;
|
||||
|
||||
/**
|
||||
* MarketingCampaign constructor.
|
||||
*
|
||||
@@ -56,13 +63,15 @@ class MarketingCampaign {
|
||||
* @param string $title The title of the marketing campaign.
|
||||
* @param string $manage_url The URL to the channel's campaign management page.
|
||||
* @param Price|null $cost The cost of the marketing campaign with the currency.
|
||||
* @param Price|null $sales The sales of the marketing campaign with the currency.
|
||||
*/
|
||||
public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null ) {
|
||||
public function __construct( string $id, MarketingCampaignType $type, string $title, string $manage_url, Price $cost = null, Price $sales = null ) {
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
$this->title = $title;
|
||||
$this->manage_url = $manage_url;
|
||||
$this->cost = $cost;
|
||||
$this->sales = $sales;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,4 +118,13 @@ class MarketingCampaign {
|
||||
public function get_cost(): ?Price {
|
||||
return $this->cost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sales of the marketing campaign with the currency.
|
||||
*
|
||||
* @return Price|null
|
||||
*/
|
||||
public function get_sales(): ?Price {
|
||||
return $this->sales;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
* @param string $option_name option name.
|
||||
*/
|
||||
public function __construct( string $option_name ) {
|
||||
$this->option_name = $option_name;
|
||||
$this->option_name = $option_name;
|
||||
add_option(
|
||||
$this->option_name,
|
||||
array(
|
||||
@@ -129,6 +129,15 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
$option['plugins'][ $plugin_name ]['status'] = 'failed';
|
||||
$option['status'] = 'failed';
|
||||
|
||||
wc_admin_record_tracks_event(
|
||||
'coreprofiler_store_extension_installed_and_activated',
|
||||
array(
|
||||
'success' => false,
|
||||
'extension' => $plugin_name,
|
||||
'error_message' => $error_message,
|
||||
)
|
||||
);
|
||||
|
||||
$this->update( $option );
|
||||
}
|
||||
|
||||
@@ -223,7 +232,18 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
continue;
|
||||
}
|
||||
|
||||
$track_data[ 'install_time_' . $this->get_plugin_track_key( $plugin ) ] = $this->get_timeframe( $data['time'][ $plugin ] );
|
||||
$plugin_track_key = $this->get_plugin_track_key( $plugin );
|
||||
$install_time = $this->get_timeframe( $data['time'][ $plugin ] );
|
||||
$track_data[ 'install_time_' . $plugin_track_key ] = $install_time;
|
||||
|
||||
wc_admin_record_tracks_event(
|
||||
'coreprofiler_store_extension_installed_and_activated',
|
||||
array(
|
||||
'success' => true,
|
||||
'extension' => $plugin_track_key,
|
||||
'install_time' => $install_time,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
wc_admin_record_tracks_event( 'coreprofiler_store_extensions_installed_and_activated', $track_data );
|
||||
|
||||
@@ -68,4 +68,4 @@ class ComparisonOperation {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* ContentProcessor class.
|
||||
*
|
||||
* Process images for content
|
||||
*/
|
||||
class ContentProcessor {
|
||||
|
||||
/**
|
||||
* Summarize the business description to ensure better results are returned by AI.
|
||||
*
|
||||
* @param string $business_description The business description.
|
||||
* @param Connection $ai_connection The AI connection.
|
||||
* @param string $token The JWT token.
|
||||
* @param integer $character_limit The character limit for the business description.
|
||||
*
|
||||
* @return mixed|WP_Error
|
||||
*/
|
||||
public static function summarize_business_description( $business_description, $ai_connection, $token, $character_limit = 150 ) {
|
||||
if ( empty( $business_description ) ) {
|
||||
return new WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( strlen( $business_description ) > $character_limit ) {
|
||||
$prompt = sprintf( 'You are a professional writer. Read the following business description and write a text with less than %s characters to summarize the products the business is selling: "%s". Make sure you do not add double quotes in your response. Do not add any explanations in the response', $character_limit, $business_description );
|
||||
|
||||
$response = $ai_connection->fetch_ai_response( $token, $prompt, 30 );
|
||||
|
||||
$business_description = $response['completion'] ?? $business_description;
|
||||
}
|
||||
|
||||
return $business_description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that images are provided for assignment to products and patterns.
|
||||
*
|
||||
* @param array|WP_Error $images The array of images.
|
||||
* @param Connection $ai_connection The AI connection.
|
||||
* @param string $token The JWT token.
|
||||
* @param string $business_description The business description.
|
||||
*
|
||||
* @return array|int|mixed|string|WP_Error
|
||||
*/
|
||||
public static function verify_images( $images, $ai_connection, $token, $business_description ) {
|
||||
if ( ! is_wp_error( $images ) && ! empty( $images['images'] ) && ! empty( $images['search_term'] ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
$images = ( new Pexels() )->get_images( $ai_connection, $token, $business_description );
|
||||
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( empty( $images['images'] ) || empty( $images['search_term'] ) ) {
|
||||
return new WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the size of images for optimal performance on products and patterns.
|
||||
*
|
||||
* @param string $image_url The image URL.
|
||||
* @param string $usage_type The usage type of the image. Either 'products' or 'patterns'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function adjust_image_size( $image_url, $usage_type ) {
|
||||
$parsed_url = wp_parse_url( $image_url );
|
||||
|
||||
if ( ! isset( $parsed_url['query'] ) ) {
|
||||
return $image_url;
|
||||
}
|
||||
|
||||
$width = 'products' === $usage_type ? 400 : 500;
|
||||
|
||||
parse_str( $parsed_url['query'], $query_params );
|
||||
|
||||
unset( $query_params['h'], $query_params['w'] );
|
||||
$query_params['w'] = $width;
|
||||
$url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'];
|
||||
|
||||
return add_query_arg( $query_params, $url );
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Pattern Images Helper class.
|
||||
* Patterns Helper class.
|
||||
*/
|
||||
class PatternsHelper {
|
||||
/**
|
||||
* Returns the image for the given pattern.
|
||||
* Fetches the AI-selected image for the pattern or returns the default image.
|
||||
*
|
||||
* @param array $images The array of images.
|
||||
* @param int $index The index of the image to return.
|
||||
@@ -17,7 +17,7 @@ class PatternsHelper {
|
||||
*
|
||||
* @return string The image.
|
||||
*/
|
||||
public static function get_image_url( array $images, int $index, string $default_image ): string {
|
||||
public static function get_image_url( $images, $index, $default_image ) {
|
||||
$image = filter_var( $default_image, FILTER_VALIDATE_URL )
|
||||
? $default_image
|
||||
: plugins_url( $default_image, dirname( __DIR__, 2 ) );
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use WP_Error;
|
||||
@@ -8,7 +8,7 @@ use WP_Error;
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*/
|
||||
class PatternUpdater {
|
||||
class UpdatePatterns {
|
||||
|
||||
/**
|
||||
* All patterns that are actively in use in the Assembler.
|
||||
@@ -36,40 +36,12 @@ class PatternUpdater {
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function generate_content( $ai_connection, $token, $images, $business_description ) {
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
if ( ! isset( $images['images'] ) ) {
|
||||
return new \WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( 0 === count( $images['images'] ) ) {
|
||||
$images = get_transient( 'woocommerce_ai_managed_images' );
|
||||
}
|
||||
|
||||
if ( empty( $images['images'] ) ) {
|
||||
return new WP_Error( 'no_images_found', __( 'No images found.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
// This is required in case something interrupts the execution of the script and the endpoint is called again on retry.
|
||||
set_transient( 'woocommerce_ai_managed_images', $images, 60 );
|
||||
|
||||
$patterns_dictionary = self::get_patterns_dictionary();
|
||||
$images = ContentProcessor::verify_images( $images, $ai_connection, $token, $business_description );
|
||||
$patterns_dictionary = PatternsHelper::get_patterns_dictionary();
|
||||
|
||||
if ( is_wp_error( $patterns_dictionary ) ) {
|
||||
return $patterns_dictionary;
|
||||
@@ -336,7 +308,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['titles'][ $j ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['titles'][ $j ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -348,7 +320,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['descriptions'][ $k ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['descriptions'][ $k ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -360,7 +332,7 @@ class PatternUpdater {
|
||||
$ai_response_content[ $counter ] = $ai_response_content[ $counter - 1 ] ?? '';
|
||||
}
|
||||
|
||||
$patterns[ $i ]['content']['buttons'][ $l ]['default'] = $ai_response_content[ $counter ];
|
||||
$patterns[ $i ]['content']['buttons'][ $l ]['default'] = $this->sanitize_string( $ai_response_content[ $counter ] );
|
||||
|
||||
$counter ++;
|
||||
}
|
||||
@@ -372,6 +344,18 @@ class PatternUpdater {
|
||||
return $patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the string from the AI generated content. It removes double quotes that can cause issues when
|
||||
* decoding the patterns JSON.
|
||||
*
|
||||
* @param string $string The string to be sanitized.
|
||||
*
|
||||
* @return string The sanitized string.
|
||||
*/
|
||||
private function sanitize_string( $string ) {
|
||||
return str_replace( '"', '', $string );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign selected images to patterns.
|
||||
*
|
||||
@@ -469,7 +453,9 @@ class PatternUpdater {
|
||||
continue;
|
||||
}
|
||||
|
||||
$images[] = $selected_image['URL'];
|
||||
$selected_image_url = ContentProcessor::adjust_image_size( $selected_image['URL'], 'patterns' );
|
||||
|
||||
$images[] = $selected_image_url;
|
||||
$alts[] = $selected_image['title'];
|
||||
}
|
||||
|
||||
@@ -477,7 +463,7 @@ class PatternUpdater {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected image format. Defaults to landscape.
|
||||
* Returns the selected image format. Defaults to portrait.
|
||||
*
|
||||
* @param array $selected_image The selected image to be assigned to the pattern.
|
||||
*
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Patterns;
|
||||
namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use WP_Error;
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*/
|
||||
class ProductUpdater {
|
||||
class UpdateProducts {
|
||||
|
||||
/**
|
||||
* The dummy products.
|
||||
@@ -62,41 +62,20 @@ class ProductUpdater {
|
||||
* @return array|WP_Error The generated content for the products. An error if the content could not be generated.
|
||||
*/
|
||||
public function generate_content( $ai_connection, $token, $images, $business_description ) {
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
if ( ! isset( $images['images'] ) || ! isset( $images['search_term'] ) ) {
|
||||
$images = get_transient( 'woocommerce_ai_managed_images' );
|
||||
}
|
||||
$images = ContentProcessor::verify_images( $images, $ai_connection, $token, $business_description );
|
||||
|
||||
if ( ! isset( $images['images'] ) || ! isset( $images['search_term'] ) ) {
|
||||
return new \WP_Error( 'images_not_found', __( 'No images provided for generating AI content.', 'woocommerce' ) );
|
||||
if ( is_wp_error( $images ) ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
// This is required in case something interrupts the execution of the script and the endpoint is called again on retry.
|
||||
set_transient( 'woocommerce_ai_managed_images', $images, 60 );
|
||||
|
||||
if ( empty( $business_description ) ) {
|
||||
return new \WP_Error( 'missing_business_description', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$last_business_description = get_option( 'last_business_description_with_ai_content_generated' );
|
||||
|
||||
if ( $last_business_description === $business_description ) {
|
||||
if ( is_string( $business_description ) && is_string( $last_business_description ) ) {
|
||||
return array(
|
||||
'product_content' => array(),
|
||||
);
|
||||
} else {
|
||||
return new \WP_Error( 'business_description_not_found', __( 'No business description provided for generating AI content.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$dummy_products_to_update = $this->fetch_dummy_products_to_update();
|
||||
|
||||
if ( is_wp_error( $dummy_products_to_update ) ) {
|
||||
@@ -296,7 +275,7 @@ class ProductUpdater {
|
||||
*
|
||||
* @param array $ai_generated_product_content The AI-generated product content.
|
||||
*
|
||||
* @return string|void
|
||||
* @return void|WP_Error
|
||||
*/
|
||||
public function update_product_content( $ai_generated_product_content ) {
|
||||
if ( ! isset( $ai_generated_product_content['product_id'] ) ) {
|
||||
@@ -316,7 +295,7 @@ class ProductUpdater {
|
||||
$product_image_id = $this->product_image_upload( $product->get_id(), $ai_generated_product_content['image']['src'], $ai_generated_product_content['image']['alt'] );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return $product_image_id->get_error_message();
|
||||
return new \WP_Error( 'error_uploading_image', $product_image_id->get_error_message() );
|
||||
}
|
||||
|
||||
$this->product_update( $product, $product_image_id, $ai_generated_product_content['title'], $ai_generated_product_content['description'], $ai_generated_product_content['price'] );
|
||||
@@ -342,37 +321,7 @@ class ProductUpdater {
|
||||
set_time_limit( 150 );
|
||||
wp_raise_memory_limit( 'image' );
|
||||
|
||||
$product_image_id = media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return $product_image_id->get_error_message();
|
||||
}
|
||||
|
||||
return $product_image_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the size of the image for the product to improve performance and
|
||||
* avoid memory exhaustion errors when uploading them to the media library.
|
||||
*
|
||||
* @param string $image_url The image URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function adjust_image_size_for_products( $image_url ) {
|
||||
$parsed_url = wp_parse_url( $image_url );
|
||||
|
||||
if ( ! isset( $parsed_url['query'] ) ) {
|
||||
return $image_url;
|
||||
}
|
||||
|
||||
parse_str( $parsed_url['query'], $query_params );
|
||||
|
||||
unset( $query_params['h'], $query_params['w'] );
|
||||
$query_params['w'] = 300;
|
||||
$new_query_string = http_build_query( $query_params );
|
||||
|
||||
return $parsed_url['scheme'] . '://' . $parsed_url['host'] . $parsed_url['path'] . '?' . $new_query_string;
|
||||
return media_sideload_image( $image_src, $product_id, $image_alt, 'id' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,7 +339,7 @@ class ProductUpdater {
|
||||
$image_src = $ai_selected_images[ $i ]['URL'] ?? '';
|
||||
|
||||
if ( wc_is_valid_url( $image_src ) ) {
|
||||
$image_src = $this->adjust_image_size_for_products( $ai_selected_images[ $i ]['URL'] );
|
||||
$image_src = ContentProcessor::adjust_image_size( $image_src, 'products' );
|
||||
}
|
||||
|
||||
$image_alt = $ai_selected_images[ $i ]['title'] ?? '';
|
||||
@@ -400,8 +349,8 @@ class ProductUpdater {
|
||||
'description' => 'A product description',
|
||||
'price' => 'The product price',
|
||||
'image' => [
|
||||
'src' => esc_url( $image_src ),
|
||||
'alt' => esc_attr( $image_alt ),
|
||||
'src' => $image_src,
|
||||
'alt' => $image_alt,
|
||||
],
|
||||
'product_id' => $dummy_products_to_update[ $i ]->get_id(),
|
||||
];
|
||||
@@ -422,8 +371,10 @@ class ProductUpdater {
|
||||
* @return array|int|string|\WP_Error
|
||||
*/
|
||||
public function assign_ai_generated_content_to_dummy_products( $ai_connection, $token, $products_information_list, $business_description, $search_term ) {
|
||||
if ( empty( $business_description ) ) {
|
||||
return new \WP_Error( 'missing_store_description', __( 'The store description is required to generate content for your site.', 'woocommerce' ) );
|
||||
$business_description = ContentProcessor::summarize_business_description( $business_description, $ai_connection, $token, 100 );
|
||||
|
||||
if ( is_wp_error( $business_description ) ) {
|
||||
return $business_description;
|
||||
}
|
||||
|
||||
$prompts = [];
|
||||
@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\Assets;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Exception;
|
||||
use Automattic\Jetpack\Constants;
|
||||
/**
|
||||
* The Api class provides an interface to various asset registration helpers.
|
||||
*
|
||||
@@ -11,6 +12,14 @@ use Exception;
|
||||
* @since 2.5.0
|
||||
*/
|
||||
class Api {
|
||||
|
||||
/**
|
||||
* Stores the prefixed WC version. Used because the WC Blocks version has not been updated since the monorepo merge.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $wc_version;
|
||||
|
||||
/**
|
||||
* Stores inline scripts already enqueued.
|
||||
*
|
||||
@@ -59,6 +68,8 @@ class Api {
|
||||
* @param Package $package An instance of Package.
|
||||
*/
|
||||
public function __construct( Package $package ) {
|
||||
// Use wc- prefix here to prevent collisions when WC Core version catches up to a version previously used by the WC Blocks feature plugin.
|
||||
$this->wc_version = 'wc-' . Constants::get_constant( 'WC_VERSION' );
|
||||
$this->package = $package;
|
||||
$this->disable_cache = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ! $this->package->feature()->is_production_environment();
|
||||
|
||||
@@ -84,7 +95,7 @@ class Api {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $this->package->get_path() . $file ) ) {
|
||||
return filemtime( $this->package->get_path( trim( $file, '/' ) ) );
|
||||
}
|
||||
return $this->package->get_version();
|
||||
return $this->wc_version;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +134,7 @@ class Api {
|
||||
* @return string The generated hash.
|
||||
*/
|
||||
private function get_script_data_hash() {
|
||||
return md5( get_option( 'siteurl', '' ) . $this->package->get_version() . $this->package->get_path() );
|
||||
return md5( get_option( 'siteurl', '' ) . $this->wc_version . $this->package->get_path() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +154,7 @@ class Api {
|
||||
empty( $transient_value ) ||
|
||||
empty( $transient_value['script_data'] ) ||
|
||||
empty( $transient_value['version'] ) ||
|
||||
$transient_value['version'] !== $this->package->get_version() ||
|
||||
$transient_value['version'] !== $this->wc_version ||
|
||||
empty( $transient_value['hash'] ) ||
|
||||
$transient_value['hash'] !== $this->script_data_hash
|
||||
) {
|
||||
@@ -165,7 +176,7 @@ class Api {
|
||||
wp_json_encode(
|
||||
array(
|
||||
'script_data' => $this->script_data,
|
||||
'version' => $this->package->get_version(),
|
||||
'version' => $this->wc_version,
|
||||
'hash' => $this->script_data_hash,
|
||||
)
|
||||
),
|
||||
|
||||
@@ -67,8 +67,8 @@ final class AssetsController {
|
||||
// Register the interactivity components here for now.
|
||||
$this->api->register_script( 'wc-interactivity-dropdown', 'assets/client/blocks/wc-interactivity-dropdown.js', array() );
|
||||
$this->api->register_script( 'wc-interactivity-checkbox-list', 'assets/client/blocks/wc-interactivity-checkbox-list.js', array() );
|
||||
$this->register_style( 'wc-interactivity-checkbox-list', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-checkbox-list', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-dropdown', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-dropdown', 'css' ), __DIR__ ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-checkbox-list', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-checkbox-list', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
$this->register_style( 'wc-interactivity-dropdown', plugins_url( $this->api->get_block_asset_build_path( 'wc-interactivity-dropdown', 'css' ), dirname( __DIR__ ) ), array(), 'all', true );
|
||||
|
||||
wp_add_inline_script(
|
||||
'wc-blocks-middleware',
|
||||
@@ -279,7 +279,7 @@ final class AssetsController {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( \Automattic\WooCommerce\Blocks\Package::get_path() . $file ) ) {
|
||||
return filemtime( \Automattic\WooCommerce\Blocks\Package::get_path() . $file );
|
||||
}
|
||||
return \Automattic\WooCommerce\Blocks\Package::get_version();
|
||||
return $this->api->wc_version;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace Automattic\WooCommerce\Blocks;
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Package;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\ProductUpdater;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\PatternsHelper;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdateProducts;
|
||||
|
||||
/**
|
||||
* Registers patterns under the `./patterns/` directory and updates their content.
|
||||
@@ -369,13 +369,13 @@ class BlockPatterns {
|
||||
return $images->get_error_message();
|
||||
}
|
||||
|
||||
$populate_patterns = ( new PatternUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
$populate_patterns = ( new UpdatePatterns() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_patterns ) ) {
|
||||
return $populate_patterns->get_error_message();
|
||||
}
|
||||
|
||||
$populate_products = ( new ProductUpdater() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
$populate_products = ( new UpdateProducts() )->generate_content( $ai_connection, $token, $images, $business_description );
|
||||
|
||||
if ( is_wp_error( $populate_products ) ) {
|
||||
return $populate_products->get_error_message();
|
||||
|
||||
@@ -70,6 +70,15 @@ abstract class AbstractBlock {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the interactivity namespace. Only used when utilizing the interactivity API.
|
||||
|
||||
* @return string The interactivity namespace, used to namespace interactivity API actions and state.
|
||||
*/
|
||||
protected function get_full_block_name() {
|
||||
return $this->namespace . '/' . $this->block_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default render_callback for all blocks. This will ensure assets are enqueued just in time, then render
|
||||
* the block (if applicable).
|
||||
|
||||
@@ -614,6 +614,10 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( empty( $this->attributes['contentVisibility']['image'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( ! $product->is_on_sale() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,6 +244,7 @@ class Cart extends AbstractBlock {
|
||||
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
|
||||
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
|
||||
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
|
||||
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones(), true );
|
||||
|
||||
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
|
||||
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ), true );
|
||||
|
||||
@@ -169,7 +169,8 @@ class Checkout extends AbstractBlock {
|
||||
<div data-block-name="woocommerce/checkout-shipping-address-block" class="wp-block-woocommerce-checkout-shipping-address-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-billing-address-block" class="wp-block-woocommerce-checkout-billing-address-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-shipping-methods-block" class="wp-block-woocommerce-checkout-shipping-methods-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"></div>' .
|
||||
<div data-block-name="woocommerce/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"></div>
|
||||
<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' .
|
||||
( isset( $attributes['showOrderNotes'] ) && false === $attributes['showOrderNotes'] ? '' : '<div data-block-name="woocommerce/checkout-order-note-block" class="wp-block-woocommerce-checkout-order-note-block"></div>' ) .
|
||||
( isset( $attributes['showPolicyLinks'] ) && false === $attributes['showPolicyLinks'] ? '' : '<div data-block-name="woocommerce/checkout-terms-block" class="wp-block-woocommerce-checkout-terms-block"></div>' ) .
|
||||
'<div data-block-name="woocommerce/checkout-actions-block" class="wp-block-woocommerce-checkout-actions-block"></div>
|
||||
@@ -217,6 +218,18 @@ class Checkout extends AbstractBlock {
|
||||
$content = preg_replace( $shipping_address_block_regex, $local_pickup_inner_blocks, $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Additional Information block to checkouts missing it.
|
||||
*/
|
||||
$additional_information_inner_blocks = '$0' . PHP_EOL . PHP_EOL . '<div data-block-name="woocommerce/checkout-additional-information-block" class="wp-block-woocommerce-checkout-additional-information-block"></div>' . PHP_EOL . PHP_EOL;
|
||||
$has_additional_information_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-additional-information-block"[^>]*?>/mi';
|
||||
$has_additional_information_block = preg_match( $has_additional_information_regex, $content );
|
||||
|
||||
if ( ! $has_additional_information_block ) {
|
||||
$payment_block_regex = '/<div[^<]*?data-block-name="woocommerce\/checkout-payment-block" class="wp-block-woocommerce-checkout-payment-block"[^>]*?><\/div>/mi';
|
||||
$content = preg_replace( $payment_block_regex, $additional_information_inner_blocks, $content );
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -302,25 +315,7 @@ class Checkout extends AbstractBlock {
|
||||
}
|
||||
|
||||
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) {
|
||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||
$formatted_shipping_zones = array_reduce(
|
||||
$shipping_zones,
|
||||
function( $acc, $zone ) {
|
||||
$acc[] = [
|
||||
'id' => $zone['id'],
|
||||
'title' => $zone['zone_name'],
|
||||
'description' => $zone['formatted_zone_location'],
|
||||
];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$formatted_shipping_zones[] = [
|
||||
'id' => 0,
|
||||
'title' => __( 'International', 'woocommerce' ),
|
||||
'description' => __( 'Locations outside all other zones', 'woocommerce' ),
|
||||
];
|
||||
$this->asset_data_registry->add( 'activeShippingZones', $formatted_shipping_zones );
|
||||
$this->asset_data_registry->add( 'activeShippingZones', CartCheckoutUtils::get_shipping_zones() );
|
||||
}
|
||||
|
||||
if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) {
|
||||
@@ -480,6 +475,7 @@ class Checkout extends AbstractBlock {
|
||||
return [
|
||||
'Checkout',
|
||||
'CheckoutActionsBlock',
|
||||
'CheckoutAdditionalInformationBlock',
|
||||
'CheckoutBillingAddressBlock',
|
||||
'CheckoutContactInformationBlock',
|
||||
'CheckoutExpressPaymentBlock',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CheckoutAdditionalInformationBlock class.
|
||||
*/
|
||||
class CheckoutAdditionalInformationBlock extends AbstractInnerBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'checkout-additional-information-block';
|
||||
}
|
||||
@@ -28,15 +28,12 @@ class ClassicTemplate extends AbstractDynamicBlock {
|
||||
*/
|
||||
protected $api_version = '2';
|
||||
|
||||
const FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM = 'filter_stock_status';
|
||||
|
||||
/**
|
||||
* Initialize this block.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_filter( 'render_block', array( $this, 'add_alignment_class_to_wrapper' ), 10, 2 );
|
||||
add_filter( 'woocommerce_product_query_meta_query', array( $this, 'filter_products_by_stock' ) );
|
||||
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
|
||||
}
|
||||
|
||||
@@ -377,47 +374,6 @@ class ClassicTemplate extends AbstractDynamicBlock {
|
||||
return preg_replace( $pattern_get_class, '$0 ' . $align_class_and_style['class'], $content, 1 );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter products by stock status when as query param there is "filter_stock_status"
|
||||
*
|
||||
* @param array $meta_query Meta query.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_products_by_stock( $meta_query ) {
|
||||
global $wp_query;
|
||||
|
||||
if (
|
||||
is_admin() ||
|
||||
! $wp_query->is_main_query() ||
|
||||
! isset( $_GET[ self::FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM ] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
) {
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
$stock_status = array_keys( wc_get_product_stock_status_options() );
|
||||
$values = sanitize_text_field( wp_unslash( $_GET[ self::FILTER_PRODUCTS_BY_STOCK_QUERY_PARAM ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$values_to_array = explode( ',', $values );
|
||||
|
||||
$filtered_values = array_filter(
|
||||
$values_to_array,
|
||||
function( $value ) use ( $stock_status ) {
|
||||
return in_array( $value, $stock_status, true );
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! empty( $filtered_values ) ) {
|
||||
|
||||
$meta_query[] = array(
|
||||
'key' => '_stock_status',
|
||||
'value' => $filtered_values,
|
||||
'compare' => 'IN',
|
||||
);
|
||||
}
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
|
||||
|
||||
/**
|
||||
* CollectionFilters class.
|
||||
*/
|
||||
final class CollectionFilters extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-filters';
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $current_response = null;
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @see $this->register_block_type()
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
* - Hook into WP lifecycle.
|
||||
* - Register the block with WordPress.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
if ( ! is_admin() ) {
|
||||
/**
|
||||
* At this point, WP starts rendering the Collection Filters block,
|
||||
* we can safely unset the current response.
|
||||
*/
|
||||
$this->current_response = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-filters' ) ),
|
||||
'class' => 'wc-block-collection-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes_data['data-wc-navigation-id'] = sprintf(
|
||||
'wc-collection-filters-%s',
|
||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the context of inner blocks.
|
||||
*
|
||||
* @param array $context The block context.
|
||||
* @param array $parsed_block The parsed block.
|
||||
* @param WP_Block $parent_block The parent block.
|
||||
* @return array
|
||||
*/
|
||||
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
|
||||
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first direct child of Collection Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
if ( empty( $this->current_response ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated collection data from the API.
|
||||
* Loop through inner blocks and build a query string to pass to the API.
|
||||
*
|
||||
* @param WP_Block $block The block instance.
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data(
|
||||
add_query_arg(
|
||||
array_merge(
|
||||
$this->get_formatted_products_params( $block->context['query'] ?? array() ),
|
||||
$collection_data_params,
|
||||
),
|
||||
'/wc/store/v1/products/collection-data'
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $response['body'] ) ) {
|
||||
return json_decode( wp_json_encode( $response['body'] ), true );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inner blocks recursively.
|
||||
*
|
||||
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
|
||||
* @param array $results The results array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_collection_data_params( $inner_blocks, &$results = array() ) {
|
||||
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
|
||||
foreach ( $inner_blocks as $inner_block ) {
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$query_param = $inner_block->attributes['queryParam'];
|
||||
/**
|
||||
* There can be multiple attribute filters so we transform
|
||||
* the query param of each filter into an array to merge
|
||||
* them together.
|
||||
*/
|
||||
if ( ! empty( $query_param['calculate_attribute_counts'] ) ) {
|
||||
$query_param['calculate_attribute_counts'] = array( $query_param['calculate_attribute_counts'] );
|
||||
}
|
||||
$results = array_merge_recursive( $results, $query_param );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted products params for ProductCollectionData route from the
|
||||
* query context.
|
||||
*
|
||||
* @param array $query The query context.
|
||||
* @return array
|
||||
*/
|
||||
private function get_formatted_products_params( $query ) {
|
||||
$params = array();
|
||||
|
||||
if ( empty( $query['isProductCollectionBlock'] ) ) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following params can be passed directly to Store API endpoints.
|
||||
*/
|
||||
$shared_params = array( 'exclude', 'offset', 'search' );
|
||||
|
||||
/**
|
||||
* The following params just need to transform the key, their value can
|
||||
* be passed as it is to the Store API.
|
||||
*/
|
||||
$mapped_params = array(
|
||||
'woocommerceStockStatus' => 'stock_status',
|
||||
'woocommerceOnSale' => 'on_sale',
|
||||
'woocommerceHandPickedProducts' => 'include',
|
||||
);
|
||||
|
||||
$taxonomy_mapper = function( $key ) {
|
||||
$mapping = array(
|
||||
'product_tag' => 'tag',
|
||||
'product_cat' => 'category',
|
||||
);
|
||||
|
||||
return $mapping[ $key ] ?? '_unstable_tax_' . $key;
|
||||
};
|
||||
|
||||
array_walk(
|
||||
$query,
|
||||
function( $value, $key ) use ( $shared_params, $mapped_params, $taxonomy_mapper, &$params ) {
|
||||
if ( in_array( $key, $shared_params, true ) ) {
|
||||
$params[ $key ] = $value;
|
||||
}
|
||||
|
||||
if ( in_array( $key, array_keys( $mapped_params ), true ) ) {
|
||||
$params[ $mapped_params[ $key ] ] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of taxQuery and woocommerceAttributes need additional
|
||||
* transformation to the shape that Store API accepts.
|
||||
*/
|
||||
if ( 'taxQuery' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $terms, $taxonomy ) use ( $taxonomy_mapper, &$params ) {
|
||||
$params[ $taxonomy_mapper( $taxonomy ) ] = implode( ',', $terms );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'woocommerceAttributes' === $key && is_array( $value ) ) {
|
||||
array_walk(
|
||||
$value,
|
||||
function( $attribute ) use ( &$params ) {
|
||||
$params['attributes'][] = array(
|
||||
'attribute' => $attribute['taxonomy'],
|
||||
'term_id' => $attribute['termId'],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Product Collection determines the product visibility based on stock
|
||||
* statuses. We need to pass the catalog_visibility param to the Store
|
||||
* API to make sure the product visibility is correct.
|
||||
*/
|
||||
$params['catalog_visibility'] = is_search() ? 'search' : 'visible';
|
||||
|
||||
/**
|
||||
* `false` values got removed from `add_query_arg`, so we need to convert
|
||||
* them to numeric.
|
||||
*/
|
||||
return array_map(
|
||||
function( $param ) {
|
||||
return is_bool( $param ) ? +$param : $param;
|
||||
},
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* FilledCartBlock class.
|
||||
* FilterWrapper class.
|
||||
*/
|
||||
class FilterWrapper extends AbstractBlock {
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\Utils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
|
||||
|
||||
/**
|
||||
* Mini-Cart class.
|
||||
@@ -17,6 +18,8 @@ use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
|
||||
* @internal
|
||||
*/
|
||||
class MiniCart extends AbstractBlock {
|
||||
use BlockHooksTrait;
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
@@ -52,6 +55,19 @@ class MiniCart extends AbstractBlock {
|
||||
*/
|
||||
protected $display_cart_prices_including_tax = false;
|
||||
|
||||
/**
|
||||
* Block Hook API placements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hooked_block_placements = array(
|
||||
array(
|
||||
'position' => 'after',
|
||||
'anchor' => 'core/navigation',
|
||||
'area' => 'header',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@@ -72,8 +88,8 @@ class MiniCart extends AbstractBlock {
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'wp_loaded', array( $this, 'register_empty_cart_message_block_pattern' ) );
|
||||
add_action( 'hooked_block_types', array( $this, 'register_auto_insert' ), 10, 4 );
|
||||
add_action( 'wp_print_footer_scripts', array( $this, 'print_lazy_load_scripts' ), 2 );
|
||||
add_filter( 'hooked_block_types', array( $this, 'register_hooked_block' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -578,111 +594,6 @@ class MiniCart extends AbstractBlock {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for `hooked_block_types` to auto-inject the mini-cart block into headers after navigation.
|
||||
*
|
||||
* @param array $hooked_blocks An array of block slugs hooked into a given context.
|
||||
* @param string $position Position of the block insertion point.
|
||||
* @param string $anchor_block The block acting as the anchor for the inserted block.
|
||||
* @param \WP_Block_Template|array $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return array An array of block slugs hooked into a given context.
|
||||
*/
|
||||
public function register_auto_insert( $hooked_blocks, $position, $anchor_block, $context ) {
|
||||
// Cache for active theme.
|
||||
static $active_theme_name = null;
|
||||
if ( is_null( $active_theme_name ) ) {
|
||||
$active_theme_name = wp_get_theme()->get( 'Name' );
|
||||
}
|
||||
/**
|
||||
* A list of pattern slugs to exclude from auto-insert (useful when
|
||||
* there are patterns that have a very specific location for the block)
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$pattern_exclude_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_pattern_exclude_list', array( 'twentytwentytwo/header-centered-logo', 'twentytwentytwo/header-stacked' ) );
|
||||
|
||||
/**
|
||||
* A list of theme slugs to execute this with. This is a temporary
|
||||
* measure until improvements to the Block Hooks API allow for exposing
|
||||
* to all block themes.
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$theme_include_list = apply_filters( 'woocommerce_blocks_mini_cart_auto_insert_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet' ) );
|
||||
|
||||
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
|
||||
if (
|
||||
'after' === $position &&
|
||||
'core/navigation' === $anchor_block &&
|
||||
$this->is_header_part_or_pattern( $context ) &&
|
||||
! $this->pattern_is_excluded( $context, $pattern_exclude_list ) &&
|
||||
! $this->has_mini_cart_block( $context )
|
||||
) {
|
||||
$hooked_blocks[] = 'woocommerce/' . $this->block_name;
|
||||
}
|
||||
}
|
||||
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the pattern is excluded or not
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param array $pattern_exclude_list List of pattern slugs to exclude.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function pattern_is_excluded( $context, $pattern_exclude_list ) {
|
||||
$pattern_slug = is_array( $context ) && isset( $context['slug'] ) ? $context['slug'] : '';
|
||||
if ( ! $pattern_slug ) {
|
||||
/**
|
||||
* Woo patterns have a slug property in $context, but core/theme patterns dont.
|
||||
* In that case, we fallback to the name property, as they're the same.
|
||||
*/
|
||||
$pattern_slug = is_array( $context ) && isset( $context['name'] ) ? $context['name'] : '';
|
||||
}
|
||||
return in_array( $pattern_slug, $pattern_exclude_list, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided context contains a mini-cart block.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_mini_cart_block( $context ) {
|
||||
/**
|
||||
* Note: this won't work for parsing WP_Block_Template instance until it's fixed in core
|
||||
* because $context->content is set as the result of `traverse_and_serialize_blocks` so
|
||||
* the filter callback doesn't get the original content.
|
||||
*
|
||||
* @see https://core.trac.wordpress.org/ticket/59882
|
||||
*/
|
||||
$content = is_array( $context ) && isset( $context['content'] ) ? $context['content'] : '';
|
||||
$content = '' === $content && $context instanceof \WP_Block_Template ? $context->content : $content;
|
||||
return strpos( $content, 'wp:woocommerce/mini-cart' ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a provided context, returns whether the context refers to header content.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_header_part_or_pattern( $context ) {
|
||||
$is_header_pattern = is_array( $context ) &&
|
||||
(
|
||||
( isset( $context['blockTypes'] ) && in_array( 'core/template-part/header', $context['blockTypes'], true ) ) ||
|
||||
( isset( $context['categories'] ) && in_array( 'header', $context['categories'], true ) )
|
||||
);
|
||||
$is_header_part = $context instanceof \WP_Block_Template && 'header' === $context->area;
|
||||
return ( $is_header_pattern || $is_header_part );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the Mini-Cart should be rendered or not.
|
||||
*
|
||||
|
||||
@@ -298,5 +298,40 @@ abstract class AbstractOrderConfirmationBlock extends AbstractBlock {
|
||||
)
|
||||
);
|
||||
|
||||
register_block_pattern(
|
||||
'woocommerce/order-confirmation-additional-fields-heading',
|
||||
array(
|
||||
'title' => '',
|
||||
'inserter' => false,
|
||||
'content' => '<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} --><h3 class="wp-block-heading" style="font-size:24px">' . esc_html__( 'Additional information', 'woocommerce' ) . '</h3><!-- /wp:heading -->',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom fields for the order.
|
||||
*
|
||||
* @param array $fields List of additional fields with values.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_fields( $fields ) {
|
||||
if ( empty( $fields ) ) {
|
||||
return '';
|
||||
}
|
||||
return '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom field row.
|
||||
*
|
||||
* @param array $field An additional field and value.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_field( $field ) {
|
||||
return sprintf(
|
||||
'<dt>%1$s</dt><dd>%2$s</dd>',
|
||||
esc_html( $field['label'] ),
|
||||
esc_html( $field['value'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* AdditionalFields class.
|
||||
*/
|
||||
class AdditionalFields extends AbstractOrderConfirmationBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'order-confirmation-additional-fields';
|
||||
|
||||
/**
|
||||
* This renders the content of the block within the wrapper.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string|false $permission If the current user can view the order details or not.
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Original block content.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
|
||||
if ( ! $permission ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$content .= $this->render_additional_fields(
|
||||
array_merge(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
|
||||
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
|
||||
)
|
||||
);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* AdditionalFieldsWrapper class.
|
||||
*/
|
||||
class AdditionalFieldsWrapper extends AbstractOrderConfirmationBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'order-confirmation-additional-fields-wrapper';
|
||||
|
||||
/**
|
||||
* This renders the content of the downloads wrapper.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string|false $permission If the current user can view the order details or not.
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Original block content.
|
||||
*/
|
||||
protected function render_content( $order, $permission = false, $attributes = [], $content = '' ) {
|
||||
if ( ! $permission ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Contact and additional fields are currently grouped in this section.
|
||||
$additional_fields = array_merge(
|
||||
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ),
|
||||
Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' )
|
||||
);
|
||||
|
||||
return empty( $additional_fields ) ? '' : $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'additional' ) );
|
||||
$this->asset_data_registry->add( 'additionalContactFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'contact' ) );
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* BillingAddress class.
|
||||
*/
|
||||
@@ -31,6 +34,23 @@ class BillingAddress extends AbstractOrderConfirmationBlock {
|
||||
$address = '<address>' . wp_kses_post( $order->get_formatted_billing_address() ) . '</address>';
|
||||
$phone = $order->get_billing_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_billing_phone() ) . '</p>' : '';
|
||||
|
||||
return $address . $phone;
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$custom = $this->render_additional_fields(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'address', 'billing', 'view' )
|
||||
);
|
||||
|
||||
return $address . $phone . $custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalAddressFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'address' ), true );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes\OrderConfirmation;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* ShippingAddress class.
|
||||
*/
|
||||
@@ -31,6 +34,23 @@ class ShippingAddress extends AbstractOrderConfirmationBlock {
|
||||
$address = '<address>' . wp_kses_post( $order->get_formatted_shipping_address() ) . '</address>';
|
||||
$phone = $order->get_shipping_phone() ? '<p class="woocommerce-customer-details--phone">' . esc_html( $order->get_shipping_phone() ) . '</p>' : '';
|
||||
|
||||
return $address . $phone;
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$custom = $this->render_additional_fields(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'address', 'shipping', 'view' )
|
||||
);
|
||||
|
||||
return $address . $phone . $custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
$this->asset_data_registry->add( 'additionalAddressFields', Package::container()->get( CheckoutFields::class )->get_fields_for_location( 'address' ), true );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ class ProductButton extends AbstractBlock {
|
||||
'wp_enqueue_scripts',
|
||||
array( $this, 'dequeue_add_to_cart_scripts' )
|
||||
);
|
||||
} else {
|
||||
$this->dequeue_add_to_cart_scripts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +214,7 @@ class ProductButton extends AbstractBlock {
|
||||
'{custom_classes}' => esc_attr( $classname . ' ' . $custom_width_classes . ' ' . $custom_align_classes ),
|
||||
'{html_element}' => $html_element,
|
||||
'{add_to_cart_url}' => esc_url( $product->add_to_cart_url() ),
|
||||
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
|
||||
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] . ' wc-interactive' ) : 'wc-interactive',
|
||||
'{button_styles}' => esc_attr( $styles_and_classes['styles'] ),
|
||||
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
|
||||
'{add_to_cart_text}' => esc_html( $initial_product_text ),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use WP_Query;
|
||||
use WC_Tax;
|
||||
|
||||
/**
|
||||
* ProductCollection class.
|
||||
@@ -95,6 +96,8 @@ class ProductCollection extends AbstractBlock {
|
||||
// Interactivity API: Add navigation directives to the product collection block.
|
||||
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'add_navigation_id_directive' ), 10, 3 );
|
||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +184,7 @@ class ProductCollection extends AbstractBlock {
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
protected function enqueue_data( array $attributes = array() ) {
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
|
||||
@@ -209,6 +212,7 @@ class ProductCollection extends AbstractBlock {
|
||||
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
|
||||
$featured = $request->get_param( 'featured' );
|
||||
$time_frame = $request->get_param( 'timeFrame' );
|
||||
$price_range = $request->get_param( 'priceRange' );
|
||||
// This argument is required for the tests to PHP Unit Tests to run correctly.
|
||||
// Most likely this argument is being accessed in the test environment image.
|
||||
$args['author'] = '';
|
||||
@@ -223,6 +227,7 @@ class ProductCollection extends AbstractBlock {
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $featured,
|
||||
'timeFrame' => $time_frame,
|
||||
'priceRange' => $price_range,
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -306,10 +311,11 @@ class ProductCollection extends AbstractBlock {
|
||||
);
|
||||
|
||||
$is_on_sale = $query['woocommerceOnSale'] ?? false;
|
||||
$product_attributes = $query['woocommerceAttributes'] ?? [];
|
||||
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
|
||||
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? [];
|
||||
$product_attributes = $query['woocommerceAttributes'] ?? array();
|
||||
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? array() );
|
||||
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? array();
|
||||
$time_frame = $query['timeFrame'] ?? null;
|
||||
$price_range = $query['priceRange'] ?? null;
|
||||
|
||||
$final_query = $this->get_final_query_args(
|
||||
$common_query_values,
|
||||
@@ -322,6 +328,7 @@ class ProductCollection extends AbstractBlock {
|
||||
'handpicked_products' => $handpicked_products,
|
||||
'featured' => $query['featured'] ?? false,
|
||||
'timeFrame' => $time_frame,
|
||||
'priceRange' => $price_range,
|
||||
),
|
||||
$is_exclude_applied_filters
|
||||
);
|
||||
@@ -337,23 +344,26 @@ class ProductCollection extends AbstractBlock {
|
||||
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
|
||||
*/
|
||||
private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
|
||||
$handpicked_products = $query['handpicked_products'] ?? [];
|
||||
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : [];
|
||||
$handpicked_products = $query['handpicked_products'] ?? array();
|
||||
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
|
||||
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
|
||||
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
|
||||
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : [];
|
||||
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query, $query['stock_status'] ) : array();
|
||||
$featured_query = $this->get_featured_query( $query['featured'] ?? false );
|
||||
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
|
||||
$taxonomies_query = $query['taxonomies_query'] ?? [];
|
||||
$taxonomies_query = $query['taxonomies_query'] ?? array();
|
||||
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query, $featured_query );
|
||||
$date_query = $this->get_date_query( $query['timeFrame'] ?? [] );
|
||||
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
|
||||
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
|
||||
|
||||
// We exclude applied filters to generate product ids for the filter blocks.
|
||||
$applied_filters_query = $is_exclude_applied_filters ? [] : $this->get_queries_by_applied_filters();
|
||||
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
|
||||
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query );
|
||||
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query, $date_query, $price_query_args );
|
||||
|
||||
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||
$result = $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +391,7 @@ class ProductCollection extends AbstractBlock {
|
||||
private function merge_queries( ...$queries ) {
|
||||
$merged_query = array_reduce(
|
||||
$queries,
|
||||
function( $acc, $query ) {
|
||||
function ( $acc, $query ) {
|
||||
if ( ! is_array( $query ) ) {
|
||||
return $acc;
|
||||
}
|
||||
@@ -489,6 +499,8 @@ class ProductCollection extends AbstractBlock {
|
||||
'posts_per_page',
|
||||
'suppress_filters',
|
||||
'tax_query',
|
||||
'isProductCollection',
|
||||
'priceRange',
|
||||
)
|
||||
);
|
||||
|
||||
@@ -547,15 +559,13 @@ class ProductCollection extends AbstractBlock {
|
||||
foreach ( $new as $key => $value ) {
|
||||
if ( is_numeric( $key ) ) {
|
||||
$base[] = $value;
|
||||
} else {
|
||||
if ( is_array( $value ) ) {
|
||||
if ( ! isset( $base[ $key ] ) ) {
|
||||
$base[ $key ] = array();
|
||||
}
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
} else {
|
||||
$base[ $key ] = $value;
|
||||
} elseif ( is_array( $value ) ) {
|
||||
if ( ! isset( $base[ $key ] ) ) {
|
||||
$base[ $key ] = array();
|
||||
}
|
||||
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
|
||||
} else {
|
||||
$base[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,14 +685,14 @@ class ProductCollection extends AbstractBlock {
|
||||
* @return array
|
||||
*/
|
||||
private function merge_tax_queries( ...$queries ) {
|
||||
$tax_query = [];
|
||||
$tax_query = array();
|
||||
foreach ( $queries as $query ) {
|
||||
if ( ! empty( $query['tax_query'] ) ) {
|
||||
$tax_query = array_merge( $tax_query, $query['tax_query'] );
|
||||
}
|
||||
}
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
return [ 'tax_query' => $tax_query ];
|
||||
return array( 'tax_query' => $tax_query );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -748,23 +758,23 @@ class ProductCollection extends AbstractBlock {
|
||||
*/
|
||||
private function get_filter_by_taxonomies_query( $tax_query ): array {
|
||||
if ( ! is_array( $tax_query ) ) {
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of taxonomy names associated with the "product" post type because
|
||||
* we also want to include custom taxonomies associated with the "product" post type.
|
||||
*/
|
||||
$product_taxonomies = get_taxonomies( [ 'object_type' => [ 'product' ] ], 'names' );
|
||||
$product_taxonomies = get_taxonomies( array( 'object_type' => array( 'product' ) ), 'names' );
|
||||
$result = array_filter(
|
||||
$tax_query,
|
||||
function( $item ) use ( $product_taxonomies ) {
|
||||
function ( $item ) use ( $product_taxonomies ) {
|
||||
return isset( $item['taxonomy'] ) && in_array( $item['taxonomy'], $product_taxonomies, true );
|
||||
}
|
||||
);
|
||||
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery
|
||||
return ! empty( $result ) ? [ 'tax_query' => $result ] : [];
|
||||
return ! empty( $result ) ? array( 'tax_query' => $result ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -807,19 +817,19 @@ class ProductCollection extends AbstractBlock {
|
||||
$min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR );
|
||||
$max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR );
|
||||
|
||||
$max_price_query = empty( $max_price ) ? array() : [
|
||||
$max_price_query = empty( $max_price ) ? array() : array(
|
||||
'key' => '_price',
|
||||
'value' => $max_price,
|
||||
'compare' => '<',
|
||||
'type' => 'numeric',
|
||||
];
|
||||
);
|
||||
|
||||
$min_price_query = empty( $min_price ) ? array() : [
|
||||
$min_price_query = empty( $min_price ) ? array() : array(
|
||||
'key' => '_price',
|
||||
'value' => $min_price,
|
||||
'compare' => '>=',
|
||||
'type' => 'numeric',
|
||||
];
|
||||
);
|
||||
|
||||
if ( empty( $min_price_query ) && empty( $max_price_query ) ) {
|
||||
return array();
|
||||
@@ -847,7 +857,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$queries = array_reduce(
|
||||
$attributes_filter_query_args,
|
||||
function( $acc, $query_args ) {
|
||||
function ( $acc, $query_args ) {
|
||||
$attribute_name = $query_args['filter'];
|
||||
$attribute_query_type = $query_args['query_type'];
|
||||
|
||||
@@ -912,7 +922,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$this->attributes_filter_query_args = array_reduce(
|
||||
wc_get_attribute_taxonomies(),
|
||||
function( $acc, $attribute ) {
|
||||
function ( $acc, $attribute ) {
|
||||
$acc[ $attribute->attribute_name ] = array(
|
||||
'filter' => AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute->attribute_name,
|
||||
'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute->attribute_name,
|
||||
@@ -939,7 +949,7 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$filtered_stock_status_values = array_filter(
|
||||
explode( ',', $filter_stock_status_values ),
|
||||
function( $stock_status ) {
|
||||
function ( $stock_status ) {
|
||||
return in_array( $stock_status, StockFilter::get_stock_status_query_var_values(), true );
|
||||
}
|
||||
);
|
||||
@@ -980,7 +990,7 @@ class ProductCollection extends AbstractBlock {
|
||||
}
|
||||
|
||||
$rating_terms = array_map(
|
||||
function( $rating ) use ( $product_visibility_terms ) {
|
||||
function ( $rating ) use ( $product_visibility_terms ) {
|
||||
return $product_visibility_terms[ 'rated-' . $rating ];
|
||||
},
|
||||
$parsed_filter_rating_values
|
||||
@@ -1011,7 +1021,7 @@ class ProductCollection extends AbstractBlock {
|
||||
* }
|
||||
* @return array Date query array; empty if parameters are invalid.
|
||||
*/
|
||||
private function get_date_query( array $time_frame ) : array {
|
||||
private function get_date_query( array $time_frame ): array {
|
||||
// Validate time_frame elements.
|
||||
if ( empty( $time_frame['operator'] ) || empty( $time_frame['value'] ) ) {
|
||||
return array();
|
||||
@@ -1032,5 +1042,184 @@ class ProductCollection extends AbstractBlock {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query arguments for price range filter.
|
||||
* We are adding these extra query arguments to be used in `posts_clauses`
|
||||
* because there are 2 special edge cases we wanna handle for Price range filter:
|
||||
* Case 1: Prices excluding tax are displayed including tax
|
||||
* Case 2: Prices including tax are displayed excluding tax
|
||||
*
|
||||
* Both of these cases require us to modify SQL query to get the correct results.
|
||||
*
|
||||
* See add_price_range_filter_posts_clauses function in this file for more details.
|
||||
*
|
||||
* @param array $price_range Price range with min and max values.
|
||||
* @return array Query arguments.
|
||||
*/
|
||||
public function get_price_range_query_args( $price_range ) {
|
||||
if ( empty( $price_range ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'isProductCollection' => true,
|
||||
'priceRange' => $price_range,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the `posts_clauses` filter to the main query.
|
||||
*
|
||||
* @param array $clauses The query clauses.
|
||||
* @param WP_Query $query The WP_Query instance.
|
||||
*/
|
||||
public function add_price_range_filter_posts_clauses( $clauses, $query ) {
|
||||
$query_vars = $query->query_vars;
|
||||
$is_product_collection_block = $query_vars['isProductCollection'] ?? false;
|
||||
if ( ! $is_product_collection_block ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$price_range = $query_vars['priceRange'] ?? null;
|
||||
if ( empty( $price_range ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$adjust_for_taxes = $this->should_adjust_price_range_for_taxes();
|
||||
$clauses['join'] = $this->append_product_sorting_table_join( $clauses['join'] );
|
||||
|
||||
$min_price = $price_range['min'] ?? null;
|
||||
if ( $min_price ) {
|
||||
if ( $adjust_for_taxes ) {
|
||||
$clauses['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price, 'min_price', '>=' );
|
||||
} else {
|
||||
$clauses['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', $min_price );
|
||||
}
|
||||
}
|
||||
|
||||
$max_price = $price_range['max'] ?? null;
|
||||
if ( $max_price ) {
|
||||
if ( $adjust_for_taxes ) {
|
||||
$clauses['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price, 'max_price', '<=' );
|
||||
} else {
|
||||
$clauses['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', $max_price );
|
||||
}
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if price filters need adjustment based on the tax display settings.
|
||||
*
|
||||
* This function checks if there's a discrepancy between how prices are stored in the database
|
||||
* and how they are displayed to the user, specifically with respect to tax inclusion or exclusion.
|
||||
* It returns true if an adjustment is needed, indicating that the price filters should account for this
|
||||
* discrepancy to display accurate prices.
|
||||
*
|
||||
* @return bool True if the price filters need to be adjusted for tax display settings, false otherwise.
|
||||
*/
|
||||
private function should_adjust_price_range_for_taxes() {
|
||||
$display_setting = get_option( 'woocommerce_tax_display_shop' ); // Tax display setting ('incl' or 'excl').
|
||||
$price_storage_method = wc_prices_include_tax() ? 'incl' : 'excl';
|
||||
|
||||
return $display_setting !== $price_storage_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wc_product_meta_lookup to posts if not already joined.
|
||||
*
|
||||
* @param string $sql SQL join.
|
||||
* @return string
|
||||
*/
|
||||
protected function append_product_sorting_table_join( $sql ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
|
||||
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query for price filters when dealing with displayed taxes.
|
||||
*
|
||||
* @param float $price_filter Price filter to apply.
|
||||
* @param string $column Price being filtered (min or max).
|
||||
* @param string $operator Comparison operator for column.
|
||||
* @return string Constructed query.
|
||||
*/
|
||||
protected function get_price_filter_query_for_displayed_taxes( $price_filter, $column = 'min_price', $operator = '>=' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Select only used tax classes to avoid unwanted calculations.
|
||||
$product_tax_classes = $wpdb->get_col( "SELECT DISTINCT tax_class FROM {$wpdb->wc_product_meta_lookup};" );
|
||||
|
||||
if ( empty( $product_tax_classes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$or_queries = array();
|
||||
|
||||
// We need to adjust the filter for each possible tax class and combine the queries into one.
|
||||
foreach ( $product_tax_classes as $tax_class ) {
|
||||
$adjusted_price_filter = $this->adjust_price_filter_for_tax_class( $price_filter, $tax_class );
|
||||
$or_queries[] = $wpdb->prepare(
|
||||
'( wc_product_meta_lookup.tax_class = %s AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )',
|
||||
$tax_class,
|
||||
$adjusted_price_filter
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $wpdb->prepare(
|
||||
' AND (
|
||||
wc_product_meta_lookup.tax_status = "taxable" AND ( 0=1 OR ' . implode( ' OR ', $or_queries ) . ')
|
||||
OR ( wc_product_meta_lookup.tax_status != "taxable" AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )
|
||||
) ',
|
||||
$price_filter
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a price filter based on a tax class and whether or not the amount includes or excludes taxes.
|
||||
*
|
||||
* This calculation logic is based on `wc_get_price_excluding_tax` and `wc_get_price_including_tax` in core.
|
||||
*
|
||||
* @param float $price_filter Price filter amount as entered.
|
||||
* @param string $tax_class Tax class for adjustment.
|
||||
* @return float
|
||||
*/
|
||||
protected function adjust_price_filter_for_tax_class( $price_filter, $tax_class ) {
|
||||
$tax_display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$tax_rates = WC_Tax::get_rates( $tax_class );
|
||||
$base_tax_rates = WC_Tax::get_base_tax_rates( $tax_class );
|
||||
|
||||
// If prices are shown incl. tax, we want to remove the taxes from the filter amount to match prices stored excl. tax.
|
||||
if ( 'incl' === $tax_display ) {
|
||||
/**
|
||||
* Filters if taxes should be removed from locations outside the store base location.
|
||||
*
|
||||
* The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing
|
||||
* with out of base locations. e.g. If a product costs 10 including tax, all users will pay 10
|
||||
* regardless of location and taxes.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*
|
||||
* @internal Matches filter name in WooCommerce core.
|
||||
*
|
||||
* @param boolean $adjust_non_base_location_prices True by default.
|
||||
* @return boolean
|
||||
*/
|
||||
$taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $price_filter, $base_tax_rates, true ) : WC_Tax::calc_tax( $price_filter, $tax_rates, true );
|
||||
return $price_filter - array_sum( $taxes );
|
||||
}
|
||||
|
||||
// If prices are shown excl. tax, add taxes to match the prices stored in the DB.
|
||||
$taxes = WC_Tax::calc_tax( $price_filter, $tax_rates, false );
|
||||
|
||||
return $price_filter + array_sum( $taxes );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,25 @@ class ProductDetails extends AbstractBlock {
|
||||
* It isn't necessary register block assets because it is a server side block.
|
||||
*/
|
||||
protected function register_block_type_assets() {
|
||||
|
||||
// Register block styles.
|
||||
register_block_style(
|
||||
'woocommerce/product-details',
|
||||
array(
|
||||
'name' => 'classic',
|
||||
'label' => __( 'Classic', 'woocommerce' ),
|
||||
'is_default' => true,
|
||||
)
|
||||
);
|
||||
|
||||
register_block_style(
|
||||
'woocommerce/product-details',
|
||||
array(
|
||||
'name' => 'minimal',
|
||||
'label' => __( 'Minimal', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* Product Filter Block.
|
||||
*/
|
||||
final class ProductFilter extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'product-filter';
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $current_response = null;
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @see $this->register_block_type()
|
||||
* @param string $key Data to get, or default to everything.
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
* - Hook into WP lifecycle.
|
||||
* - Register the block with WordPress.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
* @param array $attributes Any attributes that currently are available from the block.
|
||||
* Note, this will be empty in the editor context when the block is
|
||||
* not in the post content on editor load.
|
||||
*/
|
||||
protected function enqueue_data( array $attributes = [] ) {
|
||||
global $pagenow;
|
||||
parent::enqueue_data( $attributes );
|
||||
|
||||
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
|
||||
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy(), true );
|
||||
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow, true );
|
||||
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the collection data is empty.
|
||||
*
|
||||
* @param mixed $attributes - Block attributes.
|
||||
* @return bool - Whether the collection data is empty.
|
||||
*/
|
||||
private function collection_data_is_empty( $attributes ) {
|
||||
$filter_type = $attributes['filterType'];
|
||||
|
||||
if ( 'active-filters' !== $filter_type && empty( $this->current_response ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( 'attribute-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['attribute_counts'] );
|
||||
}
|
||||
|
||||
if ( 'rating-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['rating_counts'] );
|
||||
}
|
||||
|
||||
if ( 'price-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['price_range'] ) || ( $this->current_response['price_range']['min_price'] === $this->current_response['price_range']['max_price'] );
|
||||
}
|
||||
|
||||
if ( 'stock-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['stock_status_counts'] );
|
||||
}
|
||||
|
||||
if ( 'active-filters' === $filter_type ) {
|
||||
// Duplicate query param logic from ProductFilterActive block, to determine if we should
|
||||
// display the ProductFilter block or not.
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$parsed_url = wp_parse_url( esc_url_raw( $request_uri ) );
|
||||
|
||||
$url_query_params = [];
|
||||
|
||||
if ( isset( $parsed_url['query'] ) ) {
|
||||
parse_str( $parsed_url['query'], $url_query_params );
|
||||
}
|
||||
|
||||
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
return empty( array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @param string $content Block content.
|
||||
* @param WP_Block $block Block instance.
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if ( is_admin() ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ( $this->collection_data_is_empty( $attributes ) ) {
|
||||
return $this->render_empty_block( $block );
|
||||
}
|
||||
|
||||
return $this->render_filter_block( $content, $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current response, must be done before rendering.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function reset_current_response() {
|
||||
/**
|
||||
* When WP starts rendering the Product Filters block,
|
||||
* we can safely unset the current response.
|
||||
*/
|
||||
$this->current_response = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's empty.
|
||||
*
|
||||
* @param mixed $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_empty_block( $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s></nav>',
|
||||
get_block_wrapper_attributes(
|
||||
$attributes
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique navigation ID for the block.
|
||||
*
|
||||
* @param mixed $block - Block instance.
|
||||
* @return string - Unique navigation ID.
|
||||
*/
|
||||
private function generate_navigation_id( $block ) {
|
||||
return sprintf(
|
||||
'wc-product-filter-%s',
|
||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's not empty.
|
||||
*
|
||||
* @param string $content - Block content.
|
||||
* @param WP_Block $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_filter_block( $content, $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the context of inner blocks.
|
||||
*
|
||||
* @param array $context The block context.
|
||||
* @param array $parsed_block The parsed block.
|
||||
* @param WP_Block $parent_block The parent block.
|
||||
* @return array
|
||||
*/
|
||||
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
|
||||
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first direct child of Product Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
if ( empty( $this->current_response ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated collection data from the API.
|
||||
* Loop through inner blocks and build a query string to pass to the API.
|
||||
*
|
||||
* @param WP_Block $block The block instance.
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'min_price' => null,
|
||||
'max_price' => null,
|
||||
'attribute_counts' => null,
|
||||
'stock_status_counts' => null,
|
||||
'rating_counts' => null,
|
||||
);
|
||||
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
|
||||
if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
|
||||
$query_vars = build_query_vars_from_query_block( $block, 1 );
|
||||
} else {
|
||||
global $wp_query;
|
||||
$query_vars = array_filter( $wp_query->query_vars );
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_price_range'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['min_price'], $filter_query_vars['max_price'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_price' );
|
||||
}
|
||||
|
||||
$price_results = $filters->get_filtered_price( $filter_query_vars );
|
||||
$data['price_range'] = array(
|
||||
'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
|
||||
'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_stock_status_counts'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['filter_stock_status'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_stock_status' );
|
||||
}
|
||||
|
||||
$counts = $filters->get_stock_status_counts( $filter_query_vars );
|
||||
|
||||
$data['stock_status_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['stock_status_counts'][] = array(
|
||||
'status' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_rating_counts'] ) ) {
|
||||
// Regenerate the products query vars without rating filter.
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'rating_filter', true );
|
||||
}
|
||||
|
||||
$counts = $filters->get_rating_counts( $filter_query_vars );
|
||||
$data['rating_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['rating_counts'][] = array(
|
||||
'rating' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_attribute_counts'] ) ) {
|
||||
foreach ( $collection_data_params['calculate_attribute_counts'] as $attributes_to_count ) {
|
||||
if ( ! isset( $attributes_to_count['taxonomy'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( 'and' !== strtolower( $attributes_to_count['queryType'] ) ) {
|
||||
unset( $filter_query_vars[ 'filter_' . str_replace( 'pa_', '', $attributes_to_count['taxonomy'] ) ] );
|
||||
}
|
||||
|
||||
unset(
|
||||
$filter_query_vars['taxonomy'],
|
||||
$filter_query_vars['term']
|
||||
);
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'taxonomy', $attributes_to_count['taxonomy'] );
|
||||
}
|
||||
|
||||
$counts = $filters->get_attribute_counts( $filter_query_vars, $attributes_to_count['taxonomy'] );
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['attribute_counts'][] = array(
|
||||
'term' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query array from tax or meta query by searching for arrays that
|
||||
* contain exact key => value pair.
|
||||
*
|
||||
* @param array $queries tax_query or meta_query.
|
||||
* @param string $key Array key to search for.
|
||||
* @param mixed $value Value to compare with search result.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function remove_query_array( $queries, $key, $value ) {
|
||||
if ( empty( $queries ) ) {
|
||||
return $queries;
|
||||
}
|
||||
|
||||
foreach ( $queries as $query_key => $query ) {
|
||||
if ( isset( $query[ $key ] ) && $query[ $key ] === $value ) {
|
||||
unset( $queries[ $query_key ] );
|
||||
}
|
||||
|
||||
if ( isset( $query['relation'] ) ) {
|
||||
$queries[ $query_key ] = $this->remove_query_array( $query, $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inner blocks recursively.
|
||||
*
|
||||
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
|
||||
* @param array $results The results array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_collection_data_params( $inner_blocks, &$results = array() ) {
|
||||
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
|
||||
foreach ( $inner_blocks as $inner_block ) {
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$query_param = $inner_block->attributes['queryParam'];
|
||||
/**
|
||||
* There can be multiple attribute filters so we transform
|
||||
* the query param of each filter into an array to merge
|
||||
* them together.
|
||||
*/
|
||||
if ( ! empty( $query_param['calculate_attribute_counts'] ) ) {
|
||||
$query_param['calculate_attribute_counts'] = array( $query_param['calculate_attribute_counts'] );
|
||||
}
|
||||
$results = array_merge_recursive( $results, $query_param );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,15 +2,15 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CollectionAttributeFilter class.
|
||||
* Product Filter: Active Block.
|
||||
*/
|
||||
final class CollectionActiveFilters extends AbstractBlock {
|
||||
final class ProductFilterActive extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-active-filters';
|
||||
protected $block_name = 'product-filter-active';
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
@@ -48,10 +48,6 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
*/
|
||||
$active_filters = apply_filters( 'collection_active_filters_data', array(), $this->get_filter_query_params( $query_id ) );
|
||||
|
||||
if ( empty( $active_filters ) ) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'queryId' => $query_id,
|
||||
'params' => array_keys( $this->get_filter_query_params( $query_id ) ),
|
||||
@@ -60,7 +56,7 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => 'wc-block-active-filters',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-active-filters' ) ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
)
|
||||
);
|
||||
@@ -68,22 +64,23 @@ final class CollectionActiveFilters extends AbstractBlock {
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div <?php echo $wrapper_attributes; ?>>
|
||||
<ul class="wc-block-active-filters__list %3$s">
|
||||
<?php foreach ( $active_filters as $filter ) : ?>
|
||||
<li>
|
||||
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<ul>
|
||||
<?php $this->render_items( $filter['items'], $attributes['displayStyle'] ); ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<button class="wc-block-active-filters__clear-all" data-wc-on--click="actions.clearAll">
|
||||
<span aria-hidden="true"><?php echo esc_html__( 'Clear All', 'woocommerce' ); ?></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Clear All Filters', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<?php if ( ! empty( $active_filters ) ) : ?>
|
||||
<ul class="wc-block-active-filters__list %3$s">
|
||||
<?php foreach ( $active_filters as $filter ) : ?>
|
||||
<li>
|
||||
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<ul>
|
||||
<?php $this->render_items( $filter['items'], $attributes['displayStyle'] ); ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<button class="wc-block-active-filters__clear-all" data-wc-on--click="actions.clearAll">
|
||||
<span aria-hidden="true"><?php echo esc_html__( 'Clear All', 'woocommerce' ); ?></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Clear All Filters', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
@@ -2,18 +2,19 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
|
||||
/**
|
||||
* CollectionAttributeFilter class.
|
||||
* Product Filter: Attribute Block.
|
||||
*/
|
||||
final class CollectionAttributeFilter extends AbstractBlock {
|
||||
final class ProductFilterAttribute extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-attribute-filter';
|
||||
protected $block_name = 'product-filter-attribute';
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
@@ -85,18 +86,20 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
}
|
||||
);
|
||||
|
||||
$action_namespace = $this->get_full_block_name();
|
||||
|
||||
foreach ( $active_product_attributes as $product_attribute ) {
|
||||
$terms = explode( ',', get_query_var( "filter_{$product_attribute}" ) );
|
||||
|
||||
// Get attribute term by slug.
|
||||
$terms = array_map(
|
||||
function( $term ) use ( $product_attribute ) {
|
||||
function( $term ) use ( $product_attribute, $action_namespace ) {
|
||||
$term_object = get_term_by( 'slug', $term, "pa_{$product_attribute}" );
|
||||
return array(
|
||||
'title' => $term_object->name,
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-attribute-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-attribute-filter::' . wp_json_encode(
|
||||
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
|
||||
'data-wc-context' => "$action_namespace::" . wp_json_encode(
|
||||
array(
|
||||
'value' => $term,
|
||||
'attributeSlug' => $product_attribute,
|
||||
@@ -127,18 +130,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if (
|
||||
is_admin() ||
|
||||
empty( $block->context['collectionData']['attribute_counts'] ) ||
|
||||
empty( $attributes['attributeId'] )
|
||||
) {
|
||||
return $content;
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
||||
|
||||
$attribute_counts = array_reduce(
|
||||
$block->context['collectionData']['attribute_counts'],
|
||||
$attribute_counts = array_reduce(
|
||||
$block->context['collectionData']['attribute_counts'] ?? [],
|
||||
function( $acc, $count ) {
|
||||
$acc[ $count['term'] ] = $count['count'];
|
||||
return $acc;
|
||||
@@ -146,6 +145,17 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
[]
|
||||
);
|
||||
|
||||
if ( empty( $attribute_counts ) ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$attribute_terms = get_terms(
|
||||
array(
|
||||
'taxonomy' => $product_attribute->slug,
|
||||
@@ -170,7 +180,16 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
$attribute_terms
|
||||
);
|
||||
|
||||
$filter_content = 'dropdown' === $attributes['displayStyle'] ? $this->render_attribute_dropdown( $attribute_options, $attributes ) : $this->render_attribute_list( $attribute_options, $attributes );
|
||||
$filtered_options = array_filter(
|
||||
$attribute_options,
|
||||
function( $option ) {
|
||||
return $option['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
$filter_content = 'dropdown' === $attributes['displayStyle'] ?
|
||||
$this->render_attribute_dropdown( $filtered_options, $attributes ) :
|
||||
$this->render_attribute_checkbox_list( $filtered_options, $attributes );
|
||||
|
||||
$context = array(
|
||||
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
||||
@@ -179,13 +198,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>%2$s</div>',
|
||||
'<div %1$s>%2$s%3$s</div>',
|
||||
get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => 'woocommerce/collection-attribute-filter' ) ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
)
|
||||
),
|
||||
$content,
|
||||
$filter_content
|
||||
);
|
||||
}
|
||||
@@ -197,8 +217,14 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
* @param bool $attributes Block attributes.
|
||||
*/
|
||||
private function render_attribute_dropdown( $options, $attributes ) {
|
||||
$list_items = array();
|
||||
$selected_item = array();
|
||||
if ( empty( $options ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$list_items = array();
|
||||
$selected_items = array();
|
||||
|
||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
||||
|
||||
foreach ( $options as $option ) {
|
||||
$item = array(
|
||||
@@ -209,79 +235,53 @@ final class CollectionAttributeFilter extends AbstractBlock {
|
||||
$list_items[] = $item;
|
||||
|
||||
if ( $option['selected'] ) {
|
||||
$selected_item = $item;
|
||||
$selected_items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'woocommerce/collection-attribute-filter::actions.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
'items' => $list_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.navigate",
|
||||
'selected_items' => $selected_items,
|
||||
'select_type' => $attributes['selectType'] ?? 'multiple',
|
||||
// translators: %s is a product attribute name.
|
||||
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list.
|
||||
* Render the attribute filter checkbox list.
|
||||
*
|
||||
* @param array $options Data to render the list.
|
||||
* @param bool $attributes Block attributes.
|
||||
* @param mixed $options Attribute filter options to render in the checkbox list.
|
||||
* @param mixed $attributes Block attributes.
|
||||
* @return string
|
||||
*/
|
||||
private function render_attribute_list( $options, $attributes ) {
|
||||
$output = '<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">';
|
||||
foreach ( $options as $option ) {
|
||||
$output .= $this->render_list_item_template( $option, $attributes['showCounts'] );
|
||||
private function render_attribute_checkbox_list( $options, $attributes ) {
|
||||
if ( empty( $options ) ) {
|
||||
return '';
|
||||
}
|
||||
$output .= '</ul>';
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list item.
|
||||
*
|
||||
* @param array $option Data to render the list item.
|
||||
* @param bool $show_counts Whether to display the count.
|
||||
*/
|
||||
private function render_list_item_template( $option, $show_counts ) {
|
||||
$count_html = $show_counts ?
|
||||
sprintf(
|
||||
'<span class="wc-filter-element-label-list-count">
|
||||
<span aria-hidden="true">%1$s</span>
|
||||
<span class="screen-reader-text">%2$s</span>
|
||||
</span>',
|
||||
$option['count'],
|
||||
// translators: %d is the number of products.
|
||||
sprintf( _n( '%d product', '%d products', $option['count'], 'woocommerce' ), $option['count'] )
|
||||
) :
|
||||
'';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
$template = '<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="%1$s">
|
||||
<input
|
||||
id="%1$s"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
data-wc-context=\'{ "attributeTermSlug": "%5$s" }\'
|
||||
value="%5$s"
|
||||
%4$s
|
||||
/>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path></svg>
|
||||
<span class="wc-block-components-checkbox__label">%2$s %3$s</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>';
|
||||
$list_options = array_map(
|
||||
function( $option ) use ( $show_counts ) {
|
||||
return array(
|
||||
'id' => $option['slug'] . '-' . $option['term_id'],
|
||||
'checked' => $option['selected'],
|
||||
'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
|
||||
'value' => $option['slug'],
|
||||
);
|
||||
},
|
||||
$options
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
$template,
|
||||
esc_attr( $option['slug'] ) . '-' . $option['term_id'],
|
||||
esc_html( $option['name'] ),
|
||||
$count_html,
|
||||
checked( $option['selected'], true, false ),
|
||||
esc_attr( $option['slug'] )
|
||||
return CheckboxList::render(
|
||||
array(
|
||||
'items' => $list_options,
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
/**
|
||||
* CollectionPriceFilter class.
|
||||
* Product Filter: Price Block.
|
||||
*/
|
||||
final class CollectionPriceFilter extends AbstractBlock {
|
||||
final class ProductFilterPrice extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-price-filter';
|
||||
protected $block_name = 'product-filter-price';
|
||||
|
||||
const MIN_PRICE_QUERY_VAR = 'min_price';
|
||||
const MAX_PRICE_QUERY_VAR = 'max_price';
|
||||
@@ -93,7 +93,7 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
array(
|
||||
'title' => $title,
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-price-filter::actions.reset',
|
||||
'data-wc-on--click' => "{$this->get_full_block_name()}::actions.reset",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -111,19 +111,14 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
protected function render( $attributes, $content, $block ) {
|
||||
if (
|
||||
is_admin() ||
|
||||
empty( $block->context['collectionData'] ) ||
|
||||
empty( $block->context['collectionData']['price_range'] )
|
||||
) {
|
||||
return $content;
|
||||
// don't render if its admin, or ajax in progress.
|
||||
if ( is_admin() || wp_doing_ajax() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$price_range = $block->context['collectionData']['price_range'];
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
$min_range = $price_range['min_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$max_range = $price_range['max_price'] / 10 ** $price_range['currency_minor_unit'];
|
||||
$price_range = $block->context['collectionData']['price_range'] ?? [];
|
||||
$min_range = $price_range['min_price'] ?? 0;
|
||||
$max_range = $price_range['max_price'] ?? 0;
|
||||
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
|
||||
$max_price = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
|
||||
$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
|
||||
@@ -136,19 +131,24 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
'maxRange' => $max_range,
|
||||
);
|
||||
|
||||
wc_initial_state(
|
||||
'woocommerce/collection-price-filter',
|
||||
$data
|
||||
);
|
||||
|
||||
list (
|
||||
'showInputFields' => $show_input_fields,
|
||||
'inlineInput' => $inline_input
|
||||
) = $attributes;
|
||||
|
||||
// Max range shouldn't be 0.
|
||||
if ( ! $max_range ) {
|
||||
return $content;
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $data ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $min_range === $max_range || ! $max_range ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
// CSS variables for the range bar style.
|
||||
@@ -156,23 +156,13 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
$__high = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
|
||||
$range_style = "--low: $__low%; --high: $__high%";
|
||||
|
||||
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-price-filter' ) );
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => $data_directive,
|
||||
)
|
||||
);
|
||||
|
||||
$price_min = $show_input_fields ?
|
||||
sprintf(
|
||||
'<input
|
||||
class="min"
|
||||
type="text"
|
||||
value="%d"
|
||||
data-wc-bind--value="state.minPrice"
|
||||
data-wc-on--input="actions.setMinPrice"
|
||||
data-wc-bind--value="context.minPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
/>',
|
||||
esc_attr( $min_price )
|
||||
@@ -188,8 +178,7 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
class="max"
|
||||
type="text"
|
||||
value="%d"
|
||||
data-wc-bind--value="state.maxPrice"
|
||||
data-wc-on--input="actions.setMaxPrice"
|
||||
data-wc-bind--value="context.maxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
/>',
|
||||
esc_attr( $max_price )
|
||||
@@ -202,41 +191,44 @@ final class CollectionPriceFilter extends AbstractBlock {
|
||||
ob_start();
|
||||
?>
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<div
|
||||
class="range"
|
||||
style="<?php echo esc_attr( $range_style ); ?>"
|
||||
data-wc-bind--style="state.rangeStyle"
|
||||
>
|
||||
<div class="range-bar"></div>
|
||||
<input
|
||||
type="range"
|
||||
class="min"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $min_price ); ?>"
|
||||
data-wc-bind--max="state.maxRange"
|
||||
data-wc-bind--value="state.minPrice"
|
||||
data-wc-class--active="state.isMinActive"
|
||||
data-wc-on--input="actions.setMinPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div class="filter-controls">
|
||||
<div
|
||||
class="range"
|
||||
style="<?php echo esc_attr( $range_style ); ?>"
|
||||
data-wc-bind--style="state.rangeStyle"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
class="max"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $max_price ); ?>"
|
||||
data-wc-bind--max="state.maxRange"
|
||||
data-wc-bind--value="state.maxPrice"
|
||||
data-wc-class--active="state.isMaxActive"
|
||||
data-wc-on--input="actions.setMaxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
</div>
|
||||
<div class="text">
|
||||
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
|
||||
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div class="range-bar"></div>
|
||||
<input
|
||||
type="range"
|
||||
class="min"
|
||||
name="min"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $min_price ); ?>"
|
||||
data-wc-bind--min="context.minRange"
|
||||
data-wc-bind--max="context.maxRange"
|
||||
data-wc-bind--value="context.minPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
class="max"
|
||||
name="max"
|
||||
min="<?php echo esc_attr( $min_range ); ?>"
|
||||
max="<?php echo esc_attr( $max_range ); ?>"
|
||||
value="<?php echo esc_attr( $max_price ); ?>"
|
||||
data-wc-bind--min="context.minRange"
|
||||
data-wc-bind--max="context.maxRange"
|
||||
data-wc-bind--value="context.maxPrice"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
>
|
||||
</div>
|
||||
<div class="text">
|
||||
<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
|
||||
<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
@@ -5,17 +5,17 @@ use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
|
||||
/**
|
||||
* Collection Rating Filter Block
|
||||
* Product Filter: Rating Block
|
||||
*
|
||||
* @package Automattic\WooCommerce\Blocks\BlockTypes
|
||||
*/
|
||||
final class CollectionRatingFilter extends AbstractBlock {
|
||||
final class ProductFilterRating extends AbstractBlock {
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-rating-filter';
|
||||
protected $block_name = 'product-filter-rating';
|
||||
|
||||
const RATING_FILTER_QUERY_VAR = 'rating_filter';
|
||||
|
||||
@@ -80,8 +80,8 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
/* translators: %d is the rating value. */
|
||||
'title' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating ),
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-rating-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-rating-filter::' . wp_json_encode( array( 'value' => $rating ) ),
|
||||
'data-wc-on--click' => "{$this->get_full_block_name()}::actions.removeFilter",
|
||||
'data-wc-context' => "{$this->get_full_block_name()}::" . wp_json_encode( array( 'value' => $rating ) ),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -114,31 +114,47 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_ratings_query_param = isset( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => 'woocommerce/collection-rating-filter',
|
||||
'data-wc-interactive' => $this->get_full_block_name(),
|
||||
'class' => 'wc-block-rating-filter',
|
||||
)
|
||||
);
|
||||
|
||||
$filtered_rating_counts = array_filter(
|
||||
$rating_counts,
|
||||
function( $rating ) {
|
||||
return $rating['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $filtered_rating_counts ) ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_ratings_query_param = isset( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::RATING_FILTER_QUERY_VAR ] ) ) : '';
|
||||
|
||||
$input = 'list' === $display_style ? CheckboxList::render(
|
||||
array(
|
||||
'items' => $this->get_checkbox_list_items( $rating_counts, $selected_ratings_query_param, $show_counts ),
|
||||
'on_change' => 'woocommerce/collection-rating-filter::actions.onCheckboxChange',
|
||||
'items' => $this->get_checkbox_list_items( $filtered_rating_counts, $selected_ratings_query_param, $show_counts ),
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.onCheckboxChange",
|
||||
)
|
||||
) : Dropdown::render(
|
||||
$this->get_dropdown_props( $rating_counts, $selected_ratings_query_param, $show_counts )
|
||||
$this->get_dropdown_props( $filtered_rating_counts, $selected_ratings_query_param, $show_counts, $attributes['selectType'] )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-rating-filter__controls">%2$s</div>
|
||||
%2$s
|
||||
<div class="wc-block-rating-filter__controls">%3$s</div>
|
||||
<div class="wc-block-rating-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$input
|
||||
);
|
||||
}
|
||||
@@ -205,22 +221,24 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
/**
|
||||
* Get the dropdown props.
|
||||
*
|
||||
* @param mixed $rating_counts The rating counts.
|
||||
* @param mixed $selected_ratings_query The url query param for selected ratings.
|
||||
* @param mixed $show_counts Whether to show the counts.
|
||||
* @param mixed $rating_counts The rating counts.
|
||||
* @param mixed $selected_ratings_query The url query param for selected ratings.
|
||||
* @param bool $show_counts Whether to show the counts.
|
||||
* @param string $select_type The select type. (single|multiple).
|
||||
* @return array<array-key, array>
|
||||
*/
|
||||
private function get_dropdown_props( $rating_counts, $selected_ratings_query, $show_counts ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
private function get_dropdown_props( $rating_counts, $selected_ratings_query, $show_counts, $select_type ) {
|
||||
$ratings_array = explode( ',', $selected_ratings_query );
|
||||
$placeholder_text = 'single' === $select_type ? __( 'Select a rating', 'woocommerce' ) : __( 'Select ratings', 'woocommerce' );
|
||||
|
||||
$selected_item = array_reduce(
|
||||
$selected_items = array_reduce(
|
||||
$rating_counts,
|
||||
function( $carry, $rating ) use ( $ratings_array, $show_counts ) {
|
||||
if ( in_array( (string) $rating['rating'], $ratings_array, true ) ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
$rating_str = (string) $rating['rating'];
|
||||
return array(
|
||||
$carry[] = array(
|
||||
/* translators: %d is referring to the average rating value. Example: Rated 4 out of 5. */
|
||||
'label' => sprintf( __( 'Rated %d out of 5', 'woocommerce' ), $rating_str ) . ' ' . $count_label,
|
||||
'value' => $rating['rating'],
|
||||
@@ -232,7 +250,7 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
);
|
||||
|
||||
return array(
|
||||
'items' => array_map(
|
||||
'items' => array_map(
|
||||
function ( $rating ) use ( $show_counts ) {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
@@ -245,8 +263,10 @@ final class CollectionRatingFilter extends AbstractBlock {
|
||||
},
|
||||
$rating_counts
|
||||
),
|
||||
'selected_item' => $selected_item,
|
||||
'action' => 'woocommerce/collection-rating-filter::actions.onDropdownChange',
|
||||
'select_type' => $select_type,
|
||||
'selected_items' => $selected_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.onDropdownChange",
|
||||
'placeholder' => $placeholder_text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,19 @@
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
|
||||
/**
|
||||
* CollectionStockFilter class.
|
||||
* Product Filter: Stock Status Block.
|
||||
*/
|
||||
final class CollectionStockFilter extends AbstractBlock {
|
||||
final class ProductFilterStockStatus extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $block_name = 'collection-stock-filter';
|
||||
protected $block_name = 'product-filter-stock-status';
|
||||
|
||||
const STOCK_STATUS_QUERY_VAR = 'filter_stock_status';
|
||||
|
||||
@@ -74,13 +75,15 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$action_namespace = $this->get_full_block_name();
|
||||
|
||||
$active_stock_statuses = array_map(
|
||||
function( $status ) use ( $stock_status_options ) {
|
||||
function( $status ) use ( $stock_status_options, $action_namespace ) {
|
||||
return array(
|
||||
'title' => $stock_status_options[ $status ],
|
||||
'attributes' => array(
|
||||
'data-wc-on--click' => 'woocommerce/collection-stock-filter::actions.removeFilter',
|
||||
'data-wc-context' => 'woocommerce/collection-stock-filter::' . wp_json_encode( array( 'value' => $status ) ),
|
||||
'data-wc-on--click' => "$action_namespace::actions.removeFilter",
|
||||
'data-wc-context' => "$action_namespace::" . wp_json_encode( array( 'value' => $status ) ),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -127,10 +130,12 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
<div class="wc-block-stock-filter__controls">%2$s</div>
|
||||
%2$s
|
||||
<div class="wc-block-stock-filter__controls">%3$s</div>
|
||||
<div class="wc-block-stock-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$this->get_stock_filter_html( $stock_status_counts, $attributes ),
|
||||
);
|
||||
}
|
||||
@@ -145,97 +150,78 @@ final class CollectionStockFilter extends AbstractBlock {
|
||||
private function get_stock_filter_html( $stock_counts, $attributes ) {
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
$select_type = $attributes['selectType'] ?? 'single';
|
||||
$stock_statuses = wc_get_product_stock_status_options();
|
||||
|
||||
$placeholder_text = 'single' === $select_type ? __( 'Select stock status', 'woocommerce' ) : __( 'Select stock statuses', 'woocommerce' );
|
||||
|
||||
// check the url params to select initial item on page load.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
|
||||
$selected_stock_status = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
$query = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
$selected_stock_statuses = explode( ',', $query );
|
||||
|
||||
$list_items = array_map(
|
||||
function( $item ) use ( $stock_statuses, $show_counts ) {
|
||||
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
|
||||
return array(
|
||||
'label' => $label,
|
||||
'value' => $item['status'],
|
||||
);
|
||||
},
|
||||
$stock_counts
|
||||
$filtered_stock_counts = array_filter(
|
||||
$stock_counts,
|
||||
function( $stock_count ) {
|
||||
return $stock_count['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $filtered_stock_counts ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$list_items = array_values(
|
||||
array_map(
|
||||
function( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) {
|
||||
$label = $show_counts ? $stock_statuses[ $item['status'] ] . ' (' . $item['count'] . ')' : $stock_statuses[ $item['status'] ];
|
||||
return array(
|
||||
'label' => $label,
|
||||
'value' => $item['status'],
|
||||
'checked' => in_array( $item['status'], $selected_stock_statuses, true ),
|
||||
);
|
||||
},
|
||||
$filtered_stock_counts
|
||||
)
|
||||
);
|
||||
|
||||
$selected_items = array_values(
|
||||
array_filter(
|
||||
$list_items,
|
||||
function( $item ) use ( $selected_stock_status ) {
|
||||
return $item['value'] === $selected_stock_status;
|
||||
function( $item ) use ( $selected_stock_statuses ) {
|
||||
return in_array( $item['value'], $selected_stock_statuses, true );
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Just for the dropdown, we can only select 1 item.
|
||||
$selected_item = $selected_items[0] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
$data_directive = wp_json_encode( array( 'namespace' => 'woocommerce/collection-stock-filter' ) );
|
||||
$data_directive = wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div data-wc-interactive='<?php echo esc_attr( $data_directive ); ?>'>
|
||||
<?php if ( 'list' === $display_style ) : ?>
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<?php foreach ( $stock_counts as $stock_count ) { ?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<label for="<?php echo esc_attr( $stock_count['status'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change="actions.updateProducts"
|
||||
value="<?php echo esc_attr( $stock_count['status'] ); ?>"
|
||||
<?php checked( strpos( $selected_stock_status, $stock_count['status'] ) !== false, 1 ); ?>
|
||||
>
|
||||
<svg class="wc-block-components-checkbox__mark" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 20">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"></path>
|
||||
</svg>
|
||||
<span class="wc-block-components-checkbox__label">
|
||||
<?php echo esc_html( $stock_statuses[ $stock_count['status'] ] ); ?>
|
||||
|
||||
<?php if ( $show_counts ) : ?>
|
||||
<?php
|
||||
// translators: %s: number of products.
|
||||
$screen_reader_text = sprintf( _n( '%s product', '%s products', $stock_count['count'], 'woocommerce' ), number_format_i18n( $stock_count['count'] ) );
|
||||
?>
|
||||
<span>
|
||||
<span aria-hidden="true">
|
||||
<?php $show_counts ? print( esc_html( '(' . $stock_count['count'] . ')' ) ) : null; ?>
|
||||
</span>
|
||||
<span class="screen-reader-text">
|
||||
<?php esc_html( $screen_reader_text ); ?>
|
||||
</span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'list' === $display_style ) { ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CheckboxList::render() escapes output.
|
||||
echo CheckboxList::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'on_change' => "{$this->get_full_block_name()}::actions.onCheckboxChange",
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ( 'dropdown' === $display_style ) : ?>
|
||||
<?php
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Dropdown::render() escapes output.
|
||||
echo Dropdown::render(
|
||||
array(
|
||||
'items' => $list_items,
|
||||
'action' => 'woocommerce/collection-stock-filter::actions.navigate',
|
||||
'selected_item' => $selected_item,
|
||||
'items' => $list_items,
|
||||
'action' => "{$this->get_full_block_name()}::actions.onDropdownChange",
|
||||
'selected_items' => $selected_items,
|
||||
'select_type' => $select_type,
|
||||
'placeholder' => $placeholder_text,
|
||||
)
|
||||
);
|
||||
?>
|
||||
@@ -87,9 +87,9 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
|
||||
return strtr(
|
||||
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
|
||||
<div class="wc-block-product-gallery-large-image__container">
|
||||
<ul class="wc-block-product-gallery-large-image__container">
|
||||
{main_images}
|
||||
</div>
|
||||
</ul>
|
||||
{content}
|
||||
</div>',
|
||||
array(
|
||||
@@ -142,7 +142,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
|
||||
$main_image_with_wrapper = array_map(
|
||||
function( $main_image_element ) {
|
||||
return "<div class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</div>';
|
||||
return "<li class='wc-block-product-gallery-large-image__wrapper'>" . $main_image_element . '</li>';
|
||||
},
|
||||
$main_images
|
||||
);
|
||||
|
||||
@@ -115,11 +115,13 @@ class ProductGalleryPager extends AbstractBlock {
|
||||
|
||||
$is_first_pager_item = 0 === $key;
|
||||
$pager_item = sprintf(
|
||||
'<li class="wc-block-product-gallery-pager__pager-item %2$s">%1$s</li>',
|
||||
'<li class="wc-block-product-gallery-pager__pager-item %2$s"><button aria-pressed="%3$s" data-wc-bind--aria-pressed="state.pagerButtonPressed">%1$s</button></li>',
|
||||
'dots' === $pager_display_mode ? $this->get_dot_icon( $is_first_pager_item ) : $key + 1,
|
||||
$is_first_pager_item ? 'wc-block-product-gallery-pager__pager-item--is-active' : ''
|
||||
$is_first_pager_item ? 'wc-block-product-gallery-pager__pager-item--is-active' : '',
|
||||
$is_first_pager_item ? 'true' : 'false'
|
||||
);
|
||||
$p = new \WP_HTML_Tag_Processor( $pager_item );
|
||||
|
||||
$p = new \WP_HTML_Tag_Processor( $pager_item );
|
||||
|
||||
if ( $p->next_tag() ) {
|
||||
$p->set_attribute(
|
||||
@@ -158,7 +160,7 @@ class ProductGalleryPager extends AbstractBlock {
|
||||
$initial_opacity = $is_active ? '1' : '0.2';
|
||||
return sprintf(
|
||||
'<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="6" cy="6" r="6" fill="black" fill-opacity="%1$s" data-wc-bind--fill-opacity="state.pagerDotFillOpacity" />
|
||||
<circle cx="6" cy="6" r="6" fill="black" fill-opacity="%1$s" data-wc-bind--fill-opacity="state.pagerDotFillOpacity" />
|
||||
</svg>',
|
||||
$initial_opacity
|
||||
);
|
||||
|
||||
@@ -240,6 +240,11 @@ final class BlockTypesController {
|
||||
'ProductCategory',
|
||||
'ProductCollection',
|
||||
'ProductCollectionNoResults',
|
||||
'ProductGallery',
|
||||
'ProductGalleryLargeImage',
|
||||
'ProductGalleryLargeImageNextPrevious',
|
||||
'ProductGalleryPager',
|
||||
'ProductGalleryThumbnails',
|
||||
'ProductImage',
|
||||
'ProductImageGallery',
|
||||
'ProductNew',
|
||||
@@ -281,6 +286,8 @@ final class BlockTypesController {
|
||||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
'OrderConfirmation\AdditionalFieldsWrapper',
|
||||
'OrderConfirmation\AdditionalFields',
|
||||
);
|
||||
|
||||
$block_types = array_merge(
|
||||
@@ -291,17 +298,12 @@ final class BlockTypesController {
|
||||
);
|
||||
|
||||
if ( Package::feature()->is_experimental_build() ) {
|
||||
$block_types[] = 'ProductGallery';
|
||||
$block_types[] = 'ProductGalleryLargeImage';
|
||||
$block_types[] = 'ProductGalleryLargeImageNextPrevious';
|
||||
$block_types[] = 'ProductGalleryPager';
|
||||
$block_types[] = 'ProductGalleryThumbnails';
|
||||
$block_types[] = 'CollectionFilters';
|
||||
$block_types[] = 'CollectionStockFilter';
|
||||
$block_types[] = 'CollectionPriceFilter';
|
||||
$block_types[] = 'CollectionAttributeFilter';
|
||||
$block_types[] = 'CollectionRatingFilter';
|
||||
$block_types[] = 'CollectionActiveFilters';
|
||||
$block_types[] = 'ProductFilter';
|
||||
$block_types[] = 'ProductFilterStockStatus';
|
||||
$block_types[] = 'ProductFilterPrice';
|
||||
$block_types[] = 'ProductFilterAttribute';
|
||||
$block_types[] = 'ProductFilterRating';
|
||||
$block_types[] = 'ProductFilterActive';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,6 +344,8 @@ final class BlockTypesController {
|
||||
'OrderConfirmation\BillingWrapper',
|
||||
'OrderConfirmation\ShippingWrapper',
|
||||
'OrderConfirmation\AdditionalInformation',
|
||||
'OrderConfirmation\AdditionalFieldsWrapper',
|
||||
'OrderConfirmation\AdditionalFields',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Automattic\WooCommerce\Blocks\AssetsController;
|
||||
use Automattic\WooCommerce\Blocks\BlockPatterns;
|
||||
use Automattic\WooCommerce\Blocks\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Blocks\BlockTypesController;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CreateAccount;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
|
||||
@@ -14,6 +15,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
|
||||
use Automattic\WooCommerce\Blocks\InboxNotifications;
|
||||
use Automattic\WooCommerce\Blocks\Installer;
|
||||
use Automattic\WooCommerce\Blocks\Migration;
|
||||
@@ -140,6 +142,7 @@ class Bootstrap {
|
||||
$this->container->get( Installer::class )->init();
|
||||
$this->container->get( GoogleAnalytics::class )->init();
|
||||
$this->container->get( CheckoutFields::class )->init();
|
||||
$this->container->get( CheckoutFieldsAdmin::class )->init();
|
||||
}
|
||||
|
||||
// Load assets unless this is a request specifically for the store API.
|
||||
@@ -160,6 +163,8 @@ class Bootstrap {
|
||||
$this->container->get( SingleProductTemplateCompatibility::class )->init();
|
||||
$this->container->get( Notices::class )->init();
|
||||
}
|
||||
|
||||
$this->container->get( QueryFilters::class )->init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +355,13 @@ class Bootstrap {
|
||||
return new CheckoutFields( $container->get( AssetDataRegistry::class ) );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsAdmin::class,
|
||||
function( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsAdmin( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PaymentsApi::class,
|
||||
function ( Container $container ) {
|
||||
@@ -413,6 +425,12 @@ class Bootstrap {
|
||||
return new TasksController();
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
QueryFilters::class,
|
||||
function() {
|
||||
return new QueryFilters();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,153 +242,262 @@ class CheckoutFields {
|
||||
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
*/
|
||||
public function register_checkout_field( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
wc_get_logger()->warning( 'A checkout field cannot be registered without an id.' );
|
||||
|
||||
// Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered.
|
||||
$result = $this->validate_options( $options );
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The above validate_options function ensures these options are valid. Type might not be supplied but then it defaults to text.
|
||||
$id = $options['id'];
|
||||
$location = $options['location'];
|
||||
$type = $options['type'] ?? 'text';
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? sprintf(
|
||||
/* translators: %s Field label. */
|
||||
__( '%s (optional)', 'woocommerce' ),
|
||||
$options['label']
|
||||
) : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
);
|
||||
|
||||
$field_data['attributes'] = $this->register_field_attributes( $id, $options['attributes'] ?? [] );
|
||||
|
||||
if ( 'checkbox' === $type ) {
|
||||
$result = $this->process_checkbox_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
if ( 'select' === $type ) {
|
||||
$result = $this->process_select_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the "base" options (id, label, location) and shows warnings if they're not supplied.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @return bool false if an error was encountered, true otherwise.
|
||||
*/
|
||||
private function validate_options( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Having fewer than 2 after exploding around a / means there is no namespace.
|
||||
if ( count( explode( '/', $options['id'] ) ) < 2 ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'A checkout field id must consist of namespace/name.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'A checkout field id must consist of namespace/name.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field label is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field label is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['location'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is required.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $options['id'] ), 'The field location is invalid.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is invalid.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = 'text';
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
esc_html( $options['id'] ),
|
||||
esc_html( $options['type'] ),
|
||||
implode( ', ', $this->supported_field_types )
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
$type = $options['type'];
|
||||
}
|
||||
|
||||
// At this point, the essentials fields and its location should be set.
|
||||
// At this point, the essentials fields and its location should be set and valid.
|
||||
$location = $options['location'];
|
||||
$id = $options['id'];
|
||||
|
||||
// Check to see if field is already in the array.
|
||||
if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'The field is already registered.' )
|
||||
);
|
||||
return;
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The field is already registered.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hidden fields are not supported right now. They will be registered with hidden => false.
|
||||
if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', esc_html( $id ) )
|
||||
$message = sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
// Don't return here unlike the other fields because this is not an issue that will prevent registration.
|
||||
}
|
||||
|
||||
if ( ! empty( $options['type'] ) ) {
|
||||
if ( ! in_array( $options['type'], $this->supported_field_types, true ) ) {
|
||||
$message = sprintf(
|
||||
'Unable to register field with id: "%s". Registering a field with type "%s" is not supported. The supported types are: %s.',
|
||||
$id,
|
||||
$options['type'],
|
||||
implode( ', ', $this->supported_field_types )
|
||||
);
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a select field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_select_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options".' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
$message = sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
$message = sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', $id, $sanitized_value );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? '' : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
'autocomplete' => empty( $options['autocomplete'] ) ? '' : $options['autocomplete'],
|
||||
'autocapitalize' => empty( $options['autocapitalize'] ) ? '' : $options['autocapitalize'],
|
||||
$field_data['options'] = $cleaned_options;
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a checkbox field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_checkbox_field( $options, $field_data ) {
|
||||
$id = $options['id'];
|
||||
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
$message = sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the attributes supplied during field registration.
|
||||
*
|
||||
* @param array $id The field ID.
|
||||
* @param array $attributes The attributes supplied during field registration.
|
||||
*
|
||||
* @return array The processed attributes.
|
||||
*/
|
||||
private function register_field_attributes( $id, $attributes ) {
|
||||
|
||||
// We check if attributes are valid. This is done to prevent too much nesting and also to allow field registration
|
||||
// even if the attributes property is invalid. We can just skip it and register the field without attributes.
|
||||
$has_attributes = false;
|
||||
|
||||
if ( empty( $attributes ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! is_array( $attributes ) || 0 === count( $attributes ) ) {
|
||||
$message = sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". %s', $id, 'Attributes must be a non-empty array.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return [];
|
||||
}
|
||||
|
||||
// These are formatted in camelCase because React components expect them that way.
|
||||
$allowed_attributes = array(
|
||||
'maxLength',
|
||||
'readOnly',
|
||||
'pattern',
|
||||
'autocomplete',
|
||||
'autocapitalize',
|
||||
'title',
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle Checkbox fields.
|
||||
*/
|
||||
if ( 'checkbox' === $type ) {
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
$field_data['required'] = false;
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
$valid_attributes = array_filter(
|
||||
$attributes,
|
||||
function( $_, $key ) use ( $allowed_attributes ) {
|
||||
return in_array( $key, $allowed_attributes, true ) || strpos( $key, 'aria-' ) === 0 || strpos( $key, 'data-' ) === 0;
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
// Any invalid attributes should show a doing_it_wrong warning. It shouldn't stop field registration, though.
|
||||
if ( count( $attributes ) !== count( $valid_attributes ) ) {
|
||||
$invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) );
|
||||
$message = sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Select fields.
|
||||
*/
|
||||
if ( 'select' === $type ) {
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options".' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', esc_html( $id ) )
|
||||
);
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
// Check all entries in $options['options'] has a key and value member.
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Unable to register field with id: "%s". %s', esc_html( $id ), 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$sanitized_value = sanitize_text_field( $option['value'] );
|
||||
$sanitized_label = sanitize_text_field( $option['label'] );
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
wc_get_logger()->warning(
|
||||
sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', esc_html( $id ), esc_html( $sanitized_value ) )
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$added_values[] = $sanitized_value;
|
||||
|
||||
$cleaned_options[] = array(
|
||||
'value' => $sanitized_value,
|
||||
'label' => $sanitized_label,
|
||||
);
|
||||
}
|
||||
|
||||
$field_data['options'] = $cleaned_options;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
// Escape attributes to remove any malicious code and return them.
|
||||
return array_map(
|
||||
function( $value ) {
|
||||
return esc_attr( $value );
|
||||
},
|
||||
$valid_attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,6 +518,91 @@ class CheckoutFields {
|
||||
return $this->additional_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the location of a field.
|
||||
*
|
||||
* @param string $field_key The key of the field to get the location for.
|
||||
* @return string The location of the field.
|
||||
*/
|
||||
public function get_field_location( $field_key ) {
|
||||
foreach ( $this->fields_locations as $location => $fields ) {
|
||||
if ( in_array( $field_key, $fields, true ) ) {
|
||||
return $location;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
|
||||
*
|
||||
* @param string $key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
public function validate_field( $key, $field_value, $request, $address_type = null ) {
|
||||
|
||||
$error = new \WP_Error();
|
||||
try {
|
||||
/**
|
||||
* Filter the result of validating an additional field.
|
||||
*
|
||||
* @param \WP_Error $error A WP_Error that extensions may add errors to.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$filtered_result = apply_filters( 'woocommerce_blocks_validate_additional_field_' . $key, $error, $field_value, $request, $address_type );
|
||||
|
||||
if ( $error !== $filtered_result ) {
|
||||
|
||||
// Different WP_Error was returned. This would remove errors from other filters. Skip filtering and allow the order to place without validating this field.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. One of the filters returned a new WP_Error. Filters should use the same WP_Error passed to the filter and use the WP_Error->add function to add errors. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them and validate the field. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. The field will not have any custom validation applied to it. %s',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $filtered_result ) ) {
|
||||
return $filtered_result;
|
||||
}
|
||||
|
||||
// If the filters didn't return a valid value, ignore them and return an empty WP_Error. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s did not return a valid value. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the default locale with additional fields without country limitations.
|
||||
*
|
||||
@@ -451,17 +645,43 @@ class CheckoutFields {
|
||||
return $this->fields_locations['additional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields definitions only meant for order.
|
||||
*
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_order_only_fields() {
|
||||
// For now, all contact fields are order only fields, along with additional fields.
|
||||
$order_fields_keys = array_merge( $this->get_contact_fields_keys(), $this->get_additional_fields_keys() );
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields for a given group.
|
||||
*
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
*
|
||||
* @return array An array of fields.
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_fields_for_location( $location ) {
|
||||
if ( in_array( $location, array_keys( $this->fields_locations ), true ) ) {
|
||||
return $this->fields_locations[ $location ];
|
||||
$order_fields_keys = $this->fields_locations[ $location ];
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,7 +689,7 @@ class CheckoutFields {
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param string $location The gslocation to validate the field for (address|contact|additional).
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
*
|
||||
* @return true|\WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
*/
|
||||
@@ -791,4 +1011,54 @@ class CheckoutFields {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional fields for an order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $context The context to get the field value for (edit|view).
|
||||
* @return array An array of fields definitions as well as their values formatted for display.
|
||||
*/
|
||||
public function get_order_additional_fields_with_values( $order, $location, $group = '', $context = 'edit' ) {
|
||||
$fields = $this->get_fields_for_location( $location );
|
||||
$fields_with_values = array();
|
||||
|
||||
foreach ( $fields as $field_key => $field ) {
|
||||
$value = $this->get_field_from_order( $field_key, $order, $group );
|
||||
|
||||
if ( '' === $value || null === $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'view' === $context ) {
|
||||
$value = $this->format_additional_field_value( $value, $field );
|
||||
}
|
||||
|
||||
$field['value'] = $value;
|
||||
$fields_with_values[ $field_key ] = $field;
|
||||
}
|
||||
|
||||
return $fields_with_values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a raw field value for display based on its type definition.
|
||||
*
|
||||
* @param string $value Value to format.
|
||||
* @param array $field Additional field definition.
|
||||
* @return string
|
||||
*/
|
||||
public function format_additional_field_value( $value, $field ) {
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$value = $value ? __( 'Yes', 'woocommerce' ) : __( 'No', 'woocommerce' );
|
||||
}
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$options = array_column( $field['options'], 'label', 'value' );
|
||||
$value = isset( $options[ $value ] ) ? $options[ $value ] : $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points in the admin area.
|
||||
*/
|
||||
class CheckoutFieldsAdmin {
|
||||
|
||||
/**
|
||||
* Checkout field controller.
|
||||
*
|
||||
* @var CheckoutFields
|
||||
*/
|
||||
private $checkout_fields_controller;
|
||||
|
||||
/**
|
||||
* Sets up core fields.
|
||||
*
|
||||
* @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller.
|
||||
*/
|
||||
public function __construct( CheckoutFields $checkout_fields_controller ) {
|
||||
$this->checkout_fields_controller = $checkout_fields_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks. This is not run Store API requests.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_billing_fields', array( $this, 'admin_contact_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_address_fields' ), 10, 3 );
|
||||
add_filter( 'woocommerce_admin_shipping_fields', array( $this, 'admin_additional_fields' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the shape of a checkout field to match whats needed in the WooCommerce meta boxes.
|
||||
*
|
||||
* @param array $field The field to format.
|
||||
* @param string $key The field key. This will be used for the ID of the field when passed to the meta box.
|
||||
* @return array Formatted field.
|
||||
*/
|
||||
protected function format_field_for_meta_box( $field, $key ) {
|
||||
$formatted_field = array(
|
||||
'id' => $key,
|
||||
'label' => $field['label'],
|
||||
'value' => $field['value'],
|
||||
'type' => $field['type'],
|
||||
'update_callback' => array( $this, 'update_callback' ),
|
||||
'show' => true,
|
||||
'wrapper_class' => 'form-field-wide',
|
||||
);
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$formatted_field['options'] = array_column( $field['options'], 'label', 'value' );
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$formatted_field['checked_value'] = '1';
|
||||
$formatted_field['unchecked_value'] = '0';
|
||||
}
|
||||
|
||||
return $formatted_field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a field value for an order.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Order $order The order to update the field for.
|
||||
*/
|
||||
public function update_callback( $key, $value, $order ) {
|
||||
$this->checkout_fields_controller->persist_field_for_order( $key, $value, $order, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects address fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_address_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$group = doing_action( 'woocommerce_admin_billing_fields' ) ? 'billing' : 'shipping';
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $group, $context );
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$group_key = '/' . $group . '/' . $key;
|
||||
$additional_fields[ $key ] = $this->format_field_for_meta_box( $field, $group_key );
|
||||
}
|
||||
|
||||
array_splice(
|
||||
$fields,
|
||||
array_search(
|
||||
'state',
|
||||
array_keys( $fields ),
|
||||
true
|
||||
) + 1,
|
||||
0,
|
||||
$additional_fields
|
||||
);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects contact fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_contact_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects additional fields in WC admin orders screen.
|
||||
*
|
||||
* @param array $fields The fields to show.
|
||||
* @param \WC_Order|boolean $order The order to show the fields for.
|
||||
* @param string $context The context to show the fields for.
|
||||
* @return array
|
||||
*/
|
||||
public function admin_additional_fields( $fields, $order = null, $context = 'edit' ) {
|
||||
if ( ! $order instanceof \WC_Order ) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$additional_fields = $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', $context );
|
||||
|
||||
return array_merge(
|
||||
$fields,
|
||||
array_map(
|
||||
array( $this, 'format_field_for_meta_box' ),
|
||||
$additional_fields,
|
||||
array_keys( $additional_fields )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,19 @@ class GoogleAnalytics {
|
||||
}
|
||||
|
||||
if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) {
|
||||
// Using an array with strategies as the final argument to wp_register_script was introduced in WP 6.3.
|
||||
// WC requires at least 6.3 at the point of adding this, so it's safe to leave in without version checks.
|
||||
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
|
||||
wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false );
|
||||
wp_register_script(
|
||||
'google-tag-manager',
|
||||
'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'],
|
||||
[],
|
||||
null,
|
||||
[
|
||||
'in_footer' => false,
|
||||
'strategy' => 'async',
|
||||
]
|
||||
);
|
||||
wp_add_inline_script(
|
||||
'google-tag-manager',
|
||||
"
|
||||
|
||||
@@ -41,8 +41,12 @@ class Notices {
|
||||
* Initialize notice hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
if ( wp_is_block_theme() ) {
|
||||
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
|
||||
add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 );
|
||||
add_action( 'wp_head', [ $this, 'enqueue_notice_styles' ] );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,6 +72,40 @@ class Notices {
|
||||
return array_merge( $allowed_tags, $svg_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces core notice templates with those from blocks.
|
||||
*
|
||||
* The new notice templates match block components with matching icons and styling. The differences are:
|
||||
* 1. Core has notices for info, success, and error notices, blocks has notices for info, success, error,
|
||||
* warning, and a default notice type.
|
||||
* 2. The block notices use different CSS classes to the core notices. Core uses `woocommerce-message`, `is-info`
|
||||
* and `is-error` classes, blocks uses `wc-block-components-notice-banner is-error`,
|
||||
* `wc-block-components-notice-banner is-info`, and `wc-block-components-notice-banner is-success`.
|
||||
* 3. The markup of the notices is different, with the block notices using SVG icons and a slightly different
|
||||
* structure to accommodate this.
|
||||
*
|
||||
* @param string $template Located template path.
|
||||
* @param string $template_name Template name.
|
||||
* @param array $args Template arguments.
|
||||
* @param string $template_path Template path.
|
||||
* @param string $default_path Default path.
|
||||
* @return string
|
||||
*/
|
||||
public function get_notices_template( $template, $template_name, $args, $template_path, $default_path ) {
|
||||
$directory = get_stylesheet_directory();
|
||||
$file = $directory . '/woocommerce/' . $template_name;
|
||||
if ( file_exists( $file ) ) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if ( in_array( $template_name, $this->notice_templates, true ) ) {
|
||||
$template = $this->package->get_path( 'templates/block-' . $template_name );
|
||||
wp_enqueue_style( 'wc-blocks-style' );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all notices with the new block based notices.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
namespace Automattic\WooCommerce\Blocks\Images;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||
use Automattic\WooCommerce\Blocks\Patterns\PatternUpdater;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\ContentProcessor;
|
||||
use Automattic\WooCommerce\Blocks\AIContent\UpdatePatterns;
|
||||
|
||||
/**
|
||||
* Pexels API client.
|
||||
@@ -27,6 +28,8 @@ class Pexels {
|
||||
* @return array|\WP_Error Array of images, or WP_Error if the request failed.
|
||||
*/
|
||||
public function get_images( $ai_connection, $token, $business_description ) {
|
||||
$business_description = ContentProcessor::summarize_business_description( $business_description, $ai_connection, $token );
|
||||
|
||||
if ( str_word_count( $business_description ) === 1 ) {
|
||||
$search_term = $business_description;
|
||||
} else {
|
||||
@@ -111,6 +114,7 @@ class Pexels {
|
||||
* @return mixed|\WP_Error
|
||||
*/
|
||||
private function define_search_term( $ai_connection, $token, $business_description ) {
|
||||
|
||||
$prompt = sprintf( 'You are a teacher. Based on the following business description, \'%s\', describe to a child exactly what this store is selling in one or two words and be as precise as you can possibly be. Do not reply with generic words that could cause confusion and be associated with other businesses as a response. Make sure you do not add double quotes in your response. Do not add any explanations in the response', $business_description );
|
||||
|
||||
$response = $ai_connection->fetch_ai_response( $token, $prompt, 30 );
|
||||
@@ -119,6 +123,10 @@ class Pexels {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ( isset( $response['code'] ) && 'completion_error' === $response['code'] ) {
|
||||
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $response['completion'] ) ) {
|
||||
return new \WP_Error( 'search_term_definition_failed', __( 'The search term definition failed.', 'woocommerce' ) );
|
||||
}
|
||||
@@ -222,7 +230,7 @@ class Pexels {
|
||||
* @return array|\WP_Error The total number of required images, or WP_Error if the request failed.
|
||||
*/
|
||||
private function total_number_required_images() {
|
||||
$patterns_dictionary = PatternUpdater::get_patterns_dictionary();
|
||||
$patterns_dictionary = UpdatePatterns::get_patterns_dictionary();
|
||||
|
||||
if ( is_wp_error( $patterns_dictionary ) ) {
|
||||
return $patterns_dictionary;
|
||||
|
||||
@@ -24,12 +24,9 @@ class CheckboxList {
|
||||
wp_enqueue_script( 'wc-interactivity-checkbox-list' );
|
||||
wp_enqueue_style( 'wc-interactivity-checkbox-list' );
|
||||
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$items = $props['items'] ?? array();
|
||||
$checkbox_list_context = array( 'items' => $items );
|
||||
|
||||
// Items should be an array of objects with a label (which can be plaintext or HTML) and value property.
|
||||
$items = $props['items'] ?? array();
|
||||
$on_change = $props['on_change'] ?? '';
|
||||
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-checkbox-list' ) );
|
||||
|
||||
@@ -50,7 +47,7 @@ class CheckboxList {
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $props['on_change'] ?? '' ); ?>"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
|
||||
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||
<?php checked( $item['checked'], 1 ); ?>
|
||||
>
|
||||
|
||||
@@ -18,69 +18,107 @@ class Dropdown {
|
||||
wp_enqueue_script( 'wc-interactivity-dropdown' );
|
||||
wp_enqueue_style( 'wc-interactivity-dropdown' );
|
||||
|
||||
$selected_item = $props['selected_item'] ?? array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
);
|
||||
|
||||
$dropdown_context = array(
|
||||
'selectedItem' => $selected_item,
|
||||
'hoveredItem' => array(
|
||||
'label' => null,
|
||||
'value' => null,
|
||||
),
|
||||
'isOpen' => false,
|
||||
);
|
||||
|
||||
wc_initial_state( 'woocommerce/interactivity-dropdown', array( 'selectedItem' => $selected_item ) );
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
$select_type = $props['select_type'] ?? 'single';
|
||||
$selected_items = $props['selected_items'] ?? array();
|
||||
|
||||
// Items should be an array of objects with a label and value property.
|
||||
$items = $props['items'] ?? array();
|
||||
|
||||
$default_placeholder = 'single' === $select_type ? __( 'Select an option', 'woocommerce' ) : __( 'Select options', 'woocommerce' );
|
||||
$placeholder = $props['placeholder'] ?? $default_placeholder;
|
||||
|
||||
$dropdown_context = array(
|
||||
'selectedItems' => $selected_items,
|
||||
'isOpen' => false,
|
||||
'selectType' => $select_type,
|
||||
'defaultPlaceholder' => $placeholder,
|
||||
);
|
||||
|
||||
$action = $props['action'] ?? '';
|
||||
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/interactivity-dropdown' ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
|
||||
<div class="wc-interactivity-dropdown" data-wc-context='<?php echo esc_attr( wp_json_encode( $dropdown_context ) ); ?>' >
|
||||
<div class="wc-blocks-components-form-token-field-wrapper single-selection" >
|
||||
<div class="components-form-token-field" tabindex="-1">
|
||||
<div class="components-form-token-field__input-container"
|
||||
data-wc-class--is-active="context.isOpen"
|
||||
tabindex="-1"
|
||||
data-wc-on--click="actions.toggleIsOpen"
|
||||
>
|
||||
<input id="components-form-token-input-1" type="text" autocomplete="off" data-wc-bind--placeholder="state.placeholderText" class="components-form-token-field__input" role="combobox" aria-expanded="false" aria-autocomplete="list" aria-describedby="components-form-token-suggestions-howto-1" value="">
|
||||
<ul hidden data-wc-bind--hidden="!context.isOpen" class="components-form-token-field__suggestions-list" id="components-form-token-suggestions-1" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array(
|
||||
'currentItem' => $item,
|
||||
);
|
||||
?>
|
||||
<li
|
||||
role="option"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
data-wc-context='<?php echo esc_attr( wp_json_encode( $context ) ); ?>'
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="state.isSelected"
|
||||
<div class="wc-interactivity-dropdown" data-wc-on--click="actions.toggleIsOpen" data-wc-context='<?php echo esc_attr( wp_json_encode( $dropdown_context ) ); ?>' >
|
||||
<div class="wc-interactivity-dropdown__dropdown" tabindex="-1" >
|
||||
<div class="wc-interactivity-dropdown__dropdown-selection" id="options-dropdown" tabindex="0" aria-haspopup="listbox">
|
||||
<span class="wc-interactivity-dropdown__placeholder" data-wc-text="state.placeholderText">
|
||||
<?php echo esc_html( $placeholder ); ?>
|
||||
</span>
|
||||
<?php if ( 'multiple' === $select_type ) { ?>
|
||||
<div class="selected-options">
|
||||
<template
|
||||
data-wc-each="context.selectedItems"
|
||||
data-wc-each-key="context.item.value"
|
||||
>
|
||||
<?php // This attribute supports HTML so should be sanitized by caller. ?>
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php echo $item['label']; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="wc-interactivity-dropdown__selected-badge">
|
||||
<span class="wc-interactivity-dropdown__badge-text" data-wc-text="context.item.label"></span>
|
||||
<svg
|
||||
data-wc-on--click="actions.unselectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
class="wc-interactivity-dropdown__badge-remove"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<?php foreach ( $selected_items as $selected ) { ?>
|
||||
<div
|
||||
class="wc-interactivity-dropdown__selected-badge"
|
||||
data-wc-key="<?php echo esc_attr( $selected['value'] ); ?>"
|
||||
data-wc-each-child
|
||||
>
|
||||
<span class="wc-interactivity-dropdown__badge-text"><?php echo esc_html( $selected['label'] ); ?></span>
|
||||
<svg
|
||||
data-wc-on--click="actions.unselectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
class="wc-interactivity-dropdown__badge-remove"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<span class="wc-interactivity-dropdown__svg-container">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div data-wc-bind--hidden="!context.isOpen" class="wc-interactivity-dropdown__dropdown-list" aria-labelledby="options-dropdown" role="listbox">
|
||||
<?php
|
||||
foreach ( $items as $item ) :
|
||||
$context = array( 'item' => $item );
|
||||
?>
|
||||
<div
|
||||
class="wc-interactivity-dropdown__dropdown-option"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
data-wc-on--click--select-item="actions.selectDropdownItem"
|
||||
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||
data-wc-class--is-selected="state.isSelected"
|
||||
class="components-form-token-field__suggestion"
|
||||
data-wc-bind--aria-selected="state.isSelected"
|
||||
data-wc-context='<?php echo wp_json_encode( $context ); ?>'
|
||||
>
|
||||
<?php echo $item['label']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="30" height="30" >
|
||||
<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z" ></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
516
wp/wp-content/plugins/woocommerce/src/Blocks/QueryFilters.php
Normal file
516
wp/wp-content/plugins/woocommerce/src/Blocks/QueryFilters.php
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks;
|
||||
|
||||
use WC_Tax;
|
||||
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
|
||||
|
||||
/**
|
||||
* Process the query data for filtering purposes.
|
||||
*/
|
||||
final class QueryFilters {
|
||||
/**
|
||||
* Initialization method.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function init() {
|
||||
add_filter( 'posts_clauses', array( $this, 'main_query_filter' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the posts clauses of the main query to suport global filters.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
public function main_query_filter( $args, $wp_query ) {
|
||||
if (
|
||||
! $wp_query->is_main_query() ||
|
||||
'product_query' !== $wp_query->get( 'wc_query' )
|
||||
) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
if ( $wp_query->get( 'filter_stock_status' ) ) {
|
||||
$args = $this->stock_filter_clauses( $args, $wp_query );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditional query clauses based on the filter params in query vars.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
public function add_query_clauses( $args, $wp_query ) {
|
||||
$args = $this->stock_filter_clauses( $args, $wp_query );
|
||||
$args = $this->price_filter_clauses( $args, $wp_query );
|
||||
$args = $this->attribute_filter_clauses( $args, $wp_query );
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price data for current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return object
|
||||
*/
|
||||
public function get_filtered_price( $query_vars ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$price_filter_sql = "
|
||||
SELECT min( min_price ) as min_price, MAX( max_price ) as max_price
|
||||
FROM {$wpdb->wc_product_meta_lookup}
|
||||
WHERE product_id IN ( {$product_query_sql} )
|
||||
";
|
||||
|
||||
return $wpdb->get_row( $price_filter_sql ); // phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock status counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array status=>count pairs.
|
||||
*/
|
||||
public function get_stock_status_counts( $query_vars ) {
|
||||
global $wpdb;
|
||||
$stock_status_options = array_map( 'esc_sql', array_keys( wc_get_product_stock_status_options() ) );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$stock_status_counts = array();
|
||||
|
||||
foreach ( $stock_status_options as $status ) {
|
||||
$stock_status_count_sql = $this->generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options );
|
||||
|
||||
$result = $wpdb->get_row( $stock_status_count_sql ); // phpcs:ignore
|
||||
$stock_status_counts[ $status ] = $result->status_count;
|
||||
}
|
||||
|
||||
return $stock_status_counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rating counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array rating=>count pairs.
|
||||
*/
|
||||
public function get_rating_counts( $query_vars ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$rating_count_sql = "
|
||||
SELECT COUNT( DISTINCT product_id ) as product_count, ROUND( average_rating, 0 ) as rounded_average_rating
|
||||
FROM {$wpdb->wc_product_meta_lookup}
|
||||
WHERE product_id IN ( {$product_query_sql} )
|
||||
AND average_rating > 0
|
||||
GROUP BY rounded_average_rating
|
||||
ORDER BY rounded_average_rating ASC
|
||||
";
|
||||
|
||||
$results = $wpdb->get_results( $rating_count_sql ); // phpcs:ignore
|
||||
|
||||
return array_map( 'absint', wp_list_pluck( $results, 'product_count', 'rounded_average_rating' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute counts for the current products.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @param string $attribute_to_count Attribute taxonomy name.
|
||||
* @return array termId=>count pairs.
|
||||
*/
|
||||
public function get_attribute_counts( $query_vars, $attribute_to_count ) {
|
||||
global $wpdb;
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10, 2 );
|
||||
add_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$query_vars['no_found_rows'] = true;
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
$attributes_to_count_sql = 'AND term_taxonomy.taxonomy IN ("' . esc_sql( wc_sanitize_taxonomy_name( $attribute_to_count ) ) . '")';
|
||||
$attribute_count_sql = "
|
||||
SELECT COUNT( DISTINCT posts.ID ) as term_count, terms.term_id as term_count_id
|
||||
FROM {$wpdb->posts} AS posts
|
||||
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON posts.ID = term_relationships.object_id
|
||||
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
|
||||
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
|
||||
WHERE posts.ID IN ( {$product_query_sql} )
|
||||
{$attributes_to_count_sql}
|
||||
GROUP BY terms.term_id
|
||||
";
|
||||
|
||||
$results = $wpdb->get_results( $attribute_count_sql ); // phpcs:ignore
|
||||
|
||||
return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for stock filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function stock_filter_clauses( $args, $wp_query ) {
|
||||
if ( ! $wp_query->get( 'filter_stock_status' ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
|
||||
$args['where'] .= ' AND wc_product_meta_lookup.stock_status IN ("' . implode( '","', array_map( 'esc_sql', explode( ',', $wp_query->get( 'filter_stock_status' ) ) ) ) . '")';
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for price filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function price_filter_clauses( $args, $wp_query ) {
|
||||
if ( ! $wp_query->get( 'min_price' ) && ! $wp_query->get( 'max_price' ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$adjust_for_taxes = $this->adjust_price_filters_for_displayed_taxes();
|
||||
$args['join'] = $this->append_product_sorting_table_join( $args['join'] );
|
||||
|
||||
if ( $wp_query->get( 'min_price' ) ) {
|
||||
$min_price_filter = intval( $wp_query->get( 'min_price' ) );
|
||||
|
||||
if ( $adjust_for_taxes ) {
|
||||
$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $min_price_filter, 'min_price', '>=' );
|
||||
} else {
|
||||
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.min_price >= %f ', $min_price_filter );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $wp_query->get( 'max_price' ) ) {
|
||||
$max_price_filter = intval( $wp_query->get( 'max_price' ) );
|
||||
|
||||
if ( $adjust_for_taxes ) {
|
||||
$args['where'] .= $this->get_price_filter_query_for_displayed_taxes( $max_price_filter, 'max_price', '<=' );
|
||||
} else {
|
||||
$args['where'] .= $wpdb->prepare( ' AND wc_product_meta_lookup.max_price <= %f ', $max_price_filter );
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join wc_product_meta_lookup to posts if not already joined.
|
||||
*
|
||||
* @param string $sql SQL join.
|
||||
* @return string
|
||||
*/
|
||||
private function append_product_sorting_table_join( $sql ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) {
|
||||
$sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id ";
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate calculate query by stock status.
|
||||
*
|
||||
* @param string $status status to calculate.
|
||||
* @param string $product_query_sql product query for current filter state.
|
||||
* @param array $stock_status_options available stock status options.
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
private function generate_stock_status_count_query( $status, $product_query_sql, $stock_status_options ) {
|
||||
if ( ! in_array( $status, $stock_status_options, true ) ) {
|
||||
return false;
|
||||
}
|
||||
global $wpdb;
|
||||
$status = esc_sql( $status );
|
||||
return "
|
||||
SELECT COUNT( DISTINCT posts.ID ) as status_count
|
||||
FROM {$wpdb->posts} as posts
|
||||
INNER JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id
|
||||
AND postmeta.meta_key = '_stock_status'
|
||||
AND postmeta.meta_value = '{$status}'
|
||||
WHERE posts.ID IN ( {$product_query_sql} )
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query for price filters when dealing with displayed taxes.
|
||||
*
|
||||
* @param float $price_filter Price filter to apply.
|
||||
* @param string $column Price being filtered (min or max).
|
||||
* @param string $operator Comparison operator for column.
|
||||
* @return string Constructed query.
|
||||
*/
|
||||
private function get_price_filter_query_for_displayed_taxes( $price_filter, $column = 'min_price', $operator = '>=' ) {
|
||||
global $wpdb;
|
||||
|
||||
// Select only used tax classes to avoid unwanted calculations.
|
||||
$product_tax_classes = $wpdb->get_col( "SELECT DISTINCT tax_class FROM {$wpdb->wc_product_meta_lookup};" );
|
||||
|
||||
if ( empty( $product_tax_classes ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$or_queries = array();
|
||||
|
||||
// We need to adjust the filter for each possible tax class and combine the queries into one.
|
||||
foreach ( $product_tax_classes as $tax_class ) {
|
||||
$adjusted_price_filter = $this->adjust_price_filter_for_tax_class( $price_filter, $tax_class );
|
||||
$or_queries[] = $wpdb->prepare(
|
||||
'( wc_product_meta_lookup.tax_class = %s AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )',
|
||||
$tax_class,
|
||||
$adjusted_price_filter
|
||||
);
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $wpdb->prepare(
|
||||
' AND (
|
||||
wc_product_meta_lookup.tax_status = "taxable" AND ( 0=1 OR ' . implode( ' OR ', $or_queries ) . ')
|
||||
OR ( wc_product_meta_lookup.tax_status != "taxable" AND wc_product_meta_lookup.`' . esc_sql( $column ) . '` ' . esc_sql( $operator ) . ' %f )
|
||||
) ',
|
||||
$price_filter
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* If price filters need adjustment to work with displayed taxes, this returns true.
|
||||
*
|
||||
* This logic is used when prices are stored in the database differently to how they are being displayed, with regards
|
||||
* to taxes.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function adjust_price_filters_for_displayed_taxes() {
|
||||
$display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$database = wc_prices_include_tax() ? 'incl' : 'excl';
|
||||
|
||||
return $display !== $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts a price filter based on a tax class and whether or not the amount includes or excludes taxes.
|
||||
*
|
||||
* This calculation logic is based on `wc_get_price_excluding_tax` and `wc_get_price_including_tax` in core.
|
||||
*
|
||||
* @param float $price_filter Price filter amount as entered.
|
||||
* @param string $tax_class Tax class for adjustment.
|
||||
* @return float
|
||||
*/
|
||||
private function adjust_price_filter_for_tax_class( $price_filter, $tax_class ) {
|
||||
$tax_display = get_option( 'woocommerce_tax_display_shop' );
|
||||
$tax_rates = WC_Tax::get_rates( $tax_class );
|
||||
$base_tax_rates = WC_Tax::get_base_tax_rates( $tax_class );
|
||||
|
||||
// If prices are shown incl. tax, we want to remove the taxes from the filter amount to match prices stored excl. tax.
|
||||
if ( 'incl' === $tax_display ) {
|
||||
/**
|
||||
* Filters if taxes should be removed from locations outside the store base location.
|
||||
*
|
||||
* The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing
|
||||
* with out of base locations. e.g. If a product costs 10 including tax, all users will pay 10
|
||||
* regardless of location and taxes.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*
|
||||
* @internal Matches filter name in WooCommerce core.
|
||||
*
|
||||
* @param boolean $adjust_non_base_location_prices True by default.
|
||||
* @return boolean
|
||||
*/
|
||||
$taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $price_filter, $base_tax_rates, true ) : WC_Tax::calc_tax( $price_filter, $tax_rates, true );
|
||||
return $price_filter - array_sum( $taxes );
|
||||
}
|
||||
|
||||
// If prices are shown excl. tax, add taxes to match the prices stored in the DB.
|
||||
$taxes = WC_Tax::calc_tax( $price_filter, $tax_rates, false );
|
||||
|
||||
return $price_filter + array_sum( $taxes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute lookup table name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_lookup_table_name() {
|
||||
return wc_get_container()->get( LookupDataStore::class )->get_lookup_table_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query clauses for attribute filter.
|
||||
*
|
||||
* @param array $args Query args.
|
||||
* @param \WP_Query $wp_query WP_Query object.
|
||||
* @return array
|
||||
*/
|
||||
private function attribute_filter_clauses( $args, $wp_query ) {
|
||||
$chosen_attributes = $this->get_chosen_attributes( $wp_query->query_vars );
|
||||
|
||||
if ( empty( $chosen_attributes ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// The extra derived table ("SELECT product_or_parent_id FROM") is needed for performance
|
||||
// (causes the filtering subquery to be executed only once).
|
||||
$clause_root = " {$wpdb->posts}.ID IN ( SELECT product_or_parent_id FROM (";
|
||||
if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
|
||||
$in_stock_clause = ' AND in_stock = 1';
|
||||
} else {
|
||||
$in_stock_clause = '';
|
||||
}
|
||||
|
||||
$attribute_ids_for_and_filtering = array();
|
||||
|
||||
foreach ( $chosen_attributes as $taxonomy => $data ) {
|
||||
$all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
|
||||
$term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' );
|
||||
$term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) );
|
||||
$term_ids_to_filter_by = array_map( 'absint', $term_ids_to_filter_by );
|
||||
$term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')';
|
||||
$is_and_query = 'and' === $data['query_type'];
|
||||
|
||||
$count = count( $term_ids_to_filter_by );
|
||||
|
||||
if ( 0 !== $count ) {
|
||||
if ( $is_and_query && $count > 1 ) {
|
||||
$attribute_ids_for_and_filtering = array_merge( $attribute_ids_for_and_filtering, $term_ids_to_filter_by );
|
||||
} else {
|
||||
$clauses[] = "
|
||||
{$clause_root}
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE term_id in {$term_ids_to_filter_by_list}
|
||||
{$in_stock_clause}
|
||||
)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $attribute_ids_for_and_filtering ) ) {
|
||||
$count = count( $attribute_ids_for_and_filtering );
|
||||
$term_ids_to_filter_by_list = '(' . join( ',', $attribute_ids_for_and_filtering ) . ')';
|
||||
$clauses[] = "
|
||||
{$clause_root}
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE is_variation_attribute=0
|
||||
{$in_stock_clause}
|
||||
AND term_id in {$term_ids_to_filter_by_list}
|
||||
GROUP BY product_id
|
||||
HAVING COUNT(product_id)={$count}
|
||||
UNION
|
||||
SELECT product_or_parent_id
|
||||
FROM {$this->get_lookup_table_name()} lt
|
||||
WHERE is_variation_attribute=1
|
||||
{$in_stock_clause}
|
||||
AND term_id in {$term_ids_to_filter_by_list}
|
||||
)";
|
||||
}
|
||||
|
||||
if ( ! empty( $clauses ) ) {
|
||||
// "temp" is needed because the extra derived tables require an alias.
|
||||
$args['where'] .= ' AND (' . join( ' temp ) AND ', $clauses ) . ' temp ))';
|
||||
} elseif ( ! empty( $attributes_to_filter_by ) ) {
|
||||
$args['where'] .= ' AND 1=0';
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of attributes and terms selected from query arguments.
|
||||
*
|
||||
* @param array $query_vars The WP_Query arguments.
|
||||
* @return array
|
||||
*/
|
||||
private function get_chosen_attributes( $query_vars ) {
|
||||
$chosen_attributes = array();
|
||||
|
||||
if ( empty( $query_vars ) ) {
|
||||
return $chosen_attributes;
|
||||
}
|
||||
|
||||
foreach ( $query_vars as $key => $value ) {
|
||||
if ( 0 === strpos( $key, 'filter_' ) ) {
|
||||
$attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) );
|
||||
$taxonomy = wc_attribute_taxonomy_name( $attribute );
|
||||
$filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array();
|
||||
|
||||
if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$query_type = ! empty( $query_vars[ 'query_type_' . $attribute ] ) && in_array( $query_vars[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $query_vars[ 'query_type_' . $attribute ] ) ) : '';
|
||||
$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
|
||||
$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : 'and';
|
||||
}
|
||||
}
|
||||
|
||||
return $chosen_attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Utils;
|
||||
|
||||
/**
|
||||
* BlockHooksTrait
|
||||
*
|
||||
* Shared functionality for using the Block Hooks API with WooCommerce Blocks.
|
||||
*/
|
||||
trait BlockHooksTrait {
|
||||
/**
|
||||
* Callback for `hooked_block_types` to auto-inject the mini-cart block into headers after navigation.
|
||||
*
|
||||
* @param array $hooked_blocks An array of block slugs hooked into a given context.
|
||||
* @param string $position Position of the block insertion point.
|
||||
* @param string $anchor_block The block acting as the anchor for the inserted block.
|
||||
* @param \WP_Block_Template|array $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return array An array of block slugs hooked into a given context.
|
||||
*/
|
||||
public function register_hooked_block( $hooked_blocks, $position, $anchor_block, $context ) {
|
||||
|
||||
/**
|
||||
* If the block has no hook placements, return early.
|
||||
*/
|
||||
if ( ! isset( $this->hooked_block_placements ) || empty( $this->hooked_block_placements ) ) {
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
// Cache for active theme.
|
||||
static $active_theme_name = null;
|
||||
if ( is_null( $active_theme_name ) ) {
|
||||
$active_theme_name = wp_get_theme()->get( 'Name' );
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of pattern slugs to exclude from auto-insert (useful when
|
||||
* there are patterns that have a very specific location for the block)
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$pattern_exclude_list = apply_filters( 'woocommerce_hooked_blocks_pattern_exclude_list', array( 'twentytwentytwo/header-centered-logo', 'twentytwentytwo/header-stacked' ) );
|
||||
|
||||
/**
|
||||
* A list of theme slugs to execute this with. This is a temporary
|
||||
* measure until improvements to the Block Hooks API allow for exposing
|
||||
* to all block themes.
|
||||
*
|
||||
* @since $VID:$
|
||||
*/
|
||||
$theme_include_list = apply_filters( 'woocommerce_hooked_blocks_theme_include_list', array( 'Twenty Twenty-Four', 'Twenty Twenty-Three', 'Twenty Twenty-Two', 'Tsubaki', 'Zaino', 'Thriving Artist', 'Amulet', 'Tazza' ) );
|
||||
|
||||
if ( $context && in_array( $active_theme_name, $theme_include_list, true ) ) {
|
||||
foreach ( $this->hooked_block_placements as $placement ) {
|
||||
if (
|
||||
$placement['position'] === $position &&
|
||||
$placement['anchor'] === $anchor_block &&
|
||||
(
|
||||
isset( $placement['area'] ) &&
|
||||
$this->is_template_part_or_pattern( $context, $placement['area'] )
|
||||
) &&
|
||||
! $this->pattern_is_excluded( $context, $pattern_exclude_list ) &&
|
||||
! $this->has_block_in_content( $context )
|
||||
) {
|
||||
$hooked_blocks[] = $this->namespace . '/' . $this->block_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $hooked_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided context contains a the block already.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function has_block_in_content( $context ) {
|
||||
$content = is_array( $context ) && isset( $context['content'] ) ? $context['content'] : '';
|
||||
$content = '' === $content && $context instanceof \WP_Block_Template ? $context->content : $content;
|
||||
return strpos( $content, 'wp:' . $this->namespace . '/' . $this->block_name ) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a provided context, returns whether the context refers to header content.
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param string $area The area to check against before inserting.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function is_template_part_or_pattern( $context, $area ) {
|
||||
$is_pattern = is_array( $context ) &&
|
||||
(
|
||||
( isset( $context['blockTypes'] ) && in_array( 'core/template-part/' . $area, $context['blockTypes'], true ) ) ||
|
||||
( isset( $context['categories'] ) && in_array( $area, $context['categories'], true ) )
|
||||
);
|
||||
$is_template_part = $context instanceof \WP_Block_Template && $area === $context->area;
|
||||
return ( $is_pattern || $is_template_part );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the pattern is excluded or not
|
||||
*
|
||||
* @param array|\WP_Block_Template $context Where the block is embedded.
|
||||
* @param array $pattern_exclude_list List of pattern slugs to exclude.
|
||||
* @since $VID:$
|
||||
* @return boolean
|
||||
*/
|
||||
protected function pattern_is_excluded( $context, $pattern_exclude_list = array() ) {
|
||||
$pattern_slug = is_array( $context ) && isset( $context['slug'] ) ? $context['slug'] : '';
|
||||
if ( ! $pattern_slug ) {
|
||||
/**
|
||||
* Woo patterns have a slug property in $context, but core/theme patterns dont.
|
||||
* In that case, we fallback to the name property, as they're the same.
|
||||
*/
|
||||
$pattern_slug = is_array( $context ) && isset( $context['name'] ) ? $context['name'] : '';
|
||||
}
|
||||
return in_array( $pattern_slug, $pattern_exclude_list, true );
|
||||
}
|
||||
}
|
||||
@@ -105,4 +105,31 @@ class CartCheckoutUtils {
|
||||
asort( $array_without_accents );
|
||||
return array_replace( $array_without_accents, $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves formatted shipping zones from WooCommerce.
|
||||
*
|
||||
* @return array An array of formatted shipping zones.
|
||||
*/
|
||||
public static function get_shipping_zones() {
|
||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||
$formatted_shipping_zones = array_reduce(
|
||||
$shipping_zones,
|
||||
function( $acc, $zone ) {
|
||||
$acc[] = [
|
||||
'id' => $zone['id'],
|
||||
'title' => $zone['zone_name'],
|
||||
'description' => $zone['formatted_zone_location'],
|
||||
];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$formatted_shipping_zones[] = [
|
||||
'id' => 0,
|
||||
'title' => __( 'International', 'woocommerce' ),
|
||||
'description' => __( 'Locations outside all other zones', 'woocommerce' ),
|
||||
];
|
||||
return $formatted_shipping_zones;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMigrationServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\EnginesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\FeaturesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LoggingServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\MarketingServiceProvider;
|
||||
@@ -28,7 +29,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\Proxie
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\RestockRefundedItemsAdjusterServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\UtilsClassesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BatchProcessingServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\BlockTemplatesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\LayoutTemplatesServiceProvider;
|
||||
|
||||
/**
|
||||
* PSR11 compliant dependency injection container for WooCommerce.
|
||||
@@ -75,8 +76,9 @@ final class Container {
|
||||
FeaturesServiceProvider::class,
|
||||
MarketingServiceProvider::class,
|
||||
MarketplaceServiceProvider::class,
|
||||
BlockTemplatesServiceProvider::class,
|
||||
LayoutTemplatesServiceProvider::class,
|
||||
LoggingServiceProvider::class,
|
||||
EnginesServiceProvider::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,8 @@ class CLIRunner {
|
||||
WP_CLI::add_command( 'wc cot enable', array( $this, 'enable' ) );
|
||||
WP_CLI::add_command( 'wc cot disable', array( $this, 'disable' ) );
|
||||
WP_CLI::add_command( 'wc hpos cleanup', array( $this, 'cleanup_post_data' ) );
|
||||
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
|
||||
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,11 +585,7 @@ class CLIRunner {
|
||||
* @return array Failed IDs with meta details.
|
||||
*/
|
||||
private function verify_meta_data( array $order_ids, array $failed_ids ) : array {
|
||||
$meta_keys_to_ignore = array(
|
||||
'_paid_date', // This has been deprecated and replaced by '_date_paid' in the CPT datastore.
|
||||
'_completed_date', // This has been deprecated and replaced by '_date_completed' in the CPT datastore.
|
||||
'_edit_lock',
|
||||
);
|
||||
$meta_keys_to_ignore = $this->synchronizer->get_ignored_order_props();
|
||||
|
||||
global $wpdb;
|
||||
if ( ! count( $order_ids ) ) {
|
||||
@@ -959,4 +957,110 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a summary of HPOS situation on this site.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function status( array $args = array(), array $assoc_args = array() ) {
|
||||
$legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
|
||||
|
||||
// translators: %s is either 'yes' or 'no'.
|
||||
WP_CLI::log( sprintf( __( 'HPOS enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->controller->custom_orders_table_usage_is_enabled() ) ) );
|
||||
|
||||
// translators: %s is either 'yes' or 'no'.
|
||||
WP_CLI::log( sprintf( __( 'Compatibility mode enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->synchronizer->data_sync_is_enabled() ) ) );
|
||||
|
||||
// translators: %d is an order count.
|
||||
WP_CLI::log( sprintf( __( 'Unsynced orders: %d', 'woocommerce' ), $this->synchronizer->get_current_orders_pending_sync_count() ) );
|
||||
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
/* translators: %d is an order count. */
|
||||
__( 'Orders subject to cleanup: %d', 'woocommerce' ),
|
||||
( $this->synchronizer->custom_orders_table_is_authoritative() && ! $this->synchronizer->data_sync_is_enabled() )
|
||||
? $legacy_handler->count_orders_for_cleanup()
|
||||
: 0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays differences for an order between the HPOS and post datastore.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* :The ID of the order.
|
||||
*
|
||||
* [--format=<format>]
|
||||
* : Render output in a particular format.
|
||||
* ---
|
||||
* default: table
|
||||
* options:
|
||||
* - table
|
||||
* - csv
|
||||
* - json
|
||||
* - yaml
|
||||
* ---
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Find differences between datastores for order 123.
|
||||
* $ wp wc hpos diff 123
|
||||
*
|
||||
* # Find differences for order 123 and display as CSV.
|
||||
* $ wp wc hpos diff 123 --format=csv
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function diff( array $args = array(), array $assoc_args = array() ) {
|
||||
$id = absint( $args[0] );
|
||||
|
||||
try {
|
||||
$diff = wc_get_container()->get( LegacyDataHandler::class )->get_diff_for_order( $id );
|
||||
} catch ( \Exception $e ) {
|
||||
// translators: %1$d is an order ID, %2$s is an error message.
|
||||
WP_CLI::error( sprintf( __( 'An error occurred while computing a diff for order %1$d: %2$s', 'woocommerce' ), $id, $e->getMessage() ) );
|
||||
}
|
||||
|
||||
if ( ! $diff ) {
|
||||
WP_CLI::success( __( 'No differences found.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the diff array.
|
||||
$diff = array_map(
|
||||
function( $key, $hpos_value, $cpt_value ) {
|
||||
// Format for dates.
|
||||
$hpos_value = is_a( $hpos_value, \WC_DateTime::class ) ? $hpos_value->format( DATE_ATOM ) : $hpos_value;
|
||||
$cpt_value = is_a( $cpt_value, \WC_DateTime::class ) ? $cpt_value->format( DATE_ATOM ) : $cpt_value;
|
||||
|
||||
return array(
|
||||
'property' => $key,
|
||||
'hpos' => $hpos_value,
|
||||
'post' => $cpt_value,
|
||||
);
|
||||
},
|
||||
array_keys( $diff ),
|
||||
array_column( $diff, 0 ),
|
||||
array_column( $diff, 1 ),
|
||||
);
|
||||
|
||||
WP_CLI::warning(
|
||||
// translators: %d is an order ID.
|
||||
sprintf( __( 'Differences found for order %d:', 'woocommerce' ), $id )
|
||||
);
|
||||
WP_CLI\Utils\format_items(
|
||||
$assoc_args['format'] ?? 'table',
|
||||
$diff,
|
||||
array( 'property', 'hpos', 'post' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ abstract class MetaToCustomTableMigrator extends TableMigrator {
|
||||
$this->clear_errors();
|
||||
$exception = null;
|
||||
|
||||
if ( count( $data['data'] ) === 0 ) {
|
||||
if ( ! isset( $data['data'] ) || ! is_array( $data['data'] ) || count( $data['data'] ) === 0 ) {
|
||||
return array(
|
||||
'errors' => $this->get_errors(),
|
||||
'exception' => null,
|
||||
|
||||
@@ -231,7 +231,7 @@ abstract class MetaToMetaTableMigrator extends TableMigrator {
|
||||
$meta_query = $this->build_meta_table_query( $entity_ids );
|
||||
|
||||
$meta_data_rows = $this->db_get_results( $meta_query );
|
||||
if ( empty( $meta_data_rows ) ) {
|
||||
if ( ! is_array( $meta_data_rows ) || empty( $meta_data_rows ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ WHERE destination.$destination_entity_id_column in ( $entity_ids_placeholder ) O
|
||||
$entity_id_column = $this->schema_config['source']['entity']['id_column'];
|
||||
$entity_meta_id_mapping_column = $this->schema_config['source']['entity']['source_id_column'];
|
||||
|
||||
if ( $this->schema_config['source']['excluded_keys'] ) {
|
||||
if ( isset( $this->schema_config['source']['excluded_keys'] ) && is_array( $this->schema_config['source']['excluded_keys'] ) ) {
|
||||
$key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) );
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_key_column is escaped for backticks, $key_placeholder is hardcoded.
|
||||
$exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] );
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template registry.
|
||||
*/
|
||||
final class BlockTemplateRegistry {
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
* @var BlockTemplateRegistry|null
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Templates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $templates = array();
|
||||
|
||||
/**
|
||||
* Get the instance of the class.
|
||||
*/
|
||||
public static function get_instance(): BlockTemplateRegistry {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single template.
|
||||
*
|
||||
* @param BlockTemplateInterface $template Template to register.
|
||||
*
|
||||
* @throws \ValueError If a template with the same ID already exists.
|
||||
*/
|
||||
public function register( BlockTemplateInterface $template ) {
|
||||
$id = $template->get_id();
|
||||
|
||||
if ( isset( $this->templates[ $id ] ) ) {
|
||||
throw new \ValueError( 'A template with the specified ID already exists in the registry.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a template is registered.
|
||||
*
|
||||
* @param BlockTemplateInterface $template Template that was registered.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_register', $template );
|
||||
|
||||
$this->templates[ $id ] = $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the registered templates.
|
||||
*/
|
||||
public function get_all_registered(): array {
|
||||
return $this->templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single registered template.
|
||||
*
|
||||
* @param string $id ID of the template.
|
||||
*/
|
||||
public function get_registered( $id ): BlockTemplateInterface {
|
||||
return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
/**
|
||||
* Block template controller.
|
||||
*/
|
||||
class BlockTemplatesController {
|
||||
|
||||
/**
|
||||
* Block template registry
|
||||
*
|
||||
* @var BlockTemplateRegistry
|
||||
*/
|
||||
private $block_template_registry;
|
||||
|
||||
/**
|
||||
* Block template transformer.
|
||||
*
|
||||
* @var TemplateTransformer
|
||||
*/
|
||||
private $template_transformer;
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*/
|
||||
public function init( $block_template_registry, $template_transformer ) {
|
||||
$this->block_template_registry = $block_template_registry;
|
||||
$this->template_transformer = $template_transformer;
|
||||
add_action( 'rest_api_init', array( $this, 'register_templates' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register templates in the blocks endpoint.
|
||||
*/
|
||||
public function register_templates() {
|
||||
$templates = $this->block_template_registry->get_all_registered();
|
||||
|
||||
foreach ( $templates as $template ) {
|
||||
add_filter( 'pre_get_block_templates', function( $query_result, $query, $template_type ) use( $template ) {
|
||||
if ( ! isset( $query['area'] ) || $query['area'] !== $template->get_area() ) {
|
||||
return $query_result;
|
||||
}
|
||||
|
||||
$wp_block_template = $this->template_transformer->transform( $template );
|
||||
$query_result[] = $wp_block_template;
|
||||
|
||||
return $query_result;
|
||||
}, 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Template transformer.
|
||||
*/
|
||||
class TemplateTransformer {
|
||||
|
||||
/**
|
||||
* Transform the WooCommerceBlockTemplate to a WP_Block_Template.
|
||||
*
|
||||
* @param object $block_template The product template.
|
||||
*/
|
||||
public function transform( BlockTemplateInterface $block_template ): \WP_Block_Template {
|
||||
$template = new \WP_Block_Template();
|
||||
$template->id = $block_template->get_id();
|
||||
$template->theme = 'woocommerce/woocommerce';
|
||||
$template->content = $block_template->get_formatted_template();
|
||||
$template->source = 'plugin';
|
||||
$template->slug = $block_template->get_id();
|
||||
$template->type = 'wp_template';
|
||||
$template->title = $block_template->get_title();
|
||||
$template->description = $block_template->get_description();
|
||||
$template->status = 'publish';
|
||||
$template->has_theme_file = true;
|
||||
$template->origin = 'plugin';
|
||||
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
|
||||
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
|
||||
$template->area = $block_template->get_area();
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -131,4 +131,19 @@ abstract class AbstractBlockTemplate implements BlockTemplateInterface {
|
||||
|
||||
return $inner_blocks_formatted_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template as JSON like array.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
public function to_json(): array {
|
||||
return array(
|
||||
'id' => $this->get_id(),
|
||||
'title' => $this->get_title(),
|
||||
'description' => $this->get_description(),
|
||||
'area' => $this->get_area(),
|
||||
'blockTemplates' => $this->get_formatted_template(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,25 +210,35 @@ class BlockTemplateLogger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events for a given template.
|
||||
* Get all template events for a given template as a JSON like array.
|
||||
*
|
||||
* @param string $template_id Template ID.
|
||||
*/
|
||||
public function get_formatted_template_events( string $template_id ): array {
|
||||
public function template_events_to_json( string $template_id ): array {
|
||||
if ( ! isset( $this->all_template_events[ $template_id ] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$template_events = $this->all_template_events[ $template_id ];
|
||||
$template = $this->templates[ $template_id ];
|
||||
|
||||
$formatted_template_events = array();
|
||||
return $this->to_json( $template_events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template events as a JSON like array.
|
||||
*
|
||||
* @param array $template_events Template events.
|
||||
*
|
||||
* @return array The JSON.
|
||||
*/
|
||||
private function to_json( array $template_events ): array {
|
||||
$json = array();
|
||||
|
||||
foreach ( $template_events as $template_event ) {
|
||||
$container = $template_event['container'];
|
||||
$block = $template_event['block'];
|
||||
|
||||
$formatted_template_events[] = array(
|
||||
$json[] = array(
|
||||
'level' => $template_event['level'],
|
||||
'event_type' => $template_event['event_type'],
|
||||
'message' => $template_event['message'],
|
||||
@@ -246,7 +256,7 @@ class BlockTemplateLogger {
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted_template_events;
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,7 +321,7 @@ class BlockTemplateLogger {
|
||||
* @param array $template_events Template events.
|
||||
*/
|
||||
private function generate_template_events_hash( array $template_events ): string {
|
||||
return md5( wp_json_encode( $template_events ) );
|
||||
return md5( wp_json_encode( $this->to_json( $template_events ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace Automattic\WooCommerce\Internal\Admin;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\Reviews;
|
||||
use Automattic\WooCommerce\Internal\Admin\ProductReviews\ReviewsCommentsOverrides;
|
||||
use Automattic\WooCommerce\Internal\Admin\Settings;
|
||||
@@ -73,7 +72,6 @@ class Loader {
|
||||
|
||||
wc_get_container()->get( Reviews::class );
|
||||
wc_get_container()->get( ReviewsCommentsOverrides::class );
|
||||
wc_get_container()->get( BlockTemplatesController::class );
|
||||
|
||||
add_filter( 'admin_body_class', array( __CLASS__, 'add_admin_body_classes' ) );
|
||||
add_filter( 'admin_title', array( __CLASS__, 'update_admin_title' ) );
|
||||
|
||||
@@ -86,7 +86,7 @@ class FileController {
|
||||
* Class FileController
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->log_directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
$this->log_directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController };
|
||||
use WC_Log_Handler;
|
||||
|
||||
/**
|
||||
@@ -18,11 +17,19 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of the Settings class.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* LogHandlerFileV2 class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->file_controller = wc_get_container()->get( FileController::class );
|
||||
$this->settings = wc_get_container()->get( Settings::class );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,20 +80,17 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
$time_string = static::format_time( $timestamp );
|
||||
$level_string = strtoupper( $level );
|
||||
|
||||
// Remove line breaks so the whole entry is on one line in the file.
|
||||
$formatted_message = str_replace( PHP_EOL, ' ', $message );
|
||||
|
||||
unset( $context['source'] );
|
||||
if ( ! empty( $context ) ) {
|
||||
if ( isset( $context['backtrace'] ) && true === filter_var( $context['backtrace'], FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$context['backtrace'] = static::get_backtrace();
|
||||
}
|
||||
|
||||
$formatted_context = wp_json_encode( $context );
|
||||
$formatted_message .= " CONTEXT: $formatted_context";
|
||||
$formatted_context = wp_json_encode( $context );
|
||||
$message .= " CONTEXT: $formatted_context";
|
||||
}
|
||||
|
||||
$entry = "$time_string $level_string $formatted_message";
|
||||
$entry = "$time_string $level_string $message";
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/abstracts/abstract-wc-log-handler.php */
|
||||
@@ -152,6 +156,63 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
return sanitize_title( $source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs from a specific source.
|
||||
*
|
||||
* @param string $source The source of the log entries.
|
||||
*
|
||||
* @return int The number of files that were deleted.
|
||||
*/
|
||||
public function clear( string $source ): int {
|
||||
$source = File::sanitize_source( $source );
|
||||
|
||||
$files = $this->file_controller->get_files(
|
||||
array(
|
||||
'source' => $source,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$file_ids = array_map(
|
||||
fn( $file ) => $file->get_file_id(),
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %1$s is a number of log files, %2$s is a slug-style name for a file.
|
||||
_n(
|
||||
'%1$s log file from source %2$s was deleted.',
|
||||
'%1$s log files from source %2$s were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $source )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
'backtrace' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logs older than a specified timestamp.
|
||||
*
|
||||
@@ -172,7 +233,7 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) ) {
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -181,12 +242,8 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
$files
|
||||
);
|
||||
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/** This filter is documented in includes/class-wc-logger.php. */
|
||||
$retention_days = absint( apply_filters( 'woocommerce_logger_days_to_retain_logs', 30 ) );
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
$deleted = $this->file_controller->delete_files( $file_ids );
|
||||
$retention_days = $this->settings->get_retention_period();
|
||||
|
||||
if ( $deleted > 0 ) {
|
||||
$this->handle(
|
||||
|
||||
@@ -4,7 +4,7 @@ declare( strict_types = 1 );
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\{ LogHandlerFileV2, Settings };
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\{ File, FileController, FileListTable, SearchListTable };
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Status;
|
||||
@@ -26,6 +26,13 @@ class PageController {
|
||||
*/
|
||||
private $file_controller;
|
||||
|
||||
/**
|
||||
* Instance of Settings.
|
||||
*
|
||||
* @var Settings
|
||||
*/
|
||||
private $settings;
|
||||
|
||||
/**
|
||||
* Instance of FileListTable or SearchListTable.
|
||||
*
|
||||
@@ -39,13 +46,16 @@ class PageController {
|
||||
* @internal
|
||||
*
|
||||
* @param FileController $file_controller Instance of FileController.
|
||||
* @param Settings $settings Instance of Settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public function init(
|
||||
FileController $file_controller
|
||||
FileController $file_controller,
|
||||
Settings $settings
|
||||
): void {
|
||||
$this->file_controller = $file_controller;
|
||||
$this->settings = $settings;
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
@@ -56,8 +66,61 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'setup_screen_options' ) );
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'handle_list_table_bulk_actions' ) );
|
||||
self::add_action( 'load-woocommerce_page_wc-status', array( $this, 'maybe_do_logs_tab_action' ), 2 );
|
||||
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'setup_screen_options' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'handle_list_table_bulk_actions' ) );
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'notices' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current tab on the Status page is Logs, and if so, fire an action.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_do_logs_tab_action(): void {
|
||||
$is_logs_tab = 'logs' === filter_input( INPUT_GET, 'tab' );
|
||||
|
||||
if ( $is_logs_tab ) {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
/**
|
||||
* Action fires when the Logs tab starts loading.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_load_tab', $params['view'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notices to display on Logs screens.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function notices() {
|
||||
if ( ! $this->settings->logging_is_enabled() ) {
|
||||
add_action(
|
||||
'admin_notices',
|
||||
function() {
|
||||
?>
|
||||
<div class="notice notice-warning">
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
// translators: %s is a URL to another admin screen.
|
||||
wp_kses_post( __( 'Logging is disabled. It can be enabled in <a href="%s">Logs Settings</a>.', 'woocommerce' ) ),
|
||||
esc_url( add_query_arg( 'view', 'settings', $this->get_logs_tab_url() ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,40 +138,83 @@ class PageController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the default log handler.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_handler(): string {
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) || ! class_exists( $handler ) ) {
|
||||
$handler = WC_Log_Handler_File::class;
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Logs" tab, depending on the current default log handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render(): void {
|
||||
$handler = $this->get_default_handler();
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
|
||||
$this->render_section_nav();
|
||||
|
||||
if ( 'settings' === $params['view'] ) {
|
||||
$this->settings->render_form();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
$this->render_filev2();
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
return;
|
||||
case WC_Log_Handler_DB::class:
|
||||
WC_Admin_Status::status_logs_db();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
case WC_Log_Handler_File::class:
|
||||
WC_Admin_Status::status_logs_file();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action fires only if there is not a built-in rendering method for the current default log handler.
|
||||
*
|
||||
* This is intended as a way for extensions to render log views for custom handlers.
|
||||
*
|
||||
* @param string $handler
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_render_page', $handler );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render navigation to switch between logs browsing and settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function render_section_nav(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$browse_url = $this->get_logs_tab_url();
|
||||
$settings_url = add_query_arg( 'view', 'settings', $this->get_logs_tab_url() );
|
||||
|
||||
?>
|
||||
<ul class="subsubsub">
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $browse_url ),
|
||||
'settings' !== $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Browse', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
|
|
||||
</li>
|
||||
<li>
|
||||
<?php
|
||||
printf(
|
||||
'<a href="%1$s"%2$s>%3$s</a>',
|
||||
esc_url( $settings_url ),
|
||||
'settings' === $params['view'] ? ' class="current"' : '',
|
||||
esc_html__( 'Settings', 'woocommerce' )
|
||||
);
|
||||
?>
|
||||
</li>
|
||||
</ul>
|
||||
<br class="clear">
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +400,17 @@ class PageController {
|
||||
?>
|
||||
<?php endwhile; ?>
|
||||
</section>
|
||||
<script>
|
||||
// Clear the line number hash and highlight with a click.
|
||||
document.documentElement.addEventListener( 'click', ( event ) => {
|
||||
if ( window.location.hash && ! event.target.classList.contains( 'line-anchor' ) ) {
|
||||
let scrollPos = document.documentElement.scrollTop;
|
||||
window.location.hash = '';
|
||||
document.documentElement.scrollTop = scrollPos;
|
||||
history.replaceState( null, '', window.location.pathname + window.location.search );
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -303,7 +420,7 @@ class PageController {
|
||||
* @return void
|
||||
*/
|
||||
private function render_search_results_view(): void {
|
||||
$params = $this->get_query_params( array( 'order', 'orderby', 'search', 'source', 'view' ) );
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
|
||||
$list_table->prepare_items();
|
||||
@@ -380,7 +497,7 @@ class PageController {
|
||||
'view' => array(
|
||||
'filter' => FILTER_VALIDATE_REGEXP,
|
||||
'options' => array(
|
||||
'regexp' => '/^(list_files|single_file|search_results)$/',
|
||||
'regexp' => '/^(list_files|single_file|search_results|settings)$/',
|
||||
'default' => $defaults['view'],
|
||||
),
|
||||
),
|
||||
@@ -423,17 +540,18 @@ class PageController {
|
||||
/**
|
||||
* Register screen options for the logging views.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setup_screen_options(): void {
|
||||
$params = $this->get_query_params( array( 'view' ) );
|
||||
$handler = $this->get_default_handler();
|
||||
private function setup_screen_options( string $view ): void {
|
||||
$handler = $this->settings->get_default_handler();
|
||||
$list_table = null;
|
||||
|
||||
switch ( $handler ) {
|
||||
case LogHandlerFileV2::class:
|
||||
if ( in_array( $params['view'], array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $params['view'] );
|
||||
if ( in_array( $view, array( 'list_files', 'search_results' ), true ) ) {
|
||||
$list_table = $this->get_list_table( $view );
|
||||
}
|
||||
break;
|
||||
case 'WC_Log_Handler_DB':
|
||||
@@ -458,22 +576,24 @@ class PageController {
|
||||
/**
|
||||
* Process bulk actions initiated from the log file list table.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_list_table_bulk_actions(): void {
|
||||
private function handle_list_table_bulk_actions( string $view ): void {
|
||||
// Bail if we're not using the file handler.
|
||||
if ( LogHandlerFileV2::class !== $this->get_default_handler() ) {
|
||||
if ( LogHandlerFileV2::class !== $this->settings->get_default_handler() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = $this->get_query_params( array( 'file_id', 'view' ) );
|
||||
$params = $this->get_query_params( array( 'file_id' ) );
|
||||
|
||||
// Bail if this is not the list table view.
|
||||
if ( 'list_files' !== $params['view'] ) {
|
||||
if ( 'list_files' !== $view ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $this->get_list_table( $params['view'] )->current_action();
|
||||
$action = $this->get_list_table( $view )->current_action();
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : $this->get_logs_tab_url();
|
||||
@@ -562,10 +682,9 @@ class PageController {
|
||||
* @return string
|
||||
*/
|
||||
private function format_line( string $line, int $line_number ): string {
|
||||
$severity_levels = WC_Log_Levels::get_all_severity_levels();
|
||||
$classes = array( 'line' );
|
||||
$classes = array( 'line' );
|
||||
|
||||
$line = esc_html( trim( $line ) );
|
||||
$line = esc_html( $line );
|
||||
if ( empty( $line ) ) {
|
||||
$line = ' ';
|
||||
}
|
||||
@@ -583,11 +702,11 @@ class PageController {
|
||||
$has_timestamp = true;
|
||||
}
|
||||
|
||||
if ( isset( $segments[1] ) && in_array( strtolower( $segments[1] ), $severity_levels, true ) ) {
|
||||
if ( isset( $segments[1] ) && WC_Log_Levels::is_valid_level( strtolower( $segments[1] ) ) ) {
|
||||
$segments[1] = sprintf(
|
||||
'<span class="%1$s">%2$s</span>',
|
||||
esc_attr( 'log-level log-level--' . strtolower( $segments[1] ) ),
|
||||
esc_html( $segments[1] )
|
||||
esc_html( WC_Log_Levels::get_level_label( strtolower( $segments[1] ) ) )
|
||||
);
|
||||
$has_level = true;
|
||||
}
|
||||
@@ -600,7 +719,7 @@ class PageController {
|
||||
$context = json_decode( $maybe_json, false, 512, JSON_THROW_ON_ERROR );
|
||||
|
||||
$message_chunks[1] = sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
'<details><summary>%1$s</summary>%2$s</details>',
|
||||
esc_html__( 'Additional context', 'woocommerce' ),
|
||||
wp_json_encode( $context, JSON_PRETTY_PRINT )
|
||||
);
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Settings;
|
||||
use WC_Log_Handler, WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
|
||||
|
||||
/**
|
||||
* Settings class.
|
||||
*/
|
||||
class Settings {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Default values for logging settings.
|
||||
*
|
||||
* @const array
|
||||
*/
|
||||
private const DEFAULTS = array(
|
||||
'logging_enabled' => true,
|
||||
'default_handler' => LogHandlerFileV2::class,
|
||||
'retention_period_days' => 30,
|
||||
'level_threshold' => 'none',
|
||||
'file_entry_collapse_lines' => true,
|
||||
);
|
||||
|
||||
/**
|
||||
* The prefix for settings keys used in the options table.
|
||||
*
|
||||
* @const string
|
||||
*/
|
||||
private const PREFIX = 'woocommerce_logs_';
|
||||
|
||||
/**
|
||||
* Class Settings.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_action( 'wc_logs_load_tab', array( $this, 'save_settings' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render and save settings controls.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_settings_definitions(): array {
|
||||
$settings = array(
|
||||
'start' => array(
|
||||
'title' => __( 'Logs settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'logging_enabled' => array(
|
||||
'title' => __( 'Logger', 'woocommerce' ),
|
||||
'desc' => __( 'Enable logging', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'logging_enabled',
|
||||
'type' => 'checkbox',
|
||||
'value' => $this->logging_is_enabled() ? 'yes' : 'no',
|
||||
'default' => self::DEFAULTS['logging_enabled'] ? 'yes' : 'no',
|
||||
'autoload' => false,
|
||||
),
|
||||
'default_handler' => array(),
|
||||
'retention_period_days' => array(),
|
||||
'level_threshold' => array(),
|
||||
'end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
|
||||
if ( true === $this->logging_is_enabled() ) {
|
||||
$settings['default_handler'] = $this->get_default_handler_setting_definition();
|
||||
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
|
||||
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
|
||||
}
|
||||
|
||||
$default_handler = $this->get_default_handler();
|
||||
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
|
||||
$settings += $this->get_filesystem_settings_definitions();
|
||||
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
|
||||
$settings += $this->get_database_settings_definitions();
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the default_handler setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_default_handler_setting_definition(): array {
|
||||
$handler_options = array(
|
||||
LogHandlerFileV2::class => __( 'File system (default)', 'woocommerce' ),
|
||||
WC_Log_Handler_DB::class => __( 'Database (not recommended on live sites)', 'woocommerce' ),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the list of logging handlers that can be set as the default handler.
|
||||
*
|
||||
* @param array $handler_options An associative array of class_name => description.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$handler_options = apply_filters( 'woocommerce_logger_handler_options', $handler_options );
|
||||
|
||||
$current_value = $this->get_default_handler();
|
||||
if ( ! array_key_exists( $current_value, $handler_options ) ) {
|
||||
$handler_options[ $current_value ] = $current_value;
|
||||
}
|
||||
|
||||
$desc = array();
|
||||
|
||||
$desc[] = __( 'Note that if this setting is changed, any log entries that have already been recorded will remain stored in their current location, but will not migrate.', 'woocommerce' );
|
||||
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_HANDLER' ) );
|
||||
if ( $hardcoded ) {
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a code variable.
|
||||
__( 'This setting cannot be changed here because it is defined in the %s constant.', 'woocommerce' ),
|
||||
'<code>WC_LOG_HANDLER</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Log storage', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This determines where log entries are saved.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'default_handler',
|
||||
'type' => 'radio',
|
||||
'value' => $current_value,
|
||||
'default' => self::DEFAULTS['default_handler'],
|
||||
'autoload' => false,
|
||||
'options' => $handler_options,
|
||||
'disabled' => $hardcoded ? array_keys( $handler_options ) : array(),
|
||||
'desc' => implode( '<br><br>', $desc ),
|
||||
'desc_at_end' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the retention_period_days setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_retention_period_days_setting_definition(): array {
|
||||
$custom_attributes = array(
|
||||
'min' => 1,
|
||||
'step' => 1,
|
||||
);
|
||||
|
||||
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
|
||||
$desc = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_days_to_retain_logs</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Retention period', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'retention_period_days',
|
||||
'type' => 'number',
|
||||
'value' => $this->get_retention_period(),
|
||||
'default' => self::DEFAULTS['retention_period_days'],
|
||||
'autoload' => false,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:70px;',
|
||||
'row_class' => 'logs-retention-period-days',
|
||||
'suffix' => sprintf(
|
||||
' %s',
|
||||
__( 'days', 'woocommerce' ),
|
||||
),
|
||||
'desc' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for the level_threshold setting.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_level_threshold_setting_definition(): array {
|
||||
$hardcoded = ! is_null( Constants::get_constant( 'WC_LOG_THRESHOLD' ) );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$desc = sprintf(
|
||||
// translators: %1$s is the name of a code variable. %2$s is the name of a file.
|
||||
__( 'This setting cannot be changed here because it is defined in the %1$s constant, probably in your %2$s file.', 'woocommerce' ),
|
||||
'<code>WC_LOG_THRESHOLD</code>',
|
||||
'<b>wp-config.php</b>'
|
||||
);
|
||||
}
|
||||
|
||||
$labels = WC_Log_Levels::get_all_level_labels();
|
||||
$labels['none'] = __( 'None', 'woocommerce' );
|
||||
|
||||
$custom_attributes = array();
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Level threshold', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets the minimum severity level of logs that will be stored. Lower severity levels will be ignored. "None" means all logs will be stored.', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'level_threshold',
|
||||
'type' => 'select',
|
||||
'value' => $this->get_level_threshold(),
|
||||
'default' => self::DEFAULTS['level_threshold'],
|
||||
'autoload' => false,
|
||||
'options' => $labels,
|
||||
'custom_attributes' => $custom_attributes,
|
||||
'css' => 'width:auto;',
|
||||
'desc' => $desc,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to filesystem log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_filesystem_settings_definitions(): array {
|
||||
$location_info = array();
|
||||
$directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log files are stored in this directory: %s', 'woocommerce' ),
|
||||
sprintf(
|
||||
'<code>%s</code>',
|
||||
esc_html( $directory )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! wp_is_writable( $directory ) ) {
|
||||
$location_info[] = __( '⚠️ This directory does not appear to be writable.', 'woocommerce' );
|
||||
}
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %1$s is a code variable. %2$s is the name of a file.
|
||||
__( 'Change the location by defining the %1$s constant in your %2$s file with a new path.', 'woocommerce' ),
|
||||
'<code>WC_LOG_DIR</code>',
|
||||
'<code>wp-config.php</code>'
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'File system settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'log_directory' => array(
|
||||
'type' => 'info',
|
||||
'text' => implode( "\n\n", $location_info ),
|
||||
),
|
||||
'entry_format' => array(),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The definitions used by WC_Admin_Settings to render settings related to database log handlers.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_database_settings_definitions(): array {
|
||||
global $wpdb;
|
||||
$table = "{$wpdb->prefix}woocommerce_log";
|
||||
|
||||
$location_info = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
__( 'Log entries are stored in this database table: %s', 'woocommerce' ),
|
||||
"<code>$table</code>"
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'Database settings', 'woocommerce' ),
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'title',
|
||||
),
|
||||
'database_table' => array(
|
||||
'type' => 'info',
|
||||
'text' => $location_info,
|
||||
),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
'type' => 'sectionend',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission of the settings form and update the settings values.
|
||||
*
|
||||
* @param string $view The current view within the Logs tab.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function save_settings( string $view ): void {
|
||||
$is_saving = 'settings' === $view && isset( $_POST['save_settings'] );
|
||||
|
||||
if ( $is_saving ) {
|
||||
check_admin_referer( self::PREFIX . 'settings' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to manage logging settings.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
WC_Admin_Settings::save_fields( $settings );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_form(): void {
|
||||
$settings = $this->get_settings_definitions();
|
||||
|
||||
?>
|
||||
<form id="mainform" class="wc-logs-settings" method="post">
|
||||
<?php WC_Admin_Settings::output_fields( $settings ); ?>
|
||||
<?php
|
||||
/**
|
||||
* Action fires after the built-in logging settings controls have been rendered.
|
||||
*
|
||||
* This is intended as a way to allow other logging settings controls to be added by extensions.
|
||||
*
|
||||
* @param bool $enabled True if logging is currently enabled.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
do_action( 'wc_logs_settings_form_fields', $this->logging_is_enabled() );
|
||||
?>
|
||||
<?php wp_nonce_field( self::PREFIX . 'settings' ); ?>
|
||||
<?php submit_button( __( 'Save changes', 'woocommerce' ), 'primary', 'save_settings' ); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the logging_enabled setting.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function logging_is_enabled(): bool {
|
||||
$key = self::PREFIX . 'logging_enabled';
|
||||
|
||||
$enabled = WC_Admin_Settings::get_option( $key, self::DEFAULTS['logging_enabled'] );
|
||||
$enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
|
||||
|
||||
if ( is_null( $enabled ) ) {
|
||||
$enabled = self::DEFAULTS['logging_enabled'];
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the default_handler setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_default_handler(): string {
|
||||
$key = self::PREFIX . 'default_handler';
|
||||
|
||||
$handler = Constants::get_constant( 'WC_LOG_HANDLER' );
|
||||
|
||||
if ( is_null( $handler ) ) {
|
||||
$handler = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! class_exists( $handler ) || ! is_a( $handler, 'WC_Log_Handler_Interface', true ) ) {
|
||||
$handler = self::DEFAULTS['default_handler'];
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the retention_period_days setting.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_retention_period(): int {
|
||||
$key = self::PREFIX . 'retention_period_days';
|
||||
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
|
||||
if ( has_filter( 'woocommerce_logger_days_to_retain_logs' ) ) {
|
||||
/**
|
||||
* Filter the retention period of log entries.
|
||||
*
|
||||
* @param int $days The number of days to retain log entries.
|
||||
*
|
||||
* @since 3.4.0
|
||||
*/
|
||||
$retention_period = apply_filters( 'woocommerce_logger_days_to_retain_logs', $retention_period );
|
||||
} else {
|
||||
$retention_period = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
$retention_period = absint( $retention_period );
|
||||
|
||||
if ( $retention_period < 1 ) {
|
||||
$retention_period = self::DEFAULTS['retention_period_days'];
|
||||
}
|
||||
|
||||
return $retention_period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the current value of the level_threshold setting.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_level_threshold(): string {
|
||||
$key = self::PREFIX . 'level_threshold';
|
||||
|
||||
$threshold = Constants::get_constant( 'WC_LOG_THRESHOLD' );
|
||||
|
||||
if ( is_null( $threshold ) ) {
|
||||
$threshold = WC_Admin_Settings::get_option( $key );
|
||||
}
|
||||
|
||||
if ( ! WC_Log_Levels::is_valid_level( $threshold ) ) {
|
||||
$threshold = self::DEFAULTS['level_threshold'];
|
||||
}
|
||||
|
||||
return $threshold;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,20 @@ class Marketing {
|
||||
|
||||
use CouponsMovedTrait;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu name value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_NAME_KEY = 0;
|
||||
|
||||
/**
|
||||
* Constant representing the key for the submenu location value in the global $submenu array.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SUBMENU_LOCATION_KEY = 2;
|
||||
|
||||
/**
|
||||
* Class instance.
|
||||
*
|
||||
@@ -44,6 +58,9 @@ class Marketing {
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 5 );
|
||||
add_action( 'admin_menu', array( $this, 'add_parent_menu_item' ), 6 );
|
||||
|
||||
// Overwrite submenu default ordering for marketing menu. High priority gives plugins the chance to register their own menu items.
|
||||
add_action( 'admin_menu', array( $this, 'reorder_marketing_submenu' ), 99 );
|
||||
|
||||
add_filter( 'woocommerce_admin_shared_settings', array( $this, 'component_settings' ), 30 );
|
||||
}
|
||||
|
||||
@@ -140,6 +157,67 @@ class Marketing {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order marketing menu items alphabeticaly.
|
||||
* Overview should be first, and Coupons should be second, followed by other marketing menu items.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reorder_marketing_submenu() {
|
||||
global $submenu;
|
||||
|
||||
if ( ! isset( $submenu['woocommerce-marketing'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$marketing_submenu = $submenu['woocommerce-marketing'];
|
||||
$new_menu_order = array();
|
||||
|
||||
// Overview should be first.
|
||||
$overview_key = array_search( 'Overview', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $overview_key ) {
|
||||
/*
|
||||
* If Overview is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the overview page by its path.
|
||||
*/
|
||||
$overview_key = array_search( 'admin.php?page=wc-admin&path=/marketing', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $overview_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $overview_key ];
|
||||
array_splice( $marketing_submenu, $overview_key, 1 );
|
||||
}
|
||||
|
||||
// Coupons should be second.
|
||||
$coupons_key = array_search( 'Coupons', array_column( $marketing_submenu, self::SUBMENU_NAME_KEY ), true );
|
||||
|
||||
if ( false === $coupons_key ) {
|
||||
/*
|
||||
* If Coupons is not found we may be on a site witha different language.
|
||||
* We can use a fallback and try to find the coupons page by its path.
|
||||
*/
|
||||
$coupons_key = array_search( 'edit.php?post_type=shop_coupon', array_column( $marketing_submenu, self::SUBMENU_LOCATION_KEY ), true );
|
||||
}
|
||||
|
||||
if ( false !== $coupons_key ) {
|
||||
$new_menu_order[] = $marketing_submenu[ $coupons_key ];
|
||||
array_splice( $marketing_submenu, $coupons_key, 1 );
|
||||
}
|
||||
|
||||
// Sort the rest of the items alphabetically.
|
||||
usort(
|
||||
$marketing_submenu,
|
||||
function( $a, $b ) {
|
||||
return strcmp( $a[0], $b[0] );
|
||||
}
|
||||
);
|
||||
|
||||
$new_menu_order = array_merge( $new_menu_order, $marketing_submenu );
|
||||
|
||||
$submenu['woocommerce-marketing'] = $new_menu_order; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings for marketing feature.
|
||||
*
|
||||
|
||||
@@ -14,13 +14,6 @@ namespace Automattic\WooCommerce\Internal\Admin\Marketing;
|
||||
* @since x.x.x
|
||||
*/
|
||||
class MarketingSpecs {
|
||||
/**
|
||||
* Name of recommended plugins transient.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins';
|
||||
|
||||
/**
|
||||
* Name of knowledge base post transient.
|
||||
*
|
||||
@@ -28,111 +21,6 @@ class MarketingSpecs {
|
||||
*/
|
||||
const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base';
|
||||
|
||||
/**
|
||||
* Slug of the category specifying marketing extensions on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_EXTENSION_CATEGORY_SLUG = 'marketing';
|
||||
|
||||
/**
|
||||
* Slug of the subcategory specifying marketing channels on the Woo.com store.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MARKETING_CHANNEL_SUBCATEGORY_SLUG = 'sales-channels';
|
||||
|
||||
/**
|
||||
* Load recommended plugins from Woo.com
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_plugins(): array {
|
||||
$plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT );
|
||||
|
||||
if ( false === $plugins ) {
|
||||
$request = wp_remote_get(
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-tab/1.3/recommendations.json',
|
||||
array(
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$plugins = [];
|
||||
|
||||
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
|
||||
$plugins = json_decode( $request['body'], true );
|
||||
}
|
||||
|
||||
set_transient(
|
||||
self::RECOMMENDED_PLUGINS_TRANSIENT,
|
||||
$plugins,
|
||||
// Expire transient in 15 minutes if remote get failed.
|
||||
// Cache an empty result to avoid repeated failed requests.
|
||||
empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
return array_values( $plugins );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the recommended marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_marketing_channels(): array {
|
||||
return array_filter( $this->get_recommended_plugins(), [ $this, 'is_marketing_channel_plugin' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all recommended marketing extensions EXCEPT the marketing channels from Woo.com.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_marketing_extensions_excluding_channels(): array {
|
||||
return array_filter(
|
||||
$this->get_recommended_plugins(),
|
||||
function ( array $plugin_data ) {
|
||||
return $this->is_marketing_plugin( $plugin_data ) && ! $this->is_marketing_channel_plugin( $plugin_data );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing extension.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_marketing_plugin( array $plugin_data ): bool {
|
||||
$categories = $plugin_data['categories'] ?? [];
|
||||
|
||||
return in_array( self::MARKETING_EXTENSION_CATEGORY_SLUG, $categories, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a plugin is a marketing channel.
|
||||
*
|
||||
* @param array $plugin_data The plugin properties returned by the API.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_marketing_channel_plugin( array $plugin_data ): bool {
|
||||
if ( ! $this->is_marketing_plugin( $plugin_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subcategories = $plugin_data['subcategories'] ?? [];
|
||||
foreach ( $subcategories as $subcategory ) {
|
||||
if ( isset( $subcategory['slug'] ) && self::MARKETING_CHANNEL_SUBCATEGORY_SLUG === $subcategory['slug'] ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load knowledge base posts from Woo.com
|
||||
*
|
||||
@@ -165,21 +53,21 @@ class MarketingSpecs {
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
);
|
||||
$posts = [];
|
||||
$posts = array();
|
||||
|
||||
if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) {
|
||||
$raw_posts = json_decode( $request['body'], true );
|
||||
|
||||
foreach ( $raw_posts as $raw_post ) {
|
||||
$post = [
|
||||
$post = array(
|
||||
'title' => html_entity_decode( $raw_post['title']['rendered'] ),
|
||||
'date' => $raw_post['date_gmt'],
|
||||
'link' => $raw_post['link'],
|
||||
'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '',
|
||||
'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '',
|
||||
];
|
||||
);
|
||||
|
||||
$featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? [];
|
||||
$featured_media = isset( $raw_post['_embedded']['wp:featuredmedia'] ) && is_array( $raw_post['_embedded']['wp:featuredmedia'] ) ? $raw_post['_embedded']['wp:featuredmedia'] : array();
|
||||
if ( count( $featured_media ) > 0 ) {
|
||||
$image = current( $featured_media );
|
||||
$post['image'] = add_query_arg(
|
||||
|
||||
@@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\OrderAttribution;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
@@ -250,6 +251,15 @@ class Edit {
|
||||
'high'
|
||||
);
|
||||
|
||||
// Add customer history meta box if analytics is enabled.
|
||||
if ( 'yes' !== get_option( 'woocommerce_analytics_enabled' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! OrderUtil::is_order_edit_screen() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer history meta box.
|
||||
*
|
||||
@@ -260,7 +270,7 @@ class Edit {
|
||||
add_meta_box(
|
||||
'woocommerce-customer-history',
|
||||
__( 'Customer history', 'woocommerce' ),
|
||||
function( $post_or_order ) use ( $customer_history_meta_box ) {
|
||||
function ( $post_or_order ) use ( $customer_history_meta_box ) {
|
||||
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$customer_history_meta_box->output( $order );
|
||||
|
||||
@@ -372,7 +372,7 @@ class ListTable extends WP_List_Table {
|
||||
'type' => $this->order_type,
|
||||
);
|
||||
|
||||
foreach ( array( 'status', 's', 'm', '_customer_user' ) as $query_var ) {
|
||||
foreach ( array( 'status', 's', 'm', '_customer_user', 'search-filter' ) as $query_var ) {
|
||||
$this->request[ $query_var ] = sanitize_text_field( wp_unslash( $_REQUEST[ $query_var ] ?? '' ) );
|
||||
}
|
||||
|
||||
@@ -532,6 +532,11 @@ class ListTable extends WP_List_Table {
|
||||
$this->order_query_args['s'] = $search_term;
|
||||
$this->has_filter = true;
|
||||
}
|
||||
|
||||
$filter = trim( sanitize_text_field( $this->request['search-filter'] ) );
|
||||
if ( ! empty( $filter ) ) {
|
||||
$this->order_query_args['search_filter'] = $filter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -541,8 +546,22 @@ class ListTable extends WP_List_Table {
|
||||
* @return array
|
||||
*/
|
||||
public function get_views() {
|
||||
$view_links = array();
|
||||
|
||||
/**
|
||||
* Filters the list of available list table view links before the actual query runs.
|
||||
* This can be used to, e.g., remove counts from the links.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string[] $views An array of available list table view links.
|
||||
*/
|
||||
$view_links = apply_filters( 'woocommerce_before_' . $this->order_type . '_list_table_view_links', $view_links );
|
||||
if ( ! empty( $view_links ) ) {
|
||||
return $view_links;
|
||||
}
|
||||
|
||||
$view_counts = array();
|
||||
$view_links = array();
|
||||
$statuses = $this->get_visible_statuses();
|
||||
$current = ! empty( $this->request['status'] ) ? sanitize_text_field( $this->request['status'] ) : 'all';
|
||||
$all_count = 0;
|
||||
@@ -620,6 +639,24 @@ class ListTable extends WP_List_Table {
|
||||
* @return boolean TRUE when the blank state should be rendered, FALSE otherwise.
|
||||
*/
|
||||
private function should_render_blank_state(): bool {
|
||||
/**
|
||||
* Whether we should render a blank state so that custom count queries can be used.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param null $should_render_blank_state `null` will use the built-in counts. Sending a boolean will short-circuit that path.
|
||||
* @param object ListTable The current instance of the class.
|
||||
*/
|
||||
$should_render_blank_state = apply_filters(
|
||||
'woocommerce_' . $this->order_type . '_list_table_should_render_blank_state',
|
||||
null,
|
||||
$this
|
||||
);
|
||||
|
||||
if ( is_bool( $should_render_blank_state ) ) {
|
||||
return $should_render_blank_state;
|
||||
}
|
||||
|
||||
return ( ! $this->has_filter ) && 0 === $this->count_orders_by_status( array_keys( $this->get_visible_statuses() ) );
|
||||
}
|
||||
|
||||
@@ -719,11 +756,22 @@ class ListTable extends WP_List_Table {
|
||||
private function months_filter() {
|
||||
// XXX: [review] we may prefer to move this logic outside of the ListTable class.
|
||||
|
||||
/**
|
||||
* Filters whether to remove the 'Months' drop-down from the order list table.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param bool $disable Whether to disable the drop-down. Default false.
|
||||
*/
|
||||
if ( apply_filters( 'woocommerce_' . $this->order_type . '_list_table_disable_months_filter', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_locale;
|
||||
global $wpdb;
|
||||
|
||||
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
|
||||
$utc_offset = wc_timezone_offset();
|
||||
$utc_offset = wc_timezone_offset();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$order_dates = $wpdb->get_results(
|
||||
@@ -1347,7 +1395,7 @@ class ListTable extends WP_List_Table {
|
||||
* @return int Number of orders that were trashed.
|
||||
*/
|
||||
private function do_delete( array $ids, bool $force_delete = false ): int {
|
||||
$changed = 0;
|
||||
$changed = 0;
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
@@ -1537,4 +1585,55 @@ class ListTable extends WP_List_Table {
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the search box with various options to limit order search results.
|
||||
*
|
||||
* @param string $text The search button text.
|
||||
* @param string $input_id The search input ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function search_box( $text, $input_id ) {
|
||||
if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input_id = $input_id . '-search-input';
|
||||
|
||||
if ( ! empty( $_REQUEST['orderby'] ) ) {
|
||||
echo '<input type="hidden" name="orderby" value="' . esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) ) . '" />';
|
||||
}
|
||||
if ( ! empty( $_REQUEST['order'] ) ) {
|
||||
echo '<input type="hidden" name="order" value="' . esc_attr( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) . '" />';
|
||||
}
|
||||
?>
|
||||
<p class="search-box">
|
||||
<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?>:</label>
|
||||
<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
|
||||
<?php $this->search_filter(); ?>
|
||||
<?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the search filter dropdown.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function search_filter() {
|
||||
$options = array(
|
||||
'order_id' => __( 'Order ID', 'woocommerce' ),
|
||||
'customer_email' => __( 'Customer Email', 'woocommerce' ),
|
||||
'customers' => __( 'Customers', 'woocommerce' ),
|
||||
'products' => __( 'Products', 'woocommerce' ),
|
||||
'all' => __( 'All', 'woocommerce' ),
|
||||
);
|
||||
?>
|
||||
<select name="search-filter" id="order-search-filter">
|
||||
<?php foreach ( $options as $value => $label ) { ?>
|
||||
<option value="<?php echo esc_attr( wp_unslash( sanitize_text_field( $value ) ) ); ?>" <?php selected( $value, sanitize_text_field( wp_unslash( $_REQUEST['search-filter'] ?? 'all' ) ) ); ?>><?php echo esc_html( $label ); ?></option>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\OrderAttributionMeta;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Customers\Query as CustomersQuery;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
@@ -12,8 +12,6 @@ use WC_Order;
|
||||
*/
|
||||
class CustomerHistory {
|
||||
|
||||
use OrderAttributionMeta;
|
||||
|
||||
/**
|
||||
* Output the customer history template for the order.
|
||||
*
|
||||
@@ -22,34 +20,47 @@ class CustomerHistory {
|
||||
* @return void
|
||||
*/
|
||||
public function output( WC_Order $order ): void {
|
||||
$this->display_customer_history( $order->get_customer_id(), $order->get_billing_email() );
|
||||
}
|
||||
// No history when adding a new order.
|
||||
if ( 'auto-draft' === $order->get_status() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the customer history template for the customer.
|
||||
*
|
||||
* @param int $customer_id The customer ID.
|
||||
* @param string $billing_email The customer billing email.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function display_customer_history( int $customer_id, string $billing_email ): void {
|
||||
$has_customer_id = false;
|
||||
if ( $customer_id ) {
|
||||
$has_customer_id = true;
|
||||
$args = $this->get_customer_history( $customer_id );
|
||||
} elseif ( $billing_email ) {
|
||||
$args = $this->get_customer_history( $billing_email );
|
||||
} else {
|
||||
$args = array(
|
||||
'order_count' => 0,
|
||||
'total_spent' => 0,
|
||||
'average_spent' => 0,
|
||||
$customer_history = null;
|
||||
|
||||
if ( method_exists( $order, 'get_report_customer_id' ) ) {
|
||||
$customer_history = $this->get_customer_history( $order->get_report_customer_id() );
|
||||
}
|
||||
|
||||
if ( ! $customer_history ) {
|
||||
$customer_history = array(
|
||||
'orders_count' => 0,
|
||||
'total_spend' => 0,
|
||||
'avg_order_value' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$args['has_customer_id'] = $has_customer_id;
|
||||
wc_get_template( 'order/customer-history.php', $args );
|
||||
wc_get_template( 'order/customer-history.php', $customer_history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the order history for the customer (data matches Customers report).
|
||||
*
|
||||
* @param int $customer_report_id The reports customer ID (not necessarily User ID).
|
||||
*
|
||||
* @return array|null Order count, total spend, and average spend per order.
|
||||
*/
|
||||
private function get_customer_history( $customer_report_id ): ?array {
|
||||
|
||||
$args = array(
|
||||
'customers' => array( $customer_report_id ),
|
||||
// If unset, these params have default values that affect the results.
|
||||
'order_after' => null,
|
||||
'order_before' => null,
|
||||
);
|
||||
|
||||
$customers_query = new CustomersQuery( $args );
|
||||
$customer_data = $customers_query->get_data();
|
||||
return $customer_data->data[0] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,6 @@ class OrderAttribution {
|
||||
// Only show more details toggle if there is more than just the origin.
|
||||
'has_more_details' => array( 'origin' ) !== array_keys( $meta ),
|
||||
);
|
||||
wc_get_template( 'order/attribution-data-fields.php', $template_data );
|
||||
wc_get_template( 'order/attribution-details.php', $template_data );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ abstract class Component {
|
||||
*/
|
||||
public static function get_argument_from_path( $arguments, $path, $delimiter = '.' ) {
|
||||
$path_keys = explode( $delimiter, $path );
|
||||
$num_keys = count( $path_keys );
|
||||
$num_keys = false !== $path_keys ? count( $path_keys ) : 0;
|
||||
|
||||
$val = $arguments;
|
||||
for ( $i = 0; $i < $num_keys; $i++ ) {
|
||||
|
||||
@@ -64,7 +64,25 @@ class MailchimpScheduler {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = $this->make_request( $profile_data['store_email'] );
|
||||
$country_code = WC()->countries->get_base_country();
|
||||
$country_name = WC()->countries->countries[ $country_code ] ?? 'N/A';
|
||||
|
||||
$state = WC()->countries->get_base_state();
|
||||
$state_name = WC()->countries->states[ $country_code ][ $state ] ?? 'N/A';
|
||||
|
||||
$address = array(
|
||||
// Setting N/A for addr1, city, state, zipcode and country as they are
|
||||
// required fields. Setting '' doesn't work.
|
||||
'addr1' => 'N/A',
|
||||
'addr2' => '',
|
||||
'city' => 'N/A',
|
||||
'state' => $state_name,
|
||||
'zip' => 'N/A',
|
||||
'country' => $country_name,
|
||||
);
|
||||
|
||||
$response = $this->make_request( $profile_data['store_email'], $address );
|
||||
|
||||
if ( is_wp_error( $response ) || ! isset( $response['body'] ) ) {
|
||||
$this->handle_request_error();
|
||||
return false;
|
||||
@@ -85,10 +103,11 @@ class MailchimpScheduler {
|
||||
*
|
||||
* @internal
|
||||
* @param string $store_email Email address to subscribe.
|
||||
* @param array $address Store address.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function make_request( $store_email ) {
|
||||
public function make_request( $store_email, $address ) {
|
||||
if ( true === defined( 'WP_ENVIRONMENT_TYPE' ) && 'development' === constant( 'WP_ENVIRONMENT_TYPE' ) ) {
|
||||
$subscribe_endpoint = self::SUBSCRIBE_ENDPOINT_DEV;
|
||||
} else {
|
||||
@@ -101,7 +120,8 @@ class MailchimpScheduler {
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
'method' => 'POST',
|
||||
'body' => array(
|
||||
'email' => $store_email,
|
||||
'email' => $store_email,
|
||||
'address' => $address,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -240,7 +240,19 @@ class Settings {
|
||||
|
||||
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
|
||||
|
||||
$settings['gutenberg_version'] = defined( 'GUTENBERG_VERSION' ) ? constant( 'GUTENBERG_VERSION' ) : 0;
|
||||
$has_gutenberg = is_plugin_active( 'gutenberg/gutenberg.php' );
|
||||
$gutenberg_version = '';
|
||||
if ( $has_gutenberg ) {
|
||||
if ( defined( 'GUTENBERG_VERSION' ) ) {
|
||||
$gutenberg_version = GUTENBERG_VERSION;
|
||||
}
|
||||
|
||||
if ( ! $gutenberg_version ) {
|
||||
$gutenberg_data = get_plugin_data( WP_PLUGIN_DIR . '/gutenberg/gutenberg.php' );
|
||||
$gutenberg_version = $gutenberg_data['Version'];
|
||||
}
|
||||
}
|
||||
$settings['gutenberg_version'] = $has_gutenberg ? $gutenberg_version : 0;
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,34 @@ class WCPaymentGatewayPreInstallWCPayPromotion extends \WC_Payment_Gateway {
|
||||
$this->method_description = $wc_pay_spec->content;
|
||||
$this->has_fields = false;
|
||||
|
||||
// Set the promotion pseudo-gateway support features.
|
||||
// If the promotion spec provides the supports property, use it.
|
||||
if ( property_exists( $wc_pay_spec, 'supports' ) ) {
|
||||
$this->supports = $wc_pay_spec->supports;
|
||||
} else {
|
||||
// Otherwise, use the default supported features in line with WooPayments ones.
|
||||
// We include all features here, even if some of them are behind settings, since this is for info only.
|
||||
$this->supports = array(
|
||||
// Regular features.
|
||||
'products',
|
||||
'refunds',
|
||||
// Subscriptions features.
|
||||
'subscriptions',
|
||||
'multiple_subscriptions',
|
||||
'subscription_cancellation',
|
||||
'subscription_reactivation',
|
||||
'subscription_suspension',
|
||||
'subscription_amount_changes',
|
||||
'subscription_date_changes',
|
||||
'subscription_payment_method_change_admin',
|
||||
'subscription_payment_method_change_customer',
|
||||
'subscription_payment_method_change',
|
||||
// Saved cards features.
|
||||
'tokenization',
|
||||
'add_payment_method',
|
||||
);
|
||||
}
|
||||
|
||||
// Get setting values.
|
||||
$this->enabled = false;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use ActionScheduler;
|
||||
use WC_Admin_Settings;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
@@ -326,9 +327,16 @@ class CustomOrdersTableController {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
if ( ! filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'hpos-sync-now' ) ) {
|
||||
WC_Admin_Settings::add_error( esc_html__( 'Unable to start synchronization. The link you followed may have expired.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,11 +514,14 @@ class CustomOrdersTableController {
|
||||
$orders_pending_sync_count
|
||||
);
|
||||
} elseif ( $sync_is_pending ) {
|
||||
$sync_now_url = add_query_arg(
|
||||
array(
|
||||
self::SYNC_QUERY_ARG => true,
|
||||
$sync_now_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
self::SYNC_QUERY_ARG => true,
|
||||
),
|
||||
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
|
||||
),
|
||||
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
|
||||
'hpos-sync-now'
|
||||
);
|
||||
|
||||
if ( ! $is_dangerous ) {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Caches\OrderCache;
|
||||
use Automattic\WooCommerce\Caches\OrderCacheController;
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessingController, BatchProcessorInterface };
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
@@ -335,6 +335,33 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
return $interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys that can be ignored during synchronization or verification.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ignored_order_props() {
|
||||
/**
|
||||
* Allows modifying the list of order properties that are ignored during HPOS synchronization or verification.
|
||||
*
|
||||
* @param string[] List of order properties or meta keys.
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$ignored_props = apply_filters( 'woocommerce_hpos_sync_ignored_order_props', array() );
|
||||
$ignored_props = array_filter( array_map( 'trim', array_filter( $ignored_props, 'is_string' ) ) );
|
||||
|
||||
return array_merge(
|
||||
$ignored_props,
|
||||
array(
|
||||
'_paid_date', // This has been deprecated and replaced by '_date_paid' in the CPT datastore.
|
||||
'_completed_date', // This has been deprecated and replaced by '_date_completed' in the CPT datastore.
|
||||
EditLock::META_KEY_NAME,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an event to run background sync when the mode is set to interval.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
@@ -171,13 +173,11 @@ class LegacyDataHandler {
|
||||
/**
|
||||
* Checks whether an HPOS-backed order is newer than the corresponding post.
|
||||
*
|
||||
* @param int|\WC_Order $order An HPOS order.
|
||||
* @param \WC_Abstract_Order $order An HPOS order.
|
||||
* @return bool TRUE if the order is up to date with the corresponding post.
|
||||
* @throws \Exception When the order is not an HPOS order.
|
||||
*/
|
||||
private function is_order_newer_than_post( $order ): bool {
|
||||
$order = is_a( $order, 'WC_Order' ) ? $order : wc_get_order( absint( $order ) );
|
||||
|
||||
private function is_order_newer_than_post( \WC_Abstract_Order $order ): bool {
|
||||
if ( ! is_a( $order->get_data_store()->get_current_class_name(), OrdersTableDataStore::class, true ) ) {
|
||||
throw new \Exception( __( 'Order is not an HPOS order.', 'woocommerce' ) );
|
||||
}
|
||||
@@ -195,6 +195,137 @@ class LegacyDataHandler {
|
||||
return $order_modified_gmt >= $post_modified_gmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array with properties and metadata for which HPOS and post record have different values.
|
||||
* Given it's mostly informative nature, it doesn't perform any deep or recursive searches and operates only on top-level properties/metadata.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return array Array of [HPOS value, post value] keyed by property, for all properties where HPOS and post value differ.
|
||||
*/
|
||||
public function get_diff_for_order( int $order_id ): array {
|
||||
$diff = array();
|
||||
|
||||
$hpos_order = $this->get_order_from_datastore( $order_id, 'hpos' );
|
||||
$cpt_order = $this->get_order_from_datastore( $order_id, 'cpt' );
|
||||
|
||||
if ( $hpos_order->get_type() !== $cpt_order->get_type() ) {
|
||||
$diff['type'] = array( $hpos_order->get_type(), $cpt_order->get_type() );
|
||||
}
|
||||
|
||||
$hpos_meta = $this->order_meta_to_array( $hpos_order );
|
||||
$cpt_meta = $this->order_meta_to_array( $cpt_order );
|
||||
|
||||
// Consider only keys for which we actually have a corresponding HPOS column or are meta.
|
||||
$all_keys = array_unique(
|
||||
array_diff(
|
||||
array_merge(
|
||||
$this->get_order_base_props(),
|
||||
array_keys( $hpos_meta ),
|
||||
array_keys( $cpt_meta )
|
||||
),
|
||||
$this->data_synchronizer->get_ignored_order_props()
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $all_keys as $key ) {
|
||||
$val1 = in_array( $key, $this->get_order_base_props(), true ) ? $hpos_order->{"get_$key"}() : ( $hpos_meta[ $key ] ?? null );
|
||||
$val2 = in_array( $key, $this->get_order_base_props(), true ) ? $cpt_order->{"get_$key"}() : ( $cpt_meta[ $key ] ?? null );
|
||||
|
||||
// Workaround for https://github.com/woocommerce/woocommerce/issues/43126.
|
||||
if ( ! $val2 && in_array( $key, array( '_billing_address_index', '_shipping_address_index' ), true ) ) {
|
||||
$val2 = get_post_meta( $order_id, $key, true );
|
||||
}
|
||||
|
||||
if ( $val1 != $val2 ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
|
||||
$diff[ $key ] = array( $val1, $val2 );
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an order object as seen by either the HPOS or CPT datastores.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'cpt'. Defaults to 'hpos'.
|
||||
* @return \WC_Order Order instance.
|
||||
*/
|
||||
public function get_order_from_datastore( int $order_id, string $data_store_id = 'hpos' ) {
|
||||
$data_store = ( 'hpos' === $data_store_id ) ? $this->data_store : $this->data_store->get_cpt_data_store_instance();
|
||||
|
||||
wp_cache_delete( \WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' );
|
||||
|
||||
// Prime caches if we can.
|
||||
if ( method_exists( $data_store, 'prime_caches_for_orders' ) ) {
|
||||
$data_store->prime_caches_for_orders( array( $order_id ), array() );
|
||||
}
|
||||
|
||||
$classname = wc_get_order_type( $data_store->get_order_type( $order_id ) )['class_name'];
|
||||
$order = new $classname();
|
||||
$order->set_id( $order_id );
|
||||
|
||||
// Switch datastore if necessary.
|
||||
$update_data_store_func = function ( $data_store ) {
|
||||
// Each order object contains a reference to its data store, but this reference is itself
|
||||
// held inside of an instance of WC_Data_Store, so we create that first.
|
||||
$data_store_wrapper = \WC_Data_Store::load( 'order' );
|
||||
|
||||
// Bind $data_store to our WC_Data_Store.
|
||||
( function ( $data_store ) {
|
||||
$this->current_class_name = get_class( $data_store );
|
||||
$this->instance = $data_store;
|
||||
} )->call( $data_store_wrapper, $data_store );
|
||||
|
||||
// Finally, update the $order object with our WC_Data_Store( $data_store ) instance.
|
||||
$this->data_store = $data_store_wrapper;
|
||||
};
|
||||
$update_data_store_func->call( $order, $data_store );
|
||||
|
||||
// Read order.
|
||||
$data_store->read( $order );
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all metadata in an order object as an array.
|
||||
*
|
||||
* @param \WC_Order $order Order instance.
|
||||
* @return array Array of metadata grouped by meta key.
|
||||
*/
|
||||
private function order_meta_to_array( \WC_Order &$order ): array {
|
||||
$result = array();
|
||||
|
||||
foreach ( ArrayUtil::select( $order->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD ) as &$meta ) {
|
||||
if ( array_key_exists( $meta['key'], $result ) ) {
|
||||
$result[ $meta['key'] ] = array( $result[ $meta['key'] ] );
|
||||
$result[ $meta['key'] ][] = $meta['value'];
|
||||
} else {
|
||||
$result[ $meta['key'] ] = $meta['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns names of all order base properties supported by HPOS.
|
||||
*
|
||||
* @return string[] Property names.
|
||||
*/
|
||||
private function get_order_base_props(): array {
|
||||
return array_column(
|
||||
call_user_func_array(
|
||||
'array_merge',
|
||||
array_values( $this->data_store->get_all_order_column_mappings() )
|
||||
),
|
||||
'name'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
'_new_order_email_sent',
|
||||
);
|
||||
|
||||
/**
|
||||
* Meta keys that are considered ephemereal and do not trigger a full save (updating modified date) when changed.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $ephemeral_meta_keys = array(
|
||||
EditLock::META_KEY_NAME,
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles custom metadata in the wc_orders_meta table.
|
||||
*
|
||||
@@ -1253,12 +1262,10 @@ WHERE
|
||||
$post_order_modified_date = is_null( $post_order_modified_date ) ? 0 : $post_order_modified_date->getTimestamp();
|
||||
|
||||
/**
|
||||
* We are here because there was difference in posts and order data, although the sync is enabled.
|
||||
* When order modified date is more recent than post modified date, it can only mean that COT definitely has more updated version of the order.
|
||||
*
|
||||
* In a case where post meta was updated (without updating post_modified date), post_modified would be equal to order_modified date.
|
||||
*
|
||||
* So we write back to the order table when order modified date is more recent than post modified date. Otherwise, we write to the post table.
|
||||
* We are here because there was difference in the post and order data even though sync is enabled. If the modified date in
|
||||
* the post is the same or more recent than the modified date in the order object, we update the order object with the data
|
||||
* from the post. The opposite case is handled in 'backfill_post_record'. This mitigates the case where other plugins write
|
||||
* to the post or postmeta directly.
|
||||
*/
|
||||
if ( $post_order_modified_date >= $order_modified_date ) {
|
||||
$this->migrate_post_record( $order, $post_order );
|
||||
@@ -1547,7 +1554,7 @@ WHERE
|
||||
*
|
||||
* @param array $ids List of order IDs.
|
||||
*
|
||||
* @return \stdClass[]|object|null DB Order objects or error.
|
||||
* @return \stdClass[] DB Order objects or error.
|
||||
*/
|
||||
protected function get_order_data_for_ids( $ids ) {
|
||||
global $wpdb;
|
||||
@@ -2978,9 +2985,7 @@ CREATE TABLE $meta_table (
|
||||
private function should_save_after_meta_change( $order, $meta = null ) {
|
||||
$current_time = $this->legacy_proxy->call_function( 'current_time', 'mysql', 1 );
|
||||
$current_date_time = new \WC_DateTime( $current_time, new \DateTimeZone( 'GMT' ) );
|
||||
$skip_for = array(
|
||||
EditLock::META_KEY_NAME,
|
||||
);
|
||||
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $skip_for, true ) );
|
||||
|
||||
return $order->get_date_modified() < $current_date_time && empty( $order->get_changes() ) && ( ! is_object( $meta ) || ! in_array( $meta->key, $this->ephemeral_meta_keys, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class OrdersTableFieldQuery {
|
||||
} else {
|
||||
$relation = $q['relation'];
|
||||
unset( $q['relation'] );
|
||||
|
||||
$chunks = array();
|
||||
foreach ( $q as $query ) {
|
||||
$chunks[] = $this->process( $query );
|
||||
}
|
||||
@@ -292,11 +292,10 @@ class OrdersTableFieldQuery {
|
||||
}
|
||||
|
||||
$clause_compare = $clause['compare'];
|
||||
|
||||
switch ( $clause_compare ) {
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( (array) $clause_value ) ), 1 ) . ')', $clause_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'BETWEEN':
|
||||
case 'NOT BETWEEN':
|
||||
@@ -327,7 +326,7 @@ class OrdersTableFieldQuery {
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $where ) {
|
||||
if ( ! empty( $where ) ) {
|
||||
if ( 'CHAR' === $clause['cast'] ) {
|
||||
return "`{$clause['alias']}`.`{$clause['column']}` {$clause_compare} {$where}";
|
||||
} else {
|
||||
|
||||
@@ -389,7 +389,7 @@ class OrdersTableMetaQuery {
|
||||
// Nested.
|
||||
$relation = $arg['relation'];
|
||||
unset( $arg['relation'] );
|
||||
|
||||
$chunks = array();
|
||||
foreach ( $arg as $index => &$clause ) {
|
||||
$chunks[] = $this->process( $clause, $arg );
|
||||
}
|
||||
@@ -519,6 +519,9 @@ class OrdersTableMetaQuery {
|
||||
|
||||
$alias = $clause['alias'];
|
||||
|
||||
$meta_compare_string_start = '';
|
||||
$meta_compare_string_end = '';
|
||||
$subquery_alias = '';
|
||||
if ( in_array( $clause['compare_key'], array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
|
||||
$i = count( $this->table_aliases );
|
||||
$subquery_alias = self::ALIAS_PREFIX . $i;
|
||||
@@ -541,7 +544,7 @@ class OrdersTableMetaQuery {
|
||||
$where = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
break;
|
||||
case 'IN':
|
||||
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
|
||||
$meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( (array) $clause['key'] ) ), 1 ) . ')';
|
||||
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'RLIKE':
|
||||
@@ -566,7 +569,7 @@ class OrdersTableMetaQuery {
|
||||
$where = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
case 'NOT IN':
|
||||
$array_subclause = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
|
||||
$array_subclause = '(' . substr( str_repeat( ',%s', count( (array) $clause['key'] ) ), 1 ) . ') ';
|
||||
$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
|
||||
$where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
@@ -618,7 +621,7 @@ class OrdersTableMetaQuery {
|
||||
switch ( $meta_compare ) {
|
||||
case 'IN':
|
||||
case 'NOT IN':
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$where = $wpdb->prepare( '(' . substr( str_repeat( ',%s', count( (array) $meta_value ) ), 1 ) . ')', $meta_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
break;
|
||||
|
||||
case 'BETWEEN':
|
||||
|
||||
@@ -346,6 +346,7 @@ class OrdersTableQuery {
|
||||
'day' => '',
|
||||
);
|
||||
|
||||
$precision = null;
|
||||
if ( is_numeric( $date ) ) {
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
|
||||
$precision = 'second';
|
||||
@@ -919,6 +920,23 @@ class OrdersTableQuery {
|
||||
}
|
||||
$orders_table = $this->tables['orders'];
|
||||
$this->count_sql = "SELECT COUNT(DISTINCT $fields) FROM $orders_table $join WHERE $where";
|
||||
|
||||
if ( ! $this->suppress_filters ) {
|
||||
/**
|
||||
* Filters the count SQL query.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string $sql The count SQL query.
|
||||
* @param OrdersTableQuery $query The OrdersTableQuery instance (passed by reference).
|
||||
* @param array $args Query args.
|
||||
* @param string $fields Prepared fields for SELECT clause.
|
||||
* @param string $join Prepared JOIN clause.
|
||||
* @param string $where Prepared WHERE clause.
|
||||
* @param string $groupby Prepared GROUP BY clause.
|
||||
*/
|
||||
$this->count_sql = apply_filters_ref_array( 'woocommerce_orders_table_query_count_sql', array( $this->count_sql, &$this, $this->args, $fields, $join, $where, $groupby ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1131,7 +1149,7 @@ class OrdersTableQuery {
|
||||
$values = is_array( $values ) ? $values : array( $values );
|
||||
$ids = array();
|
||||
$emails = array();
|
||||
|
||||
$pieces = array();
|
||||
foreach ( $values as $value ) {
|
||||
if ( is_array( $value ) ) {
|
||||
$sql = $this->generate_customer_query( $value, 'AND' );
|
||||
|
||||
@@ -24,6 +24,13 @@ class OrdersTableSearchQuery {
|
||||
*/
|
||||
private $search_term;
|
||||
|
||||
/**
|
||||
* Limits the search to a specific field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $search_filters;
|
||||
|
||||
/**
|
||||
* Creates the JOIN and WHERE clauses needed to execute a search of orders.
|
||||
*
|
||||
@@ -32,8 +39,31 @@ class OrdersTableSearchQuery {
|
||||
* @param OrdersTableQuery $query The order query object.
|
||||
*/
|
||||
public function __construct( OrdersTableQuery $query ) {
|
||||
$this->query = $query;
|
||||
$this->search_term = urldecode( $query->get( 's' ) );
|
||||
$this->query = $query;
|
||||
$this->search_term = urldecode( $query->get( 's' ) );
|
||||
$this->search_filters = $this->sanitize_search_filters( urldecode( $query->get( 'search_filter' ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize search filter param.
|
||||
*
|
||||
* @param string $search_filter Search filter param.
|
||||
*
|
||||
* @return array Array of search filters.
|
||||
*/
|
||||
private function sanitize_search_filters( string $search_filter ) : array {
|
||||
$available_filters = array(
|
||||
'order_id',
|
||||
'customer_email',
|
||||
'customers', // customers also searches in meta.
|
||||
'products',
|
||||
);
|
||||
|
||||
if ( 'all' === $search_filter || '' === $search_filter ) {
|
||||
return $available_filters;
|
||||
} else {
|
||||
return array_intersect( $available_filters, array( $search_filter ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,12 +92,34 @@ class OrdersTableSearchQuery {
|
||||
* @return string
|
||||
*/
|
||||
private function generate_join(): string {
|
||||
$orders_table = $this->query->get_table_name( 'orders' );
|
||||
$items_table = $this->query->get_table_name( 'items' );
|
||||
$join = array();
|
||||
|
||||
return "
|
||||
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
|
||||
";
|
||||
foreach ( $this->search_filters as $search_filter ) {
|
||||
$join[] = $this->generate_join_for_search_filter( $search_filter );
|
||||
}
|
||||
|
||||
return implode( ' ', $join );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JOIN clause for a given search filter.
|
||||
* Right now we only have the products filter that actually does a JOIN, but in the future we may add more -- for example, custom order fields, payment tokens, and so on. This function makes it easier to add more filters in the future.
|
||||
*
|
||||
* If a search filter needs a JOIN, it will also need a WHERE clause.
|
||||
*
|
||||
* @param string $search_filter Name of the search filter.
|
||||
*
|
||||
* @return string JOIN clause.
|
||||
*/
|
||||
private function generate_join_for_search_filter( $search_filter ) : string {
|
||||
if ( 'products' === $search_filter ) {
|
||||
$orders_table = $this->query->get_table_name( 'orders' );
|
||||
$items_table = $this->query->get_table_name( 'items' );
|
||||
return "
|
||||
LEFT JOIN $items_table AS search_query_items ON search_query_items.order_id = $orders_table.id
|
||||
";
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,27 +130,66 @@ class OrdersTableSearchQuery {
|
||||
* @return string
|
||||
*/
|
||||
private function generate_where(): string {
|
||||
global $wpdb;
|
||||
$where = '';
|
||||
$where = array();
|
||||
$possible_order_id = (string) absint( $this->search_term );
|
||||
$order_table = $this->query->get_table_name( 'orders' );
|
||||
|
||||
// Support the passing of an order ID as the search term.
|
||||
if ( (string) $this->query->get( 's' ) === $possible_order_id ) {
|
||||
$where = "`$order_table`.id = $possible_order_id OR ";
|
||||
$where[] = "`$order_table`.id = $possible_order_id";
|
||||
}
|
||||
|
||||
$meta_sub_query = $this->generate_where_for_meta_table();
|
||||
foreach ( $this->search_filters as $search_filter ) {
|
||||
$search_where = $this->generate_where_for_search_filter( $search_filter );
|
||||
if ( ! empty( $search_where ) ) {
|
||||
$where[] = $search_where;
|
||||
}
|
||||
}
|
||||
|
||||
$where .= $wpdb->prepare(
|
||||
"
|
||||
search_query_items.order_item_name LIKE %s
|
||||
OR `$order_table`.id IN ( $meta_sub_query )
|
||||
",
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
$where_statement = implode( ' OR ', $where );
|
||||
|
||||
return " ( $where ) ";
|
||||
return " ( $where_statement ) ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates WHERE clause for a given search filter. Right now we only have the products and customers filters that actually use WHERE, but in the future we may add more -- for example, custom order fields, payment tokens and so on. This function makes it easier to add more filters in the future.
|
||||
*
|
||||
* @param string $search_filter Name of the search filter.
|
||||
*
|
||||
* @return string WHERE clause.
|
||||
*/
|
||||
private function generate_where_for_search_filter( string $search_filter ) : string {
|
||||
global $wpdb;
|
||||
|
||||
$order_table = $this->query->get_table_name( 'orders' );
|
||||
|
||||
if ( 'customer_email' === $search_filter ) {
|
||||
return $wpdb->prepare(
|
||||
"`$order_table`.billing_email LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
|
||||
$wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'order_id' === $search_filter && is_numeric( $this->search_term ) ) {
|
||||
return $wpdb->prepare(
|
||||
"`$order_table`.id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $order_table is hardcoded.
|
||||
absint( $this->search_term )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'products' === $search_filter ) {
|
||||
return $wpdb->prepare(
|
||||
'search_query_items.order_item_name LIKE %s',
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'customers' === $search_filter ) {
|
||||
$meta_sub_query = $this->generate_where_for_meta_table();
|
||||
return "`$order_table`.id IN ( $meta_sub_query ) ";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +205,12 @@ class OrdersTableSearchQuery {
|
||||
global $wpdb;
|
||||
$meta_table = $this->query->get_table_name( 'meta' );
|
||||
$meta_fields = $this->get_meta_fields_to_be_searched();
|
||||
|
||||
if ( '' === $meta_fields ) {
|
||||
return '-1';
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $meta_fields is already escaped before imploding, $meta_table is hardcoded.
|
||||
return $wpdb->prepare(
|
||||
"
|
||||
SELECT search_query_meta.order_id
|
||||
@@ -124,6 +221,7 @@ GROUP BY search_query_meta.order_id
|
||||
",
|
||||
'%' . $wpdb->esc_like( $this->search_term ) . '%'
|
||||
);
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +233,11 @@ GROUP BY search_query_meta.order_id
|
||||
* @return string
|
||||
*/
|
||||
private function get_meta_fields_to_be_searched(): string {
|
||||
$meta_fields_to_search = array(
|
||||
'_billing_address_index',
|
||||
'_shipping_address_index',
|
||||
);
|
||||
|
||||
/**
|
||||
* Controls the order meta keys to be included in search queries.
|
||||
*
|
||||
@@ -147,10 +250,7 @@ GROUP BY search_query_meta.order_id
|
||||
*/
|
||||
$meta_keys = apply_filters(
|
||||
'woocommerce_order_table_search_query_meta_keys',
|
||||
array(
|
||||
'_billing_address_index',
|
||||
'_shipping_address_index',
|
||||
)
|
||||
$meta_fields_to_search
|
||||
);
|
||||
|
||||
$meta_keys = (array) array_map(
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* BlockTemplatesServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplatesController;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\TemplateTransformer;
|
||||
|
||||
/**
|
||||
* Service provider for the block templates controller classes in the Automattic\WooCommerce\Internal\BlockTemplateRegistry namespace.
|
||||
*/
|
||||
class BlockTemplatesServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
BlockTemplateRegistry::class,
|
||||
BlockTemplatesController::class,
|
||||
TemplateTransformer::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( TemplateTransformer::class );
|
||||
$this->share( BlockTemplateRegistry::class );
|
||||
$this->share( BlockTemplatesController::class )->addArguments(
|
||||
array(
|
||||
BlockTemplateRegistry::class,
|
||||
TemplateTransformer::class,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user