Merged in feature/MAW-855-import-code-into-aws (pull request #2)
code import from pantheon * code import from pantheon
This commit is contained in:
@@ -97,7 +97,37 @@ class OnboardingFreeExtensions extends WC_REST_Data_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
$extensions = $this->replace_jetpack_with_jetpack_boost_for_treatment( $extensions );
|
||||
|
||||
return new WP_REST_Response( $extensions );
|
||||
}
|
||||
|
||||
private function replace_jetpack_with_jetpack_boost_for_treatment( array $extensions ) {
|
||||
$is_treatment = \WooCommerce\Admin\Experimental_Abtest::in_treatment( 'woocommerce_jetpack_copy' );
|
||||
|
||||
if ( ! $is_treatment ) {
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
$has_core_profiler = array_search( 'obw/core-profiler', array_column( $extensions, 'key' ) );
|
||||
|
||||
if ( $has_core_profiler === false ) {
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
$has_jetpack = array_search( 'jetpack', array_column( $extensions[ $has_core_profiler ]['plugins'], 'key' ) );
|
||||
|
||||
if ( $has_jetpack === false ) {
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
$jetpack = &$extensions[ $has_core_profiler ]['plugins'][ $has_jetpack ];
|
||||
$jetpack->key = 'jetpack-boost';
|
||||
$jetpack->name = 'Jetpack Boost';
|
||||
$jetpack->label = __( 'Optimize store performance with Jetpack Boost', 'woocommerce' );
|
||||
$jetpack->description = __( 'Speed up your store and improve your SEO with performance-boosting tools from Jetpack. Learn more', 'woocommerce' );
|
||||
$jetpack->learn_more_link = 'https://jetpack.com/boost/';
|
||||
|
||||
return $extensions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,9 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
add_action( 'woocommerce_plugins_install_error', array( $this, 'log_plugins_install_error' ), 10, 4 );
|
||||
add_action( 'woocommerce_plugins_install_api_error', array( $this, 'log_plugins_install_api_error' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,4 +414,41 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function log_plugins_install_error( $slug, $api, $result, $upgrader ) {
|
||||
$properties = array(
|
||||
'error_message' => sprintf(
|
||||
/* translators: %s: plugin slug (example: woocommerce-services) */
|
||||
__(
|
||||
'The requested plugin `%s` could not be installed.',
|
||||
'woocommerce'
|
||||
),
|
||||
$slug
|
||||
),
|
||||
'type' => 'plugin_info_api_error',
|
||||
'slug' => $slug,
|
||||
'api_version' => $api->version,
|
||||
'api_download_link' => $api->download_link,
|
||||
'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ),
|
||||
'result' => is_wp_error( $result ) ? $result->get_error_message() : 'null',
|
||||
);
|
||||
wc_admin_record_tracks_event( 'coreprofiler_install_plugin_error', $properties );
|
||||
}
|
||||
|
||||
public function log_plugins_install_api_error( $slug, $api ) {
|
||||
$properties = array(
|
||||
'error_message' => sprintf(
|
||||
// translators: %s: plugin slug (example: woocommerce-services).
|
||||
__(
|
||||
'The requested plugin `%s` could not be installed. Plugin API call failed.',
|
||||
'woocommerce'
|
||||
),
|
||||
$slug
|
||||
),
|
||||
'type' => 'plugin_install_error',
|
||||
'api_error_message' => $api->get_error_message(),
|
||||
'slug' => $slug,
|
||||
);
|
||||
wc_admin_record_tracks_event( 'coreprofiler_install_plugin_error', $properties );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +391,7 @@ class OnboardingProfile extends \WC_REST_Data_Controller {
|
||||
'items' => array(
|
||||
'enum' => array(
|
||||
'jetpack',
|
||||
'jetpack-boost',
|
||||
'woocommerce-services',
|
||||
'woocommerce-payments',
|
||||
'mailchimp-for-woocommerce',
|
||||
|
||||
@@ -39,7 +39,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
/**
|
||||
* Duration to milisecond mapping.
|
||||
*
|
||||
* @var string
|
||||
* @var array
|
||||
*/
|
||||
protected $duration_to_ms = array(
|
||||
'day' => DAY_IN_SECONDS * 1000,
|
||||
@@ -762,6 +762,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
if ( ! $task && $id ) {
|
||||
$task = new DeprecatedExtendedTask(
|
||||
null,
|
||||
array(
|
||||
'id' => $id,
|
||||
'is_dismissable' => true,
|
||||
@@ -795,6 +796,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
if ( ! $task && $id ) {
|
||||
$task = new DeprecatedExtendedTask(
|
||||
null,
|
||||
array(
|
||||
'id' => $id,
|
||||
'is_dismissable' => true,
|
||||
@@ -837,6 +839,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
if ( ! $task && $task_id ) {
|
||||
$task = new DeprecatedExtendedTask(
|
||||
null,
|
||||
array(
|
||||
'id' => $task_id,
|
||||
'is_snoozeable' => true,
|
||||
@@ -874,6 +877,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
if ( ! $task && $id ) {
|
||||
$task = new DeprecatedExtendedTask(
|
||||
null,
|
||||
array(
|
||||
'id' => $id,
|
||||
'is_snoozeable' => true,
|
||||
@@ -961,6 +965,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
if ( ! $task && $id ) {
|
||||
$task = new DeprecatedExtendedTask(
|
||||
null,
|
||||
array(
|
||||
'id' => $id,
|
||||
)
|
||||
|
||||
@@ -61,6 +61,29 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/recommended',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_recommended_themes' ),
|
||||
'permission_callback' => array( $this, 'get_item_permissions_check' ),
|
||||
'args' => array(
|
||||
'industry' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Limits the results to themes relevant for this industry (optional)',
|
||||
),
|
||||
'currency' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'USD', 'AUD', 'CAD', 'EUR', 'GBP' ),
|
||||
'default' => 'USD',
|
||||
'description' => 'Returns pricing in this currency (optional, default: USD)',
|
||||
),
|
||||
),
|
||||
'schema' => array( $this, 'get_recommended_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,12 +106,7 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
* @return WP_Error|array Theme installation status.
|
||||
*/
|
||||
public function install_theme( $request ) {
|
||||
$allowed_themes = Themes::get_allowed_themes();
|
||||
$theme = sanitize_text_field( $request['theme'] );
|
||||
|
||||
if ( ! in_array( $theme, $allowed_themes, true ) ) {
|
||||
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce' ), 404 );
|
||||
}
|
||||
$theme = sanitize_text_field( $request['theme'] );
|
||||
|
||||
$installed_themes = wp_get_themes();
|
||||
|
||||
@@ -120,7 +138,7 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
return new \WP_Error(
|
||||
'woocommerce_rest_theme_install',
|
||||
sprintf(
|
||||
/* translators: %s: theme slug (example: woocommerce-services) */
|
||||
/* translators: %s: theme slug (example: woocommerce-services) */
|
||||
__( 'The requested theme `%s` could not be installed. Theme API call failed.', 'woocommerce' ),
|
||||
$theme
|
||||
),
|
||||
@@ -135,7 +153,7 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
return new \WP_Error(
|
||||
'woocommerce_rest_theme_install',
|
||||
sprintf(
|
||||
/* translators: %s: theme slug (example: woocommerce-services) */
|
||||
/* translators: %s: theme slug (example: woocommerce-services) */
|
||||
__( 'The requested theme `%s` could not be installed.', 'woocommerce' ),
|
||||
$theme
|
||||
),
|
||||
@@ -157,11 +175,7 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
* @return WP_Error|array Theme activation status.
|
||||
*/
|
||||
public function activate_theme( $request ) {
|
||||
$allowed_themes = Themes::get_allowed_themes();
|
||||
$theme = sanitize_text_field( $request['theme'] );
|
||||
if ( ! in_array( $theme, $allowed_themes, true ) ) {
|
||||
return new \WP_Error( 'woocommerce_rest_invalid_theme', __( 'Invalid theme.', 'woocommerce' ), 404 );
|
||||
}
|
||||
$theme = sanitize_text_field( $request['theme'] );
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/theme.php';
|
||||
|
||||
@@ -184,6 +198,190 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended themes.
|
||||
*
|
||||
* @param WP_REST_Request $request Full details about the request.
|
||||
* @return WP_Error|array Theme activation status.
|
||||
*/
|
||||
public function get_recommended_themes( $request ) {
|
||||
// Check if "industry" and "currency" parameters are provided in the request.
|
||||
$industry = $request->get_param( 'industry' );
|
||||
$currency = $request->get_param( 'currency' ) ?? 'USD';
|
||||
|
||||
// Return empty response if marketplace suggestions are disabled.
|
||||
if (
|
||||
/**
|
||||
* Filter allow marketplace suggestions.
|
||||
*
|
||||
* User can disable all suggestions via filter.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*/
|
||||
! apply_filters( 'woocommerce_allow_marketplace_suggestions', true ) ||
|
||||
get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no'
|
||||
) {
|
||||
|
||||
/**
|
||||
* Filter the onboarding recommended themes response.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*
|
||||
* @param array $response The recommended themes response.
|
||||
* @param array $filtered_themes The filtered themes.
|
||||
* @param string $industry The industry to filter by (if provided).
|
||||
* @param string $currency The currency to convert prices to. (USD, AUD, CAD, EUR, GBP).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
return apply_filters(
|
||||
'__experimental_woocommerce_rest_get_recommended_themes',
|
||||
array(
|
||||
'themes' => array(),
|
||||
'_links' => array(
|
||||
'browse_all' => array(
|
||||
'href' => home_url( '/wp-admin/themes.php' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
$industry,
|
||||
$currency
|
||||
);
|
||||
}
|
||||
|
||||
$current_theme_slug = wp_get_theme()->get_stylesheet();
|
||||
|
||||
// To be implemented: 1. Fetch themes from the marketplace API. 2. Convert prices to the requested currency.
|
||||
// These are Dotcom themes.
|
||||
$themes = array(
|
||||
array(
|
||||
'name' => 'Tsubaki',
|
||||
'price' => 'Free',
|
||||
'color_palettes' => array(),
|
||||
'total_palettes' => 0,
|
||||
'slug' => 'tsubaki',
|
||||
'is_active' => 'tsubaki' === $current_theme_slug,
|
||||
'thumbnail_url' => 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/tsubaki/screenshot.png',
|
||||
'link_url' => 'https://wordpress.com/theme/tsubaki/',
|
||||
),
|
||||
array(
|
||||
'name' => 'Tazza',
|
||||
'price' => 'Free',
|
||||
'color_palettes' => array(),
|
||||
'total_palettes' => 0,
|
||||
'slug' => 'tazza',
|
||||
'is_active' => 'tazza' === $current_theme_slug,
|
||||
'thumbnail_url' => 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/tazza/screenshot.png',
|
||||
'link_url' => 'https://wordpress.com/theme/tazza/',
|
||||
'total_palettes' => 0,
|
||||
),
|
||||
array(
|
||||
'name' => 'Amulet',
|
||||
'price' => 'Free',
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Default',
|
||||
'primary' => '#FEFBF3',
|
||||
'secondary' => '#7F7E7A',
|
||||
),
|
||||
array(
|
||||
'title' => 'Brown Sugar',
|
||||
'primary' => '#EFEBE0',
|
||||
'secondary' => '#AC6239',
|
||||
),
|
||||
array(
|
||||
'title' => 'Midnight',
|
||||
'primary' => '#161514',
|
||||
'secondary' => '#AFADA7',
|
||||
),
|
||||
array(
|
||||
'title' => 'Olive',
|
||||
'primary' => '#FEFBF3',
|
||||
'secondary' => '#7F7E7A',
|
||||
),
|
||||
),
|
||||
'total_palettes' => 5,
|
||||
'slug' => 'amulet',
|
||||
'is_active' => 'amulet' === $current_theme_slug,
|
||||
'thumbnail_url' => 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/amulet/screenshot.png',
|
||||
'link_url' => 'https://wordpress.com/theme/amulet/',
|
||||
),
|
||||
array(
|
||||
'name' => 'Zaino',
|
||||
'price' => 'Free',
|
||||
'color_palettes' => array(
|
||||
array(
|
||||
'title' => 'Default',
|
||||
'primary' => '#202124',
|
||||
'secondary' => '#E3CBC0',
|
||||
),
|
||||
array(
|
||||
'title' => 'Aubergine',
|
||||
'primary' => '#1B1031',
|
||||
'secondary' => '#E1746D',
|
||||
),
|
||||
array(
|
||||
'title' => 'Block out',
|
||||
'primary' => '#FF5252',
|
||||
'secondary' => '#252525',
|
||||
),
|
||||
array(
|
||||
'title' => 'Canary',
|
||||
'primary' => '#FDFF85',
|
||||
'secondary' => '#353535',
|
||||
),
|
||||
),
|
||||
'total_palettes' => 11,
|
||||
'slug' => 'zaino',
|
||||
'is_active' => 'zaino' === $current_theme_slug,
|
||||
'thumbnail_url' => 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/zaino/screenshot.png',
|
||||
'link_url' => 'https://wordpress.com/theme/zaino/',
|
||||
),
|
||||
);
|
||||
|
||||
// To be implemented: Filter themes based on industry.
|
||||
if ( $industry ) {
|
||||
$filtered_themes = array_filter(
|
||||
$themes,
|
||||
function ( $theme ) use ( $industry ) {
|
||||
// Filter themes by industry.
|
||||
// Example: return $theme['industry'] === $industry;.
|
||||
return true;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$filtered_themes = $themes;
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'themes' => $filtered_themes,
|
||||
'_links' => array(
|
||||
'browse_all' => array(
|
||||
'href' => admin_url( 'themes.php' ),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the onboarding recommended themes response.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*
|
||||
* @param array $response The recommended themes response.
|
||||
* @param array $filtered_themes The filtered themes.
|
||||
* @param string $industry The industry to filter by (if provided).
|
||||
* @param string $currency The currency to convert prices to. (USD, AUD, CAD, EUR, GBP).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
return apply_filters(
|
||||
'__experimental_woocommerce_rest_get_recommended_themes',
|
||||
$response,
|
||||
$industry,
|
||||
$currency
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the schema, conforming to JSON Schema.
|
||||
*
|
||||
@@ -218,4 +416,82 @@ class OnboardingThemes extends \WC_REST_Data_Controller {
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recommended themes schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_recommended_item_schema() {
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'onboarding_theme',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'themes' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Theme Name',
|
||||
),
|
||||
'price' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Price',
|
||||
),
|
||||
'is_active' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => 'Whether theme is active',
|
||||
),
|
||||
'thumbnail_url' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Thumbnail URL',
|
||||
),
|
||||
'link_url' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Link URL for the theme',
|
||||
),
|
||||
'color_palettes' => array(
|
||||
'type' => 'array',
|
||||
'description' => 'Array of color palette objects',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'primary' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Primary color',
|
||||
),
|
||||
'secondary' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'Secondary color',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'_links' => array(
|
||||
'type' => 'object',
|
||||
'description' => 'Links related to this response',
|
||||
'properties' => array(
|
||||
'browse_all' => array(
|
||||
'type' => 'object',
|
||||
'description' => 'Link to browse all themes',
|
||||
'properties' => array(
|
||||
'href' => array(
|
||||
'type' => 'string',
|
||||
'description' => 'URL for browsing all themes',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ class Options extends \WC_REST_Data_Controller {
|
||||
'woocommerce_dimension_unit',
|
||||
'woocommerce_weight_unit',
|
||||
'woocommerce_product_editor_show_feedback_bar',
|
||||
'woocommerce_single_variation_notice_dismissed',
|
||||
'woocommerce_product_tour_modal_hidden',
|
||||
'woocommerce_block_product_tour_shown',
|
||||
'woocommerce_revenue_report_date_tour_shown',
|
||||
@@ -213,6 +214,10 @@ class Options extends \WC_REST_Data_Controller {
|
||||
'wcpay_welcome_page_incentives_dismissed',
|
||||
'wcpay_welcome_page_viewed_timestamp',
|
||||
'wcpay_welcome_page_exit_survey_more_info_needed_timestamp',
|
||||
'woocommerce_customize_store_onboarding_tour_hidden',
|
||||
'woocommerce_customize_store_ai_suggestions',
|
||||
'woocommerce_admin_customize_store_completed',
|
||||
'woocommerce_admin_customize_store_completed_theme_id',
|
||||
// WC Test helper options.
|
||||
'wc-admin-test-helper-rest-api-filters',
|
||||
'wc_admin_helper_feature_values',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
namespace Automattic\WooCommerce\Admin\API;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments;
|
||||
use Automattic\WooCommerce\Admin\PaymentMethodSuggestionsDataSourcePoller;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
|
||||
@@ -600,16 +601,16 @@ class Plugins extends \WC_REST_Data_Controller {
|
||||
return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error communicating with the WooPayments plugin.', 'woocommerce' ), 500 );
|
||||
}
|
||||
|
||||
$connect_url = add_query_arg(
|
||||
array(
|
||||
'wcpay-connect' => 'WCADMIN_PAYMENT_TASK',
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay-connect' ),
|
||||
),
|
||||
admin_url()
|
||||
);
|
||||
$args = WooCommercePayments::is_account_partially_onboarded() ? [
|
||||
'wcpay-login' => '1',
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay-login' ),
|
||||
] : [
|
||||
'wcpay-connect' => 'WCADMIN_PAYMENT_TASK',
|
||||
'_wpnonce' => wp_create_nonce( 'wcpay-connect' ),
|
||||
];
|
||||
|
||||
return( array(
|
||||
'connectUrl' => $connect_url,
|
||||
'connectUrl' => add_query_arg( $args, admin_url() ),
|
||||
) );
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ class DataStore extends SqlQuery {
|
||||
*/
|
||||
protected $cache_timeout = 3600;
|
||||
|
||||
/**
|
||||
* Cache identifier.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cache_key = '';
|
||||
|
||||
/**
|
||||
* Table used as a data store for this report.
|
||||
*
|
||||
|
||||
@@ -542,6 +542,87 @@ class Segmenter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for totals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
|
||||
*
|
||||
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $segmenting_dimension_name Name of the segmenting dimension.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $totals_query Array of SQL clauses for totals query.
|
||||
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_product_related_totals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $totals_query, $unique_orders_table ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for intervals where the segmenting property is bound to product (e.g. category, product_id, variation_id).
|
||||
*
|
||||
* @param array $segmenting_selections SELECT part of segmenting SQL query--one for 'product_level' and one for 'order_level'.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $segmenting_dimension_name Name of the segmenting dimension.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $intervals_query Array of SQL clauses for intervals query.
|
||||
* @param string $unique_orders_table Name of temporary SQL table that holds unique orders.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_product_related_intervals_segments( $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $intervals_query, $unique_orders_table ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for totals query where the segmenting property is bound to order (e.g. coupon or customer type).
|
||||
*
|
||||
* @param string $segmenting_select SELECT part of segmenting SQL query.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $totals_query Array of SQL clauses for intervals query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_order_related_totals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $totals_query ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for intervals query where the segmenting property is bound to order (e.g. coupon or customer type).
|
||||
*
|
||||
* @param string $segmenting_select SELECT part of segmenting SQL query.
|
||||
* @param string $segmenting_from FROM part of segmenting SQL query.
|
||||
* @param string $segmenting_where WHERE part of segmenting SQL query.
|
||||
* @param string $segmenting_groupby GROUP BY part of segmenting SQL query.
|
||||
* @param string $table_name Name of SQL table which is the stats table for orders.
|
||||
* @param array $intervals_query Array of SQL clauses for intervals query.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_order_related_intervals_segments( $segmenting_select, $segmenting_from, $segmenting_where, $segmenting_groupby, $table_name, $intervals_query ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of segments formatted for REST response.
|
||||
*
|
||||
* @param string $type Type of segments to return--'totals' or 'intervals'.
|
||||
* @param array $query_params SQL query parameter array.
|
||||
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_segments( $type, $query_params, $table_name ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate segments for segmenting property bound to product (e.g. category, product_id, variation_id).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\BlockTemplates;
|
||||
|
||||
/**
|
||||
* Interface for block containers.
|
||||
*/
|
||||
interface BlockContainerInterface extends BlockInterface, ContainerInterface {}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\BlockTemplates;
|
||||
|
||||
/**
|
||||
* Interface for block configuration used to specify blocks in BlockTemplate.
|
||||
*/
|
||||
interface BlockInterface {
|
||||
/**
|
||||
* Key for the block name in the block configuration.
|
||||
*/
|
||||
public const NAME_KEY = 'blockName';
|
||||
|
||||
/**
|
||||
* Key for the block ID in the block configuration.
|
||||
*/
|
||||
public const ID_KEY = 'id';
|
||||
|
||||
/**
|
||||
* Key for the internal order in the block configuration.
|
||||
*/
|
||||
public const ORDER_KEY = 'order';
|
||||
|
||||
/**
|
||||
* Key for the block attributes in the block configuration.
|
||||
*/
|
||||
public const ATTRIBUTES_KEY = 'attributes';
|
||||
|
||||
/**
|
||||
* Key for the block hide conditions in the block configuration.
|
||||
*/
|
||||
public const HIDE_CONDITIONS_KEY = 'hideConditions';
|
||||
|
||||
/**
|
||||
* Get the block name.
|
||||
*/
|
||||
public function get_name(): string;
|
||||
|
||||
/**
|
||||
* Get the block ID.
|
||||
*/
|
||||
public function get_id(): string;
|
||||
|
||||
/**
|
||||
* Get the block order.
|
||||
*/
|
||||
public function get_order(): int;
|
||||
|
||||
/**
|
||||
* Set the block order.
|
||||
*
|
||||
* @param int $order The block order.
|
||||
*/
|
||||
public function set_order( int $order );
|
||||
|
||||
/**
|
||||
* Get the block attributes.
|
||||
*/
|
||||
public function get_attributes(): array;
|
||||
|
||||
/**
|
||||
* Set the block attributes.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
*/
|
||||
public function set_attributes( array $attributes );
|
||||
|
||||
/**
|
||||
* Get the parent container that the block belongs to.
|
||||
*/
|
||||
public function &get_parent(): ContainerInterface;
|
||||
|
||||
/**
|
||||
* Get the root template that the block belongs to.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Remove the block from its parent.
|
||||
*/
|
||||
public function remove();
|
||||
|
||||
/**
|
||||
* Check if the block is detached from its parent or root template.
|
||||
*
|
||||
* @return bool True if the block is detached from its parent or root template.
|
||||
*/
|
||||
public function is_detached(): bool;
|
||||
|
||||
/**
|
||||
* Add a hide condition to the block.
|
||||
*
|
||||
* The hide condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden.
|
||||
* See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details.
|
||||
*
|
||||
* @param string $expression An expression, which if true, will hide the block.
|
||||
* @return string The key of the hide condition, which can be used to remove the hide condition.
|
||||
*/
|
||||
public function add_hide_condition( string $expression ): string;
|
||||
|
||||
/**
|
||||
* Remove a hide condition from the block.
|
||||
*
|
||||
* @param string $key The key of the hide condition to remove.
|
||||
*/
|
||||
public function remove_hide_condition( string $key );
|
||||
|
||||
/**
|
||||
* Get the hide conditions of the block.
|
||||
*/
|
||||
public function get_hide_conditions(): array;
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\BlockTemplates;
|
||||
|
||||
/**
|
||||
* Interface for block-based template.
|
||||
*/
|
||||
interface BlockTemplateInterface extends ContainerInterface {
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string;
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string;
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string;
|
||||
|
||||
/**
|
||||
* Get the template area.
|
||||
*/
|
||||
public function get_area(): string;
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param string $id_base The base to use when generating an ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_block_id( string $id_base ): string;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\BlockTemplates;
|
||||
|
||||
/**
|
||||
* Interface for block containers.
|
||||
*/
|
||||
interface ContainerInterface {
|
||||
/**
|
||||
* Get the root template that the block belongs to.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array;
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*/
|
||||
public function get_block( string $block_id ): ?BlockInterface;
|
||||
|
||||
/**
|
||||
* Removes a block from the container.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the block container is not an ancestor of the block.
|
||||
*/
|
||||
public function remove_block( string $block_id );
|
||||
|
||||
/**
|
||||
* Removes all blocks from the container.
|
||||
*/
|
||||
public function remove_blocks();
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class Package {
|
||||
*/
|
||||
public static function init() {
|
||||
// Avoid double initialization when the feature plugin is in use.
|
||||
if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
|
||||
if (defined( 'WC_ADMIN_VERSION_NUMBER' ) ) {
|
||||
self::$active_version = WC_ADMIN_VERSION_NUMBER;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ defined( 'ABSPATH' ) || exit;
|
||||
* A facade to allow deprecating an entire class.
|
||||
*/
|
||||
class DeprecatedClassFacade {
|
||||
|
||||
/**
|
||||
* The instance that this facade covers over.
|
||||
*
|
||||
@@ -32,6 +33,21 @@ class DeprecatedClassFacade {
|
||||
*/
|
||||
protected $instance;
|
||||
|
||||
/**
|
||||
* The name of the non-deprecated class that this facade covers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $facade_over_classname;
|
||||
|
||||
/**
|
||||
* The version that this class was deprecated in.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $deprecated_in_version = '';
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,13 @@ class Favorites {
|
||||
*/
|
||||
const META_NAME = 'navigation_favorites';
|
||||
|
||||
/**
|
||||
* Favorites instance.
|
||||
*
|
||||
* @var Favorites|null
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Get class instance.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,27 @@ class DeprecatedExtendedTask extends Task {
|
||||
*/
|
||||
public $id = '';
|
||||
|
||||
/**
|
||||
* Additional info.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $additional_info = '';
|
||||
|
||||
/**
|
||||
* Content.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $content = '';
|
||||
|
||||
/**
|
||||
* Whether the task is complete or not.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $is_complete = false;
|
||||
|
||||
/**
|
||||
* Snoozeable.
|
||||
*
|
||||
@@ -30,6 +51,35 @@ class DeprecatedExtendedTask extends Task {
|
||||
*/
|
||||
public $is_dismissable = false;
|
||||
|
||||
/**
|
||||
* Whether the store is capable of viewing the task.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $can_view = true;
|
||||
|
||||
/**
|
||||
* Level.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $level = 3;
|
||||
|
||||
/**
|
||||
* Time.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* Title.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $title = '';
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -177,6 +177,15 @@ abstract class Task {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_badge() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Level.
|
||||
*
|
||||
@@ -486,6 +495,7 @@ abstract class Task {
|
||||
'id' => $this->get_id(),
|
||||
'parentId' => $this->get_parent_id(),
|
||||
'title' => $this->get_title(),
|
||||
'badge' => $this->get_badge(),
|
||||
'canView' => $this->can_view(),
|
||||
'content' => $this->get_content(),
|
||||
'additionalInfo' => $this->get_additional_info(),
|
||||
|
||||
@@ -9,7 +9,6 @@ use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\ReviewShippingOptions;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\TourInAppMarketplace;
|
||||
/**
|
||||
* Task Lists class.
|
||||
*/
|
||||
@@ -36,13 +35,15 @@ class TaskLists {
|
||||
protected static $default_tasks_loaded = false;
|
||||
|
||||
/**
|
||||
* Array of default tasks.
|
||||
* The contents of this array is used in init_tasks() to run their init() methods.
|
||||
* If the classes do not have an init() method then nothing is executed.
|
||||
* Beyond that, adding tasks to this list has no effect, see init_default_lists() for the list of tasks.
|
||||
* that are added for each task list.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const DEFAULT_TASKS = array(
|
||||
'StoreDetails',
|
||||
'Purchase',
|
||||
'Products',
|
||||
'WooCommercePayments',
|
||||
'Payments',
|
||||
@@ -53,7 +54,6 @@ class TaskLists {
|
||||
'AdditionalPayments',
|
||||
'ReviewShippingOptions',
|
||||
'GetMobileApp',
|
||||
'TourInAppMarketplace',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -109,19 +109,30 @@ class TaskLists {
|
||||
*/
|
||||
public static function init_default_lists() {
|
||||
$tasks = array(
|
||||
'CustomizeStore',
|
||||
'StoreDetails',
|
||||
'Purchase',
|
||||
'Products',
|
||||
'Appearance',
|
||||
'WooCommercePayments',
|
||||
'Payments',
|
||||
'Tax',
|
||||
'Shipping',
|
||||
'Marketing',
|
||||
'Appearance',
|
||||
);
|
||||
|
||||
if ( Features::is_enabled( 'core-profiler' ) ) {
|
||||
array_shift( $tasks );
|
||||
$key = array_search( 'StoreDetails', $tasks, true );
|
||||
if ( false !== $key ) {
|
||||
unset( $tasks[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the old Personalize your store task if the new CustomizeStore is enabled.
|
||||
$task_to_remove = Features::is_enabled( 'customize-store' ) ? 'Appearance' : 'CustomizeStore';
|
||||
$store_customisation_task_index = array_search( $task_to_remove, $tasks, true );
|
||||
|
||||
if ( false !== $store_customisation_task_index ) {
|
||||
unset( $tasks[ $store_customisation_task_index ] );
|
||||
}
|
||||
|
||||
self::add_list(
|
||||
@@ -182,11 +193,6 @@ class TaskLists {
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! wp_is_mobile() ) { // Permit In-App Marketplace Tour on desktops only.
|
||||
$tour_task = new TourInAppMarketplace();
|
||||
self::add_task( 'extended', $tour_task );
|
||||
}
|
||||
|
||||
if ( has_filter( 'woocommerce_admin_experimental_onboarding_tasklists' ) ) {
|
||||
/**
|
||||
* Filter to override default task lists.
|
||||
@@ -431,7 +437,7 @@ class TaskLists {
|
||||
|
||||
foreach ( $submenu['woocommerce'] as $key => $menu_item ) {
|
||||
if ( 0 === strpos( $menu_item[0], _x( 'Home', 'Admin menu name', 'woocommerce' ) ) ) {
|
||||
$submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins remaining-tasks-badge count-' . esc_attr( $tasks_count ) . '">' . number_format_i18n( $tasks_count ) . '</span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$submenu['woocommerce'][ $key ][0] .= ' <span class="awaiting-mod update-plugins remaining-tasks-badge woocommerce-task-list-remaining-tasks-badge"><span class="count-' . esc_attr( $tasks_count ) . '">' . absint( $tasks_count ) . '</span></span>'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,12 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
class Appearance extends Task {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param TaskList $task_list Parent task list.
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct( $task_list ) {
|
||||
parent::__construct( $task_list );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'add_media_scripts' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_return_notice_script' ) );
|
||||
public function __construct() {
|
||||
if ( ! $this->is_complete() ) {
|
||||
add_action( 'load-theme-install.php', array( $this, 'mark_actioned' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,13 +37,7 @@ class Appearance extends Task {
|
||||
* @return string
|
||||
*/
|
||||
public function get_title() {
|
||||
if ( $this->get_parent_option( 'use_completed_title' ) === true ) {
|
||||
if ( $this->is_complete() ) {
|
||||
return __( 'You personalized your store', 'woocommerce' );
|
||||
}
|
||||
return __( 'Personalize your store', 'woocommerce' );
|
||||
}
|
||||
return __( 'Personalize my store', 'woocommerce' );
|
||||
return __( 'Choose your theme', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +47,7 @@ class Appearance extends Task {
|
||||
*/
|
||||
public function get_content() {
|
||||
return __(
|
||||
'Add your logo, create a homepage, and start designing your store.',
|
||||
"Choose a theme that best fits your brand's look and feel, then make it your own. Change the colors, add your logo, and create pages.",
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
@@ -70,68 +62,11 @@ class Appearance extends Task {
|
||||
}
|
||||
|
||||
/**
|
||||
* Addtional data.
|
||||
* Action label.
|
||||
*
|
||||
* @return array
|
||||
* @return string
|
||||
*/
|
||||
public function get_additional_data() {
|
||||
return array(
|
||||
'has_homepage' => self::has_homepage(),
|
||||
'has_products' => Products::has_products(),
|
||||
'stylesheet' => get_option( 'stylesheet' ),
|
||||
'theme_mods' => get_theme_mods(),
|
||||
'support_custom_logo' => false !== get_theme_support( 'custom-logo' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add media scripts for image uploader.
|
||||
*/
|
||||
public function add_media_scripts() {
|
||||
if ( ! PageController::is_admin_page() || ! $this->can_view() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_media();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a return to task list notice when completing the task.
|
||||
*
|
||||
* @param string $hook Page hook.
|
||||
*/
|
||||
public function possibly_add_return_notice_script( $hook ) {
|
||||
global $post;
|
||||
|
||||
if ( $hook !== 'post.php' || $post->post_type !== 'page' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->is_complete() || ! $this->is_active() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
WCAdminAssets::register_script( 'wp-admin-scripts', 'onboarding-homepage-notice', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the site has a homepage set up.
|
||||
*/
|
||||
public static function has_homepage() {
|
||||
if ( get_option( 'classic-editor-replace' ) === 'classic' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false );
|
||||
|
||||
if ( ! $homepage_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$post = get_post( $homepage_id );
|
||||
$completed = $post && $post->post_status === 'publish';
|
||||
|
||||
return $completed;
|
||||
public function get_action_label() {
|
||||
return __( 'Choose theme', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use Jetpack_Gutenberg;
|
||||
|
||||
/**
|
||||
* Customize Your Store Task
|
||||
*/
|
||||
class CustomizeStore extends Task {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param TaskList $task_list Parent task list.
|
||||
*/
|
||||
public function __construct( $task_list ) {
|
||||
parent::__construct( $task_list );
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_site_editor_scripts' ) );
|
||||
|
||||
// Use "switch_theme" instead of "after_switch_theme" because the latter is fired after the next WP load and we don't want to trigger action when switching theme to TT3 via onboarding theme API.
|
||||
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' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id() {
|
||||
return 'customize-store';
|
||||
}
|
||||
|
||||
/**
|
||||
* Title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_title() {
|
||||
return __( 'Customize your store ', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Time.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_time() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Task completion.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_complete() {
|
||||
return get_option( 'woocommerce_admin_customize_store_completed' ) === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Task visibility.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_view() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possibly add site editor scripts.
|
||||
*/
|
||||
public function possibly_add_site_editor_scripts() {
|
||||
$is_customize_store_pages = (
|
||||
isset( $_GET['page'] ) &&
|
||||
'wc-admin' === $_GET['page'] &&
|
||||
isset( $_GET['path'] ) &&
|
||||
str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' )
|
||||
);
|
||||
if ( ! $is_customize_store_pages ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See: https://github.com/WordPress/WordPress/blob/master/wp-admin/site-editor.php.
|
||||
if ( ! wp_is_block_theme() ) {
|
||||
wp_die( esc_html__( 'The theme you are currently using is not compatible.', 'woocommerce' ) );
|
||||
}
|
||||
global $editor_styles;
|
||||
|
||||
// Flag that we're loading the block editor.
|
||||
$current_screen = get_current_screen();
|
||||
$current_screen->is_block_editor( true );
|
||||
|
||||
// Default to is-fullscreen-mode to avoid jumps in the UI.
|
||||
add_filter(
|
||||
'admin_body_class',
|
||||
static function( $classes ) {
|
||||
return "$classes is-fullscreen-mode";
|
||||
}
|
||||
);
|
||||
|
||||
$block_editor_context = new \WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) );
|
||||
$indexed_template_types = array();
|
||||
foreach ( get_default_block_template_types() as $slug => $template_type ) {
|
||||
$template_type['slug'] = (string) $slug;
|
||||
$indexed_template_types[] = $template_type;
|
||||
}
|
||||
|
||||
$custom_settings = array(
|
||||
'siteUrl' => site_url(),
|
||||
'postsPerPage' => get_option( 'posts_per_page' ),
|
||||
'styles' => get_block_editor_theme_styles(),
|
||||
'defaultTemplateTypes' => $indexed_template_types,
|
||||
'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(),
|
||||
'supportsLayout' => wp_theme_has_theme_json(),
|
||||
'supportsTemplatePartsMode' => ! wp_is_block_theme() && current_theme_supports( 'block-template-parts' ),
|
||||
);
|
||||
|
||||
// Add additional back-compat patterns registered by `current_screen` et al.
|
||||
$custom_settings['__experimentalAdditionalBlockPatterns'] = \WP_Block_Patterns_Registry::get_instance()->get_all_registered( true );
|
||||
$custom_settings['__experimentalAdditionalBlockPatternCategories'] = \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered( true );
|
||||
|
||||
$editor_settings = get_block_editor_settings( $custom_settings, $block_editor_context );
|
||||
$active_global_styles_id = \WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
|
||||
$active_theme = get_stylesheet();
|
||||
$preload_paths = array(
|
||||
array( '/wp/v2/media', 'OPTIONS' ),
|
||||
'/wp/v2/types?context=view',
|
||||
'/wp/v2/types/wp_template?context=edit',
|
||||
'/wp/v2/types/wp_template-part?context=edit',
|
||||
'/wp/v2/templates?context=edit&per_page=-1',
|
||||
'/wp/v2/template-parts?context=edit&per_page=-1',
|
||||
'/wp/v2/themes?context=edit&status=active',
|
||||
'/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit',
|
||||
'/wp/v2/global-styles/' . $active_global_styles_id,
|
||||
'/wp/v2/global-styles/themes/' . $active_theme,
|
||||
);
|
||||
|
||||
block_editor_rest_api_preload( $preload_paths, $block_editor_context );
|
||||
|
||||
wp_add_inline_script(
|
||||
'wp-blocks',
|
||||
sprintf(
|
||||
'window.wcBlockSettings = %s;',
|
||||
wp_json_encode( $editor_settings )
|
||||
)
|
||||
);
|
||||
|
||||
// Preload server-registered block schemas.
|
||||
wp_add_inline_script(
|
||||
'wp-blocks',
|
||||
'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
|
||||
);
|
||||
|
||||
wp_add_inline_script(
|
||||
'wp-blocks',
|
||||
sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( isset( $editor_settings['blockCategories'] ) ? $editor_settings['blockCategories'] : array() ) ),
|
||||
'after'
|
||||
);
|
||||
|
||||
wp_enqueue_script( 'wp-editor' );
|
||||
wp_enqueue_script( 'wp-format-library' ); // Not sure if this is needed.
|
||||
wp_enqueue_script( 'wp-router' );
|
||||
wp_enqueue_style( 'wp-editor' );
|
||||
wp_enqueue_style( 'wp-edit-site' );
|
||||
wp_enqueue_style( 'wp-format-library' );
|
||||
wp_enqueue_media();
|
||||
|
||||
if (
|
||||
current_theme_supports( 'wp-block-styles' ) &&
|
||||
( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 )
|
||||
) {
|
||||
wp_enqueue_style( 'wp-block-library-theme' );
|
||||
}
|
||||
/** This action is documented in wp-admin/edit-form-blocks.php
|
||||
*
|
||||
* @since 8.0.3
|
||||
*/
|
||||
do_action( 'enqueue_block_editor_assets' );
|
||||
|
||||
// Load Jetpack's block editor assets because they are not enqueued by default.
|
||||
if ( class_exists( 'Jetpack_Gutenberg' ) ) {
|
||||
Jetpack_Gutenberg::enqueue_block_editor_assets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark task as complete.
|
||||
*/
|
||||
public function mark_task_as_complete() {
|
||||
update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
|
||||
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
@@ -79,8 +80,7 @@ class ExperimentalShippingRecommendation extends Task {
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_plugins_active() {
|
||||
return PluginsHelper::is_plugin_active( 'woocommerce-services' ) &&
|
||||
PluginsHelper::is_plugin_active( 'jetpack' );
|
||||
return PluginsHelper::is_plugin_active( 'woocommerce-services' );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,9 +89,8 @@ class ExperimentalShippingRecommendation extends Task {
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_jetpack_connected() {
|
||||
if ( class_exists( '\Jetpack' ) && is_callable( '\Jetpack::is_connection_ready' ) ) {
|
||||
return \Jetpack::is_connection_ready();
|
||||
}
|
||||
return false;
|
||||
$jetpack_connection_manager = new Manager( 'woocommerce' );
|
||||
|
||||
return $jetpack_connection_manager->is_connected() && $jetpack_connection_manager->has_connected_owner();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class Tax extends Task {
|
||||
public function get_content() {
|
||||
return self::can_use_automated_taxes()
|
||||
? __(
|
||||
'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.',
|
||||
'Good news! WooCommerce Tax can automate your sales tax calculations for you.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
@@ -115,7 +115,10 @@ class Tax extends Task {
|
||||
*/
|
||||
public function is_complete() {
|
||||
if ( $this->is_complete_result === null ) {
|
||||
$this->is_complete_result = get_option( 'wc_connect_taxes_enabled' ) ||
|
||||
$wc_connect_taxes_enabled = get_option( 'wc_connect_taxes_enabled' );
|
||||
$is_wc_connect_taxes_enabled = ( $wc_connect_taxes_enabled === 'yes' ) || ( $wc_connect_taxes_enabled === true ); // seems that in some places boolean is used, and other places 'yes' | 'no' is used
|
||||
|
||||
$this->is_complete_result = $is_wc_connect_taxes_enabled ||
|
||||
count( TaxDataStore::get_taxes( array() ) ) > 0 ||
|
||||
get_option( 'woocommerce_no_sales_tax' ) !== false;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class TourInAppMarketplace extends Task {
|
||||
*/
|
||||
public function get_title() {
|
||||
return __(
|
||||
'Discover where to find powerful store add-ons and integrations, with a WooCommerce Marketplace tour',
|
||||
'Discover ways of extending your store with a tour of the Woo Marketplace',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ class TourInAppMarketplace extends Task {
|
||||
* @return string
|
||||
*/
|
||||
public function get_action_url() {
|
||||
return admin_url( 'admin.php?page=wc-addons&tutorial=true' );
|
||||
return admin_url( 'admin.php?page=wc-admin&path=%2Fextensions&tutorial=true' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\Init as Suggestions;
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;
|
||||
|
||||
/**
|
||||
* WooCommercePayments Task
|
||||
@@ -37,6 +37,21 @@ class WooCommercePayments extends Task {
|
||||
return __( 'Set up WooPayments', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_badge() {
|
||||
/**
|
||||
* Filter WooPayments onboarding task badge.
|
||||
*
|
||||
* @param string $badge Badge content.
|
||||
* @since 8.2.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_admin_woopayments_onboarding_task_badge', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Content.
|
||||
*
|
||||
@@ -73,6 +88,13 @@ 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>',
|
||||
'woocommerce'
|
||||
@@ -86,7 +108,7 @@ class WooCommercePayments extends Task {
|
||||
*/
|
||||
public function is_complete() {
|
||||
if ( null === $this->is_complete_result ) {
|
||||
$this->is_complete_result = self::is_connected();
|
||||
$this->is_complete_result = self::is_connected() && ! self::is_account_partially_onboarded();
|
||||
}
|
||||
|
||||
return $this->is_complete_result;
|
||||
@@ -102,8 +124,7 @@ class WooCommercePayments extends Task {
|
||||
|
||||
return ! $payments->is_complete() && // Do not re-display the task if the "add payments" task has already been completed.
|
||||
self::is_installed() &&
|
||||
self::is_supported() &&
|
||||
! self::is_connected();
|
||||
self::is_supported();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,6 +167,23 @@ class WooCommercePayments extends Task {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce Payments needs setup.
|
||||
* Errored data or payments not enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_account_partially_onboarded() {
|
||||
if ( class_exists( '\WC_Payments' ) ) {
|
||||
$wc_payments_gateway = \WC_Payments::get_gateway();
|
||||
return method_exists( $wc_payments_gateway, 'is_account_partially_onboarded' )
|
||||
? $wc_payments_gateway->is_account_partially_onboarded()
|
||||
: false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the store is in a supported country.
|
||||
*
|
||||
|
||||
@@ -904,7 +904,7 @@ class DefaultPaymentGateways {
|
||||
* @return array Array of countries.
|
||||
*/
|
||||
public static function get_wcpay_countries() {
|
||||
return array( 'US', 'PR', 'AU', 'CA', 'CY', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'IE', 'IT', 'LU', 'LT', 'LV', 'NO', 'NZ', 'MT', 'AT', 'BE', 'NL', 'PL', 'PT', 'CH', 'HK', 'SI', 'SK', 'SG', 'BG', 'CZ', 'HR', 'HU', 'RO', 'SE' );
|
||||
return array( 'US', 'PR', 'AU', 'CA', 'CY', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GB', 'GR', 'IE', 'IT', 'LU', 'LT', 'LV', 'NO', 'NZ', 'MT', 'AT', 'BE', 'NL', 'PL', 'PT', 'CH', 'HK', 'SI', 'SK', 'SG', 'BG', 'CZ', 'HR', 'HU', 'RO', 'SE', 'JP', 'AE' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@ class Init {
|
||||
*/
|
||||
public function __construct() {
|
||||
PaymentGatewaysController::init();
|
||||
add_action( 'update_option_woocommerce_default_country', array( $this, 'delete_specs_transient' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,46 +11,66 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
* Product block registration and style registration functionality.
|
||||
*/
|
||||
class BlockRegistry {
|
||||
/**
|
||||
* The directory where blocks are stored after build.
|
||||
*/
|
||||
const BLOCKS_DIR = 'product-editor/blocks';
|
||||
|
||||
/**
|
||||
* Array of all available product blocks.
|
||||
* Generic blocks directory.
|
||||
*/
|
||||
const PRODUCT_BLOCKS = [
|
||||
const GENERIC_BLOCKS_DIR = 'product-editor/blocks/generic';
|
||||
/**
|
||||
* Product fields blocks directory.
|
||||
*/
|
||||
const PRODUCT_FIELDS_BLOCKS_DIR = 'product-editor/blocks/product-fields';
|
||||
/**
|
||||
* Array of all available generic blocks.
|
||||
*/
|
||||
const GENERIC_BLOCKS = [
|
||||
'woocommerce/conditional',
|
||||
'woocommerce/product-category-field',
|
||||
'woocommerce/product-checkbox-field',
|
||||
'woocommerce/product-collapsible',
|
||||
'woocommerce/product-radio-field',
|
||||
'woocommerce/product-pricing-field',
|
||||
'woocommerce/product-section',
|
||||
'woocommerce/product-tab',
|
||||
'woocommerce/product-toggle-field',
|
||||
'woocommerce/product-taxonomy-field',
|
||||
'woocommerce/product-text-field',
|
||||
'woocommerce/product-number-field',
|
||||
];
|
||||
|
||||
/**
|
||||
* Array of all available product fields blocks.
|
||||
*/
|
||||
const PRODUCT_FIELDS_BLOCKS = [
|
||||
'woocommerce/product-catalog-visibility-field',
|
||||
'woocommerce/product-description-field',
|
||||
'woocommerce/product-downloads-field',
|
||||
'woocommerce/product-images-field',
|
||||
'woocommerce/product-inventory-email-field',
|
||||
'woocommerce/product-sku-field',
|
||||
'woocommerce/product-name-field',
|
||||
'woocommerce/product-pricing-field',
|
||||
'woocommerce/product-radio-field',
|
||||
'woocommerce/product-regular-price-field',
|
||||
'woocommerce/product-sale-price-field',
|
||||
'woocommerce/product-schedule-sale-fields',
|
||||
'woocommerce/product-section',
|
||||
'woocommerce/product-shipping-class-field',
|
||||
'woocommerce/product-shipping-dimensions-fields',
|
||||
'woocommerce/product-summary-field',
|
||||
'woocommerce/product-tab',
|
||||
'woocommerce/product-tag-field',
|
||||
'woocommerce/product-inventory-quantity-field',
|
||||
'woocommerce/product-toggle-field',
|
||||
'woocommerce/product-variation-items-field',
|
||||
'woocommerce/product-variations-fields',
|
||||
'woocommerce/product-password-field',
|
||||
'woocommerce/product-has-variations-notice',
|
||||
'woocommerce/product-single-variation-notice',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a file path for a given block file.
|
||||
*
|
||||
* @param string $path File path.
|
||||
* @param string $dir File directory.
|
||||
*/
|
||||
private function get_file_path( $path ) {
|
||||
return WC_ABSPATH . WCAdminAssets::get_path( 'js' ) . trailingslashit( self::BLOCKS_DIR ) . $path;
|
||||
private function get_file_path( $path, $dir ) {
|
||||
return WC_ABSPATH . WCAdminAssets::get_path( 'js' ) . trailingslashit( $dir ) . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,8 +85,11 @@ class BlockRegistry {
|
||||
* Register all the product blocks.
|
||||
*/
|
||||
private function register_product_blocks() {
|
||||
foreach ( self::PRODUCT_BLOCKS as $block_name ) {
|
||||
$this->register_block( $block_name );
|
||||
foreach ( self::PRODUCT_FIELDS_BLOCKS as $block_name ) {
|
||||
$this->register_block( $block_name, self::PRODUCT_FIELDS_BLOCKS_DIR );
|
||||
}
|
||||
foreach ( self::GENERIC_BLOCKS as $block_name ) {
|
||||
$this->register_block( $block_name, self::GENERIC_BLOCKS_DIR );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,16 +126,60 @@ class BlockRegistry {
|
||||
return $block_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the attributes of a block by adding attributes that are used by the product editor.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
*/
|
||||
private function augment_attributes( $attributes ) {
|
||||
// Note: If you modify this function, also update the client-side
|
||||
// registerWooBlockType function in @woocommerce/block-templates.
|
||||
return array_merge(
|
||||
$attributes,
|
||||
[
|
||||
'_templateBlockId' => [
|
||||
'type' => 'string',
|
||||
'__experimentalRole' => 'content',
|
||||
],
|
||||
'_templateBlockOrder' => [
|
||||
'type' => 'integer',
|
||||
'__experimentalRole' => 'content',
|
||||
],
|
||||
'_templateBlockHideConditions' => [
|
||||
'type' => 'array',
|
||||
'__experimentalRole' => 'content',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the uses_context of a block by adding attributes that are used by the product editor.
|
||||
*
|
||||
* @param array $uses_context Block uses_context.
|
||||
*/
|
||||
private function augment_uses_context( $uses_context ) {
|
||||
// Note: If you modify this function, also update the client-side
|
||||
// registerProductEditorBlockType function in @woocommerce/product-editor.
|
||||
return array_merge(
|
||||
isset( $uses_context ) ? $uses_context : [],
|
||||
[
|
||||
'postType',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single block.
|
||||
*
|
||||
* @param string $block_name Block name.
|
||||
* @param string $block_dir Block directory.
|
||||
*
|
||||
* @return WP_Block_Type|false The registered block type on success, or false on failure.
|
||||
*/
|
||||
private function register_block( $block_name ) {
|
||||
private function register_block( $block_name, $block_dir ) {
|
||||
$block_name = $this->remove_block_prefix( $block_name );
|
||||
$block_json_file = $this->get_file_path( $block_name . '/block.json' );
|
||||
$block_json_file = $this->get_file_path( $block_name . '/block.json', $block_dir );
|
||||
|
||||
if ( ! file_exists( $block_json_file ) ) {
|
||||
return false;
|
||||
@@ -130,7 +197,13 @@ class BlockRegistry {
|
||||
$registry->unregister( $metadata['name'] );
|
||||
}
|
||||
|
||||
return register_block_type_from_metadata( $block_json_file );
|
||||
return register_block_type_from_metadata(
|
||||
$block_json_file,
|
||||
[
|
||||
'attributes' => $this->augment_attributes( isset( $metadata['attributes'] ) ? $metadata['attributes'] : [] ),
|
||||
'uses_context' => $this->augment_uses_context( isset( $metadata['usesContext'] ) ? $metadata['usesContext'] : [] ),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\TransientNotices;
|
||||
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
|
||||
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductVariationTemplate;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Loader;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
|
||||
use WP_Block_Editor_Context;
|
||||
|
||||
/**
|
||||
@@ -38,16 +39,27 @@ class Init {
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
array_push( $this->supported_post_types, 'variable' );
|
||||
}
|
||||
|
||||
$this->redirection_controller = new RedirectionController( $this->supported_post_types );
|
||||
|
||||
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||
// Register the product block template.
|
||||
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
|
||||
$template_registry->register( new SimpleProductTemplate() );
|
||||
$template_registry->register( new ProductVariationTemplate() );
|
||||
|
||||
if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_styles' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
|
||||
add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
|
||||
}
|
||||
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_product_template' ) );
|
||||
add_filter( 'woocommerce_register_post_type_product_variation', array( $this, 'enable_rest_api_for_product_variation' ) );
|
||||
|
||||
add_action( 'current_screen', array( $this, 'set_current_screen_to_block_editor_if_wc_admin' ) );
|
||||
|
||||
@@ -68,12 +80,16 @@ class Init {
|
||||
}
|
||||
$post_type_object = get_post_type_object( 'product' );
|
||||
$block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );
|
||||
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
|
||||
|
||||
$editor_settings = array();
|
||||
if ( ! empty( $post_type_object->template ) ) {
|
||||
$editor_settings['template'] = $post_type_object->template;
|
||||
$editor_settings['templateLock'] = ! empty( $post_type_object->template_lock ) ? $post_type_object->template_lock : false;
|
||||
$editor_settings['__unstableResolvedAssets'] = $this->get_resolved_assets();
|
||||
$editor_settings['template'] = $post_type_object->template;
|
||||
$editor_settings['templateLock'] = ! empty( $post_type_object->template_lock ) ? $post_type_object->template_lock : false;
|
||||
$editor_settings['templates'] = array(
|
||||
'product' => $post_type_object->template,
|
||||
'product_variation' => $template_registry->get_registered( 'product-variation' )->get_formatted_template(),
|
||||
);
|
||||
}
|
||||
|
||||
$editor_settings = get_block_editor_settings( $editor_settings, $block_editor_context );
|
||||
@@ -92,6 +108,7 @@ class Init {
|
||||
'before'
|
||||
);
|
||||
wp_tinymce_inline_scripts();
|
||||
wp_enqueue_media();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,105 +161,6 @@ class Init {
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved assets needed for the iframe editor.
|
||||
*
|
||||
* @return array Styles and scripts.
|
||||
*/
|
||||
private function get_resolved_assets() {
|
||||
if ( function_exists( 'gutenberg_resolve_assets_override' ) ) {
|
||||
return gutenberg_resolve_assets_override();
|
||||
}
|
||||
|
||||
global $pagenow;
|
||||
|
||||
$script_handles = array(
|
||||
'wp-polyfill',
|
||||
);
|
||||
// Note for core merge: only 'wp-edit-blocks' should be in this array.
|
||||
$style_handles = array(
|
||||
'wp-edit-blocks',
|
||||
);
|
||||
|
||||
if ( current_theme_supports( 'wp-block-styles' ) ) {
|
||||
$style_handles[] = 'wp-block-library-theme';
|
||||
}
|
||||
|
||||
if ( 'widgets.php' === $pagenow || 'customize.php' === $pagenow ) {
|
||||
$style_handles[] = 'wp-widgets';
|
||||
$style_handles[] = 'wp-edit-widgets';
|
||||
}
|
||||
|
||||
$block_registry = \WP_Block_Type_Registry::get_instance();
|
||||
|
||||
foreach ( $block_registry->get_all_registered() as $block_type ) {
|
||||
// In older WordPress versions, like 6.0, these properties are not defined.
|
||||
if ( isset( $block_type->style_handles ) && is_array( $block_type->style_handles ) ) {
|
||||
$style_handles = array_merge( $style_handles, $block_type->style_handles );
|
||||
}
|
||||
|
||||
if ( isset( $block_type->editor_style_handles ) && is_array( $block_type->editor_style_handles ) ) {
|
||||
$style_handles = array_merge( $style_handles, $block_type->editor_style_handles );
|
||||
}
|
||||
|
||||
if ( isset( $block_type->script_handles ) && is_array( $block_type->script_handles ) ) {
|
||||
$script_handles = array_merge( $script_handles, $block_type->script_handles );
|
||||
}
|
||||
}
|
||||
|
||||
$style_handles = array_unique( $style_handles );
|
||||
$done = wp_styles()->done;
|
||||
|
||||
ob_start();
|
||||
|
||||
// We do not need reset styles for the iframed editor.
|
||||
wp_styles()->done = array( 'wp-reset-editor-styles' );
|
||||
wp_styles()->do_items( $style_handles );
|
||||
wp_styles()->done = $done;
|
||||
|
||||
$styles = ob_get_clean();
|
||||
|
||||
$script_handles = array_unique( $script_handles );
|
||||
$done = wp_scripts()->done;
|
||||
|
||||
ob_start();
|
||||
|
||||
wp_scripts()->done = array();
|
||||
wp_scripts()->do_items( $script_handles );
|
||||
wp_scripts()->done = $done;
|
||||
|
||||
$scripts = ob_get_clean();
|
||||
|
||||
/*
|
||||
* Generate font @font-face styles for the site editor iframe.
|
||||
* Use the registered font families for printing.
|
||||
*/
|
||||
if ( class_exists( '\WP_Fonts' ) ) {
|
||||
$wp_fonts = wp_fonts();
|
||||
$registered = $wp_fonts->get_registered_font_families();
|
||||
if ( ! empty( $registered ) ) {
|
||||
$queue = $wp_fonts->queue;
|
||||
$done = $wp_fonts->done;
|
||||
|
||||
$wp_fonts->done = array();
|
||||
$wp_fonts->queue = $registered;
|
||||
|
||||
ob_start();
|
||||
$wp_fonts->do_items();
|
||||
$styles .= ob_get_clean();
|
||||
|
||||
// Reset the Web Fonts API.
|
||||
$wp_fonts->done = $done;
|
||||
$wp_fonts->queue = $queue;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'styles' => $styles,
|
||||
'scripts' => $scripts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue styles needed for the rich text editor.
|
||||
*
|
||||
@@ -251,488 +169,47 @@ class Init {
|
||||
*/
|
||||
public function add_product_template( $args ) {
|
||||
if ( ! isset( $args['template'] ) ) {
|
||||
$args['template_lock'] = 'all';
|
||||
$args['template'] = array(
|
||||
array(
|
||||
'woocommerce/product-tab',
|
||||
array(
|
||||
'id' => 'general',
|
||||
'title' => __( 'General', 'woocommerce' ),
|
||||
'order' => 10,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Basic details', 'woocommerce' ),
|
||||
'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-name-field',
|
||||
array(
|
||||
'name' => 'Product name',
|
||||
'autoFocus' => true,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-summary-field',
|
||||
),
|
||||
array(
|
||||
'core/columns',
|
||||
array(),
|
||||
array(
|
||||
array(
|
||||
'core/column',
|
||||
array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-regular-price-field',
|
||||
array(
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'List price', 'woocommerce' ),
|
||||
'help' => __( 'Manage more settings in <PricingTab>Pricing.</PricingTab>', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'core/column',
|
||||
array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-sale-price-field',
|
||||
array(
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Description', 'woocommerce' ),
|
||||
'description' => __( 'What makes this product unique? What are its most important features? Enrich the product page by adding rich content using blocks.', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-description-field',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Images', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
|
||||
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
|
||||
'<a href="http://woocommerce.com/#" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-images-field',
|
||||
array(
|
||||
'images' => array(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Organization & visibility', 'woocommerce' ),
|
||||
'description' => __( 'Help customers find this product by assigning it to categories or featuring it across your sales channels.', 'woocommerce' ),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-category-field',
|
||||
array(
|
||||
'name' => 'categories',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Attributes', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Attributes guide link opening tag. %2$s: Attributes guide link closing tag.*/
|
||||
__( 'Add descriptive pieces of information that customers can use to filter and search for this product. %1$sLearn more%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/managing-product-taxonomies/#product-attributes" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-attributes-field',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-tab',
|
||||
array(
|
||||
'id' => 'pricing',
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
'order' => 20,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
|
||||
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(),
|
||||
array(
|
||||
array(
|
||||
'core/columns',
|
||||
array(),
|
||||
array(
|
||||
array(
|
||||
'core/column',
|
||||
array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-regular-price-field',
|
||||
array(
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'List price', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'core/column',
|
||||
array(
|
||||
'templateLock' => 'all',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-sale-price-field',
|
||||
array(
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-schedule-sale-fields',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-radio-field',
|
||||
array(
|
||||
'title' => __( 'Charge sales tax on', 'woocommerce' ),
|
||||
'property' => 'tax_status',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'Product and shipping', 'woocommerce' ),
|
||||
'value' => 'taxable',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Only shipping', 'woocommerce' ),
|
||||
'value' => 'shipping',
|
||||
),
|
||||
array(
|
||||
'label' => __( "Don't charge tax", 'woocommerce' ),
|
||||
'value' => 'none',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-collapsible',
|
||||
array(
|
||||
'toggleText' => __( 'Advanced', 'woocommerce' ),
|
||||
'initialCollapsed' => true,
|
||||
'persistRender' => true,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-radio-field',
|
||||
array(
|
||||
'title' => __( 'Tax class', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'Standard', 'woocommerce' ),
|
||||
'value' => '',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Reduced rate', 'woocommerce' ),
|
||||
'value' => 'reduced-rate',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Zero rate', 'woocommerce' ),
|
||||
'value' => 'zero-rate',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-tab',
|
||||
array(
|
||||
'id' => 'inventory',
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
'order' => 30,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Inventory settings link opening tag. %2$s: Inventory settings link closing tag.*/
|
||||
__( 'Set up and manage inventory for this product, including status and available quantity. %1$sManage store inventory settings%2$s', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-sku-field',
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-toggle-field',
|
||||
array(
|
||||
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
|
||||
'property' => 'manage_stock',
|
||||
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/conditional',
|
||||
array(
|
||||
'mustMatch' => array(
|
||||
'manage_stock' => array( true ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-inventory-quantity-field',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/conditional',
|
||||
array(
|
||||
'mustMatch' => array(
|
||||
'manage_stock' => array( false ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-radio-field',
|
||||
array(
|
||||
'title' => __( 'Stock status', 'woocommerce' ),
|
||||
'property' => 'stock_status',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'In stock', 'woocommerce' ),
|
||||
'value' => 'instock',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Out of stock', 'woocommerce' ),
|
||||
'value' => 'outofstock',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'On backorder', 'woocommerce' ),
|
||||
'value' => 'onbackorder',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-collapsible',
|
||||
array(
|
||||
'toggleText' => __( 'Advanced', 'woocommerce' ),
|
||||
'initialCollapsed' => true,
|
||||
'persistRender' => true,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'blockGap' => 'unit-40',
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/conditional',
|
||||
array(
|
||||
'mustMatch' => array(
|
||||
'manage_stock' => array( true ),
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-radio-field',
|
||||
array(
|
||||
'title' => __( 'When out of stock', 'woocommerce' ),
|
||||
'property' => 'backorders',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'Allow purchases', 'woocommerce' ),
|
||||
'value' => 'yes',
|
||||
),
|
||||
array(
|
||||
'label' => __(
|
||||
'Allow purchases, but notify customers',
|
||||
'woocommerce'
|
||||
),
|
||||
'value' => 'notify',
|
||||
),
|
||||
array(
|
||||
'label' => __( "Don't allow purchases", 'woocommerce' ),
|
||||
'value' => 'no',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-inventory-email-field',
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-checkbox-field',
|
||||
array(
|
||||
'title' => __(
|
||||
'Restrictions',
|
||||
'woocommerce'
|
||||
),
|
||||
'label' => __(
|
||||
'Limit purchases to 1 item per order',
|
||||
'woocommerce'
|
||||
),
|
||||
'property' => 'sold_individually',
|
||||
'tooltip' => __(
|
||||
'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.',
|
||||
'woocommerce'
|
||||
),
|
||||
),
|
||||
),
|
||||
// Get the template from the registry.
|
||||
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
|
||||
$template = $template_registry->get_registered( 'simple-product' );
|
||||
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-tab',
|
||||
array(
|
||||
'id' => 'shipping',
|
||||
'title' => __( 'Shipping', 'woocommerce' ),
|
||||
'order' => 40,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-section',
|
||||
array(
|
||||
'title' => __( 'Fees & dimensions', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
|
||||
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-shipping-class-field',
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-shipping-dimensions-fields',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
array_push(
|
||||
$args['template'],
|
||||
array(
|
||||
'woocommerce/product-tab',
|
||||
array(
|
||||
'id' => 'variations',
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
'order' => 40,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-variations-fields',
|
||||
array(
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Sell your product in multiple variations like size or color. strong opening tag. %2$s: Sell your product in multiple variations like size or color. strong closing tag.*/
|
||||
__( '%1$sSell your product in multiple variations like size or color.%2$s Get started by adding options for the buyers to choose on the product page.', 'woocommerce' ),
|
||||
'<strong>',
|
||||
'</strong>'
|
||||
),
|
||||
),
|
||||
array(),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
if ( isset( $template ) ) {
|
||||
$args['template_lock'] = 'all';
|
||||
$args['template'] = $template->get_formatted_template();
|
||||
}
|
||||
}
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables variation post type in REST API.
|
||||
*
|
||||
* @param array $args Array of post type arguments.
|
||||
* @return array Array of post type arguments.
|
||||
*/
|
||||
public function enable_rest_api_for_product_variation( $args ) {
|
||||
$args['show_in_rest'] = true;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds fields so that we can store user preferences for the variations block.
|
||||
*
|
||||
* @param array $user_data_fields User data fields.
|
||||
* @return array
|
||||
*/
|
||||
public function add_user_data_fields( $user_data_fields ) {
|
||||
return array_merge(
|
||||
$user_data_fields,
|
||||
array(
|
||||
'variable_product_block_tour_shown',
|
||||
'product_block_variable_options_notice_dismissed',
|
||||
'variable_items_without_price_notice_dismissed',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current screen to the block editor if a wc-admin page.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\AbstractBlockTemplate;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
abstract class AbstractProductFormTemplate extends AbstractBlockTemplate implements ProductFormTemplateInterface {
|
||||
/**
|
||||
* Get the template area.
|
||||
*/
|
||||
public function get_area(): string {
|
||||
return 'product-form';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group block by ID.
|
||||
*
|
||||
* @param string $group_id The group block ID.
|
||||
* @throws \UnexpectedValueException If block is not of type GroupInterface.
|
||||
*/
|
||||
public function get_group_by_id( string $group_id ): ?GroupInterface {
|
||||
$group = $this->get_block( $group_id );
|
||||
if ( $group && ! $group instanceof GroupInterface ) {
|
||||
throw new \UnexpectedValueException( 'Block with specified ID is not a group.' );
|
||||
}
|
||||
return $group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section block by ID.
|
||||
*
|
||||
* @param string $section_id The section block ID.
|
||||
* @throws \UnexpectedValueException If block is not of type SectionInterface.
|
||||
*/
|
||||
public function get_section_by_id( string $section_id ): ?SectionInterface {
|
||||
$section = $this->get_block( $section_id );
|
||||
if ( $section && ! $section instanceof SectionInterface ) {
|
||||
throw new \UnexpectedValueException( 'Block with specified ID is not a section.' );
|
||||
}
|
||||
return $section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block block ID.
|
||||
*/
|
||||
public function get_block_by_id( string $block_id ): ?BlockInterface {
|
||||
return $this->get_block( $block_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom block type to this template.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_group( array $block_config ): GroupInterface {
|
||||
$block = new Group( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Group Block class.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Class for Group block.
|
||||
*/
|
||||
class Group extends ProductBlock implements GroupInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Group Block constructor.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param ContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
* @throws \InvalidArgumentException If blockName key and value are passed into block configuration.
|
||||
*/
|
||||
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
if ( ! empty( $config['blockName'] ) ) {
|
||||
throw new \InvalidArgumentException( 'Unexpected key "blockName", this defaults to "woocommerce/product-tab".' );
|
||||
}
|
||||
if ( $config['id'] && ( empty( $config['attributes'] ) || empty( $config['attributes']['id'] ) ) ) {
|
||||
$config['attributes'] = empty( $config['attributes'] ) ? [] : $config['attributes'];
|
||||
$config['attributes']['id'] = $config['id'];
|
||||
}
|
||||
parent::__construct( array_merge( array( 'blockName' => 'woocommerce/product-tab' ), $config ), $root_template, $parent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a section block type to this template.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_section( array $block_config ): SectionInterface {
|
||||
$block = new Section( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
|
||||
/**
|
||||
* Interface for group containers, which contain sections and blocks.
|
||||
*/
|
||||
interface GroupInterface extends BlockContainerInterface {
|
||||
|
||||
/**
|
||||
* Adds a new section to the group
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
* @return SectionInterface new block section.
|
||||
*/
|
||||
public function add_section( array $block_config ): SectionInterface;
|
||||
|
||||
/**
|
||||
* Adds a new block to the group.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Product Block class.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\AbstractBlock;
|
||||
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Class for Product block.
|
||||
*/
|
||||
class ProductBlock extends AbstractBlock implements ContainerInterface {
|
||||
use BlockContainerTrait;
|
||||
/**
|
||||
* Adds block to the section block.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function &add_block( array $block_config ): BlockInterface {
|
||||
$block = new ProductBlock( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Interface for block containers.
|
||||
*/
|
||||
interface ProductFormTemplateInterface extends BlockTemplateInterface {
|
||||
|
||||
/**
|
||||
* Adds a new group block.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
* @return BlockInterface new block section.
|
||||
*/
|
||||
public function add_group( array $block_config ): GroupInterface;
|
||||
|
||||
/**
|
||||
* Gets Group block by id.
|
||||
*
|
||||
* @param string $group_id group id.
|
||||
* @return GroupInterface|null
|
||||
*/
|
||||
public function get_group_by_id( string $group_id ): ?GroupInterface;
|
||||
|
||||
/**
|
||||
* Gets Section block by id.
|
||||
*
|
||||
* @param string $section_id section id.
|
||||
* @return SectionInterface|null
|
||||
*/
|
||||
public function get_section_by_id( string $section_id ): ?SectionInterface;
|
||||
|
||||
/**
|
||||
* Gets Block by id.
|
||||
*
|
||||
* @param string $block_id block id.
|
||||
* @return BlockInterface|null
|
||||
*/
|
||||
public function get_block_by_id( string $block_id ): ?BlockInterface;
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
/**
|
||||
* ProductVariationTemplate
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* Simple Product Template.
|
||||
*/
|
||||
class ProductVariationTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
|
||||
/**
|
||||
* The context name used to identify the editor.
|
||||
*/
|
||||
const GROUP_IDS = array(
|
||||
'GENERAL' => 'general',
|
||||
'PRICING' => 'pricing',
|
||||
'INVENTORY' => 'inventory',
|
||||
'SHIPPING' => 'shipping',
|
||||
);
|
||||
|
||||
/**
|
||||
* The option name used check whether the single variation notice has been dismissed.
|
||||
*/
|
||||
const SINGLE_VARIATION_NOTICE_DISMISSED_OPTION = 'woocommerce_single_variation_notice_dismissed';
|
||||
|
||||
/**
|
||||
* SimpleProductTemplate constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->add_group_blocks();
|
||||
$this->add_general_group_blocks();
|
||||
$this->add_pricing_group_blocks();
|
||||
$this->add_inventory_group_blocks();
|
||||
$this->add_shipping_group_blocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return 'product-variation';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string {
|
||||
return __( 'Product Variation Template', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Template for the product variation form', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the group blocks to the template.
|
||||
*/
|
||||
private function add_group_blocks() {
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['GENERAL'],
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'General', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['PRICING'],
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['INVENTORY'],
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['SHIPPING'],
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'title' => __( 'Shipping', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the general group blocks to the template.
|
||||
*/
|
||||
private function add_general_group_blocks() {
|
||||
$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
|
||||
$general_group->add_block(
|
||||
[
|
||||
'id' => 'general-single-variation-notice',
|
||||
'blockName' => 'woocommerce/product-single-variation-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( '<strong>You’re editing details specific to this variation.</strong> Some information, like description and images, will be inherited from the main product, <noticeLink><parentProductName/></noticeLink>.', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'isDismissible' => true,
|
||||
'name' => $this::SINGLE_VARIATION_NOTICE_DISMISSED_OPTION,
|
||||
],
|
||||
]
|
||||
);
|
||||
// Basic Details Section.
|
||||
$basic_details = $general_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-details-section',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Variation details', 'woocommerce' ),
|
||||
'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$basic_details->add_block(
|
||||
[
|
||||
'id' => 'product-variation-note',
|
||||
'blockName' => 'woocommerce/product-summary-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'property' => 'description',
|
||||
'label' => __( 'Note <optional />', 'woocommerce' ),
|
||||
'helpText' => 'Enter an optional note displayed on the product page when customers select this variation.',
|
||||
],
|
||||
]
|
||||
);
|
||||
$basic_details->add_block(
|
||||
[
|
||||
'id' => 'product-variation-visibility',
|
||||
'blockName' => 'woocommerce/product-checkbox-field',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'property' => 'status',
|
||||
'label' => __( 'Hide in product catalog', 'woocommerce' ),
|
||||
'checkedValue' => 'private',
|
||||
'uncheckedValue' => 'publish',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Images section.
|
||||
$images_section = $general_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-images-section',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Image', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
|
||||
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$images_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-image',
|
||||
'blockName' => 'woocommerce/product-images-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'property' => 'image',
|
||||
'multiple' => false,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Downloads section.
|
||||
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
|
||||
$general_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-downloads-section',
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'title' => __( 'Downloads', 'woocommerce' ),
|
||||
'description' => __( "Add any files you'd like to make available for the customer to download after purchasing, such as instructions or warranty info.", 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
)->add_block(
|
||||
[
|
||||
'id' => 'product-variation-downloads',
|
||||
'blockName' => 'woocommerce/product-downloads-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pricing group blocks to the template.
|
||||
*/
|
||||
private function add_pricing_group_blocks() {
|
||||
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
|
||||
$pricing_group->add_block(
|
||||
[
|
||||
'id' => 'pricing-single-variation-notice',
|
||||
'blockName' => 'woocommerce/product-single-variation-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( '<strong>You’re editing details specific to this variation.</strong> Some information, like description and images, will be inherited from the main product, <noticeLink><parentProductName/></noticeLink>.', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'isDismissible' => true,
|
||||
'name' => $this::SINGLE_VARIATION_NOTICE_DISMISSED_OPTION,
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Pricing Section.
|
||||
$product_pricing_section = $pricing_group->add_section(
|
||||
[
|
||||
'id' => 'product-pricing-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
|
||||
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_columns = $product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-columns',
|
||||
'blockName' => 'core/columns',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$pricing_column_1 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-column-1',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_1->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-regular-price',
|
||||
'blockName' => 'woocommerce/product-regular-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'Regular price', 'woocommerce' ),
|
||||
'isRequired' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-column-2',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-sale-price',
|
||||
'blockName' => 'woocommerce/product-sale-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-schedule-sale-fields',
|
||||
'blockName' => 'woocommerce/product-schedule-sale-fields',
|
||||
'order' => 20,
|
||||
]
|
||||
);
|
||||
|
||||
$product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-tax-class',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'title' => __( 'Tax class', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'Same as main product', 'woocommerce' ),
|
||||
'value' => 'parent',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Standard', 'woocommerce' ),
|
||||
'value' => '',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Reduced rate', 'woocommerce' ),
|
||||
'value' => 'reduced-rate',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Zero rate', 'woocommerce' ),
|
||||
'value' => 'zero-rate',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the inventory group blocks to the template.
|
||||
*/
|
||||
private function add_inventory_group_blocks() {
|
||||
$inventory_group = $this->get_group_by_id( $this::GROUP_IDS['INVENTORY'] );
|
||||
$inventory_group->add_block(
|
||||
[
|
||||
'id' => 'inventory-single-variation-notice',
|
||||
'blockName' => 'woocommerce/product-single-variation-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( '<strong>You’re editing details specific to this variation.</strong> Some information, like description and images, will be inherited from the main product, <noticeLink><parentProductName/></noticeLink>.', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'isDismissible' => true,
|
||||
'name' => $this::SINGLE_VARIATION_NOTICE_DISMISSED_OPTION,
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Inventory Section.
|
||||
$product_inventory_section = $inventory_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-inventory-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Inventory settings link opening tag. %2$s: Inventory settings link closing tag.*/
|
||||
__( 'Set up and manage inventory for this product, including status and available quantity. %1$sManage store inventory settings%2$s', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section = $product_inventory_section->add_section(
|
||||
[
|
||||
'id' => 'product-variation-inventory-inner-section',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-sku-field',
|
||||
'blockName' => 'woocommerce/product-sku-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-track-stock',
|
||||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
|
||||
'property' => 'manage_stock',
|
||||
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
|
||||
'disabledCopy' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Per your %1$sstore settings%2$s, inventory management is <strong>disabled</strong>.', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_quantity_conditional = $product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-inventory-quantity-conditional-wrapper',
|
||||
'blockName' => 'woocommerce/conditional',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'mustMatch' => [
|
||||
'manage_stock' => [ true ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_quantity_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-variation-inventory-quantity',
|
||||
'blockName' => 'woocommerce/product-inventory-quantity-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_stock_status_conditional = $product_inventory_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-stock-status-conditional-wrapper',
|
||||
'blockName' => 'woocommerce/conditional',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'mustMatch' => [
|
||||
'manage_stock' => [ false ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_stock_status_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-variation-stock-status',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Stock status', 'woocommerce' ),
|
||||
'property' => 'stock_status',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'In stock', 'woocommerce' ),
|
||||
'value' => 'instock',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Out of stock', 'woocommerce' ),
|
||||
'value' => 'outofstock',
|
||||
],
|
||||
[
|
||||
'label' => __( 'On backorder', 'woocommerce' ),
|
||||
'value' => 'onbackorder',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the shipping group blocks to the template.
|
||||
*/
|
||||
private function add_shipping_group_blocks() {
|
||||
$shipping_group = $this->get_group_by_id( $this::GROUP_IDS['SHIPPING'] );
|
||||
$shipping_group->add_block(
|
||||
[
|
||||
'id' => 'shipping-single-variation-notice',
|
||||
'blockName' => 'woocommerce/product-single-variation-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( '<strong>You’re editing details specific to this variation.</strong> Some information, like description and images, will be inherited from the main product, <noticeLink><parentProductName/></noticeLink>.', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'isDismissible' => true,
|
||||
'name' => $this::SINGLE_VARIATION_NOTICE_DISMISSED_OPTION,
|
||||
],
|
||||
]
|
||||
);
|
||||
// Virtual section.
|
||||
$shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-virtual-section',
|
||||
'order' => 20,
|
||||
]
|
||||
)->add_block(
|
||||
[
|
||||
'id' => 'product-variation-virtual',
|
||||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'property' => 'virtual',
|
||||
'checkedValue' => false,
|
||||
'uncheckedValue' => true,
|
||||
'label' => __( 'This variation requires shipping or pickup', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Shipping Section.
|
||||
$product_fee_and_dimensions_section = $shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-fee-and-dimensions-section',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Fees & dimensions', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
|
||||
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_fee_and_dimensions_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-shipping-class',
|
||||
'blockName' => 'woocommerce/product-shipping-class-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_fee_and_dimensions_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-shipping-dimensions',
|
||||
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
|
||||
'order' => 20,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Section Block class.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class for Section block.
|
||||
*/
|
||||
class Section extends ProductBlock implements SectionInterface {
|
||||
|
||||
/**
|
||||
* Section Block constructor.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param ContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
* @throws \InvalidArgumentException If blockName key and value are passed into block configuration.
|
||||
*/
|
||||
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
if ( ! empty( $config['blockName'] ) ) {
|
||||
throw new \InvalidArgumentException( 'Unexpected key "blockName", this defaults to "woocommerce/product-section".' );
|
||||
}
|
||||
parent::__construct( array_merge( array( 'blockName' => 'woocommerce/product-section' ), $config ), $root_template, $parent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a section block type to this template.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_section( array $block_config ): SectionInterface {
|
||||
$block = new Section( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for section containers, which contain sub-sections and blocks.
|
||||
*/
|
||||
interface SectionInterface extends BlockContainerInterface {
|
||||
|
||||
/**
|
||||
* Adds a new sub-section to the section.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
* @return SectionInterface new block section.
|
||||
*/
|
||||
public function add_section( array $block_config ): SectionInterface;
|
||||
|
||||
/**
|
||||
* Adds a new block to the section.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface;
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
<?php
|
||||
/**
|
||||
* SimpleProductTemplate
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
|
||||
/**
|
||||
* Simple Product Template.
|
||||
*/
|
||||
class SimpleProductTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
|
||||
/**
|
||||
* The context name used to identify the editor.
|
||||
*/
|
||||
const GROUP_IDS = array(
|
||||
'GENERAL' => 'general',
|
||||
'ORGANIZATION' => 'organization',
|
||||
'PRICING' => 'pricing',
|
||||
'INVENTORY' => 'inventory',
|
||||
'SHIPPING' => 'shipping',
|
||||
'VARIATIONS' => 'variations',
|
||||
);
|
||||
|
||||
/**
|
||||
* SimpleProductTemplate constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->add_group_blocks();
|
||||
$this->add_general_group_blocks();
|
||||
$this->add_organization_group_blocks();
|
||||
$this->add_pricing_group_blocks();
|
||||
$this->add_inventory_group_blocks();
|
||||
$this->add_shipping_group_blocks();
|
||||
$this->add_variation_group_blocks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return 'simple-product';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string {
|
||||
return __( 'Simple Product Template', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return __( 'Template for the simple product form', 'woocommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the group blocks to the template.
|
||||
*/
|
||||
private function add_group_blocks() {
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['GENERAL'],
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'General', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['ORGANIZATION'],
|
||||
'order' => 15,
|
||||
'attributes' => [
|
||||
'title' => __( 'Organization', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['PRICING'],
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['INVENTORY'],
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['SHIPPING'],
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'title' => __( 'Shipping', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
if ( Features::is_enabled( 'product-variation-management' ) ) {
|
||||
$this->add_group(
|
||||
[
|
||||
'id' => $this::GROUP_IDS['VARIATIONS'],
|
||||
'order' => 50,
|
||||
'attributes' => [
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the general group blocks to the template.
|
||||
*/
|
||||
private function add_general_group_blocks() {
|
||||
$general_group = $this->get_group_by_id( $this::GROUP_IDS['GENERAL'] );
|
||||
// Basic Details Section.
|
||||
$basic_details = $general_group->add_section(
|
||||
[
|
||||
'id' => 'basic-details',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Basic details', 'woocommerce' ),
|
||||
'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$basic_details->add_block(
|
||||
[
|
||||
'id' => 'product-name',
|
||||
'blockName' => 'woocommerce/product-name-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'name' => 'Product name',
|
||||
'autoFocus' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
$basic_details->add_block(
|
||||
[
|
||||
'id' => 'product-summary',
|
||||
'blockName' => 'woocommerce/product-summary-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'property' => 'short_description',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_columns = $basic_details->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-columns',
|
||||
'blockName' => 'core/columns',
|
||||
'order' => 30,
|
||||
]
|
||||
);
|
||||
$pricing_column_1 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-column-1',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_1->add_block(
|
||||
[
|
||||
'id' => 'product-regular-price',
|
||||
'blockName' => 'woocommerce/product-regular-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'List price', 'woocommerce' ),
|
||||
/* translators: PricingTab: This is a link tag to the pricing tab. */
|
||||
'help' => __( 'Manage more settings in <PricingTab>Pricing.</PricingTab>', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-column-2',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2->add_block(
|
||||
[
|
||||
'id' => 'product-sale-price',
|
||||
'blockName' => 'woocommerce/product-sale-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Description section.
|
||||
$description_section = $general_group->add_section(
|
||||
[
|
||||
'id' => 'product-description-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Description', 'woocommerce' ),
|
||||
'description' => __( 'What makes this product unique? What are its most important features? Enrich the product page by adding rich content using blocks.', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$description_section->add_block(
|
||||
[
|
||||
'id' => 'product-description',
|
||||
'blockName' => 'woocommerce/product-description-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
// Images section.
|
||||
$images_section = $general_group->add_section(
|
||||
[
|
||||
'id' => 'product-images-section',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Images', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag. */
|
||||
__( 'Drag images, upload new ones or select files from your library. For best results, use JPEG files that are 1000 by 1000 pixels or larger. %1$sHow to prepare images?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-take-professional-product-photos-top-tips" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$images_section->add_block(
|
||||
[
|
||||
'id' => 'product-images',
|
||||
'blockName' => 'woocommerce/product-images-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'images' => [],
|
||||
'property' => 'images',
|
||||
],
|
||||
]
|
||||
);
|
||||
// Downloads section.
|
||||
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
|
||||
$general_group->add_section(
|
||||
[
|
||||
'id' => 'product-downloads-section',
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'title' => __( 'Downloads', 'woocommerce' ),
|
||||
'description' => __( "Add any files you'd like to make available for the customer to download after purchasing, such as instructions or warranty info.", 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
)->add_block(
|
||||
[
|
||||
'id' => 'product-downloads',
|
||||
'blockName' => 'woocommerce/product-downloads-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the organization group blocks to the template.
|
||||
*/
|
||||
private function add_organization_group_blocks() {
|
||||
$organization_group = $this->get_group_by_id( $this::GROUP_IDS['ORGANIZATION'] );
|
||||
// Product Catalog Section.
|
||||
$product_catalog_section = $organization_group->add_section(
|
||||
[
|
||||
'id' => 'product-catalog-section',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Product catalog', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-categories',
|
||||
'blockName' => 'woocommerce/product-taxonomy-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'slug' => 'product_cat',
|
||||
'property' => 'categories',
|
||||
'label' => __( 'Categories', 'woocommerce' ),
|
||||
'createTitle' => __( 'Create new category', 'woocommerce' ),
|
||||
'dialogNameHelpText' => __( 'Shown to customers on the product page.', 'woocommerce' ),
|
||||
'parentTaxonomyText' => __( 'Parent category', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-tags',
|
||||
'blockName' => 'woocommerce/product-tag-field',
|
||||
'attributes' => [
|
||||
'name' => 'tags',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-catalog-search-visibility',
|
||||
'blockName' => 'woocommerce/product-catalog-visibility-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'label' => __( 'Hide in product catalog', 'woocommerce' ),
|
||||
'visibility' => 'search',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-catalog-catalog-visibility',
|
||||
'blockName' => 'woocommerce/product-catalog-visibility-field',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'label' => __( 'Hide from search results', 'woocommerce' ),
|
||||
'visibility' => 'catalog',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-enable-product-reviews',
|
||||
'blockName' => 'woocommerce/product-checkbox-field',
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'label' => __( 'Enable product reviews', 'woocommerce' ),
|
||||
'property' => 'reviews_allowed',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-post-password',
|
||||
'blockName' => 'woocommerce/product-password-field',
|
||||
'order' => 50,
|
||||
'attributes' => [
|
||||
'label' => __( 'Require a password', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
// Attributes section.
|
||||
$product_catalog_section = $organization_group->add_section(
|
||||
[
|
||||
'id' => 'product-attributes-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Attributes', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_catalog_section->add_block(
|
||||
[
|
||||
'id' => 'product-attributes',
|
||||
'blockName' => 'woocommerce/product-attributes-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the pricing group blocks to the template.
|
||||
*/
|
||||
private function add_pricing_group_blocks() {
|
||||
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
|
||||
$pricing_group->add_block(
|
||||
[
|
||||
'id' => 'pricing-has-variations-notice',
|
||||
'blockName' => 'woocommerce/product-has-variations-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
|
||||
'buttonText' => __( 'Go to Variations', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Pricing Section.
|
||||
$product_pricing_section = $pricing_group->add_section(
|
||||
[
|
||||
'id' => 'product-pricing-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Pricing', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Images guide link opening tag. %2$s: Images guide link closing tag.*/
|
||||
__( 'Set a competitive price, put the product on sale, and manage tax calculations. %1$sHow to price your product?%2$s', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_columns = $product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-columns',
|
||||
'blockName' => 'core/columns',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$pricing_column_1 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-column-1',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_1->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-regular-price',
|
||||
'blockName' => 'woocommerce/product-regular-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'name' => 'regular_price',
|
||||
'label' => __( 'List price', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2 = $pricing_columns->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-group-pricing-column-2',
|
||||
'blockName' => 'core/column',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'templateLock' => 'all',
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_column_2->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-sale-price',
|
||||
'blockName' => 'woocommerce/product-sale-price-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'label' => __( 'Sale price', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-schedule-sale-fields',
|
||||
'blockName' => 'woocommerce/product-schedule-sale-fields',
|
||||
'order' => 20,
|
||||
]
|
||||
);
|
||||
$product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-sale-tax',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Charge sales tax on', 'woocommerce' ),
|
||||
'property' => 'tax_status',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'Product and shipping', 'woocommerce' ),
|
||||
'value' => 'taxable',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Only shipping', 'woocommerce' ),
|
||||
'value' => 'shipping',
|
||||
],
|
||||
[
|
||||
'label' => __( "Don't charge tax", 'woocommerce' ),
|
||||
'value' => 'none',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_advanced_block = $product_pricing_section->add_block(
|
||||
[
|
||||
'id' => 'product-pricing-advanced',
|
||||
'blockName' => 'woocommerce/product-collapsible',
|
||||
'order' => 40,
|
||||
'attributes' => [
|
||||
'toggleText' => __( 'Advanced', 'woocommerce' ),
|
||||
'initialCollapsed' => true,
|
||||
'persistRender' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
$pricing_advanced_block->add_block(
|
||||
[
|
||||
'id' => 'product-tax-class',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Tax class', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'Standard', 'woocommerce' ),
|
||||
'value' => '',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Reduced rate', 'woocommerce' ),
|
||||
'value' => 'reduced-rate',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Zero rate', 'woocommerce' ),
|
||||
'value' => 'zero-rate',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the inventory group blocks to the template.
|
||||
*/
|
||||
private function add_inventory_group_blocks() {
|
||||
$inventory_group = $this->get_group_by_id( $this::GROUP_IDS['INVENTORY'] );
|
||||
$inventory_group->add_block(
|
||||
[
|
||||
'id' => 'product_variation_notice_inventory_tab',
|
||||
'blockName' => 'woocommerce/product-has-variations-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
|
||||
'buttonText' => __( 'Go to Variations', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Pricing Section.
|
||||
$product_inventory_section = $inventory_group->add_section(
|
||||
[
|
||||
'id' => 'product-inventory-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Inventory', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Inventory settings link opening tag. %2$s: Inventory settings link closing tag.*/
|
||||
__( 'Set up and manage inventory for this product, including status and available quantity. %1$sManage store inventory settings%2$s', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'blockGap' => 'unit-40',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section = $product_inventory_section->add_section(
|
||||
[
|
||||
'id' => 'product-inventory-inner-section',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-sku-field',
|
||||
'blockName' => 'woocommerce/product-sku-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-track-stock',
|
||||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'label' => __( 'Track stock quantity for this product', 'woocommerce' ),
|
||||
'property' => 'manage_stock',
|
||||
'disabled' => 'yes' !== get_option( 'woocommerce_manage_stock' ),
|
||||
'disabledCopy' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Per your %1$sstore settings%2$s, inventory management is <strong>disabled</strong>.', 'woocommerce' ),
|
||||
'<a href="' . admin_url( 'admin.php?page=wc-settings&tab=products§ion=inventory' ) . '" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_quantity_conditional = $product_inventory_inner_section->add_block(
|
||||
[
|
||||
'id' => 'product-inventory-quantity-conditional-wrapper',
|
||||
'blockName' => 'woocommerce/conditional',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'mustMatch' => [
|
||||
'manage_stock' => [ true ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_quantity_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-inventory-quantity',
|
||||
'blockName' => 'woocommerce/product-inventory-quantity-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_stock_status_conditional = $product_inventory_section->add_block(
|
||||
[
|
||||
'id' => 'product-stock-status-conditional-wrapper',
|
||||
'blockName' => 'woocommerce/conditional',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'mustMatch' => [
|
||||
'manage_stock' => [ false ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_stock_status_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-stock-status',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Stock status', 'woocommerce' ),
|
||||
'property' => 'stock_status',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'In stock', 'woocommerce' ),
|
||||
'value' => 'instock',
|
||||
],
|
||||
[
|
||||
'label' => __( 'Out of stock', 'woocommerce' ),
|
||||
'value' => 'outofstock',
|
||||
],
|
||||
[
|
||||
'label' => __( 'On backorder', 'woocommerce' ),
|
||||
'value' => 'onbackorder',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_advanced = $product_inventory_section->add_block(
|
||||
[
|
||||
'id' => 'product-inventory-advanced',
|
||||
'blockName' => 'woocommerce/product-collapsible',
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'toggleText' => __( 'Advanced', 'woocommerce' ),
|
||||
'initialCollapsed' => true,
|
||||
'persistRender' => true,
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_inventory_advanced_wrapper = $product_inventory_advanced->add_block(
|
||||
[
|
||||
'blockName' => 'woocommerce/product-section',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'blockGap' => 'unit-40',
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_out_of_stock_conditional = $product_inventory_advanced_wrapper->add_block(
|
||||
[
|
||||
'id' => 'product-out-of-stock-conditional-wrapper',
|
||||
'blockName' => 'woocommerce/conditional',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'mustMatch' => [
|
||||
'manage_stock' => [ true ],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_out_of_stock_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-out-of-stock',
|
||||
'blockName' => 'woocommerce/product-radio-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'When out of stock', 'woocommerce' ),
|
||||
'property' => 'backorders',
|
||||
'options' => [
|
||||
[
|
||||
'label' => __( 'Allow purchases', 'woocommerce' ),
|
||||
'value' => 'yes',
|
||||
],
|
||||
[
|
||||
'label' => __(
|
||||
'Allow purchases, but notify customers',
|
||||
'woocommerce'
|
||||
),
|
||||
'value' => 'notify',
|
||||
],
|
||||
[
|
||||
'label' => __( "Don't allow purchases", 'woocommerce' ),
|
||||
'value' => 'no',
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_out_of_stock_conditional->add_block(
|
||||
[
|
||||
'id' => 'product-inventory-email',
|
||||
'blockName' => 'woocommerce/product-inventory-email-field',
|
||||
'order' => 20,
|
||||
]
|
||||
);
|
||||
|
||||
$product_inventory_advanced_wrapper->add_block(
|
||||
[
|
||||
'id' => 'product-limit-purchase',
|
||||
'blockName' => 'woocommerce/product-checkbox-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __(
|
||||
'Restrictions',
|
||||
'woocommerce'
|
||||
),
|
||||
'label' => __(
|
||||
'Limit purchases to 1 item per order',
|
||||
'woocommerce'
|
||||
),
|
||||
'property' => 'sold_individually',
|
||||
'tooltip' => __(
|
||||
'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.',
|
||||
'woocommerce'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the shipping group blocks to the template.
|
||||
*/
|
||||
private function add_shipping_group_blocks() {
|
||||
$shipping_group = $this->get_group_by_id( $this::GROUP_IDS['SHIPPING'] );
|
||||
$shipping_group->add_block(
|
||||
[
|
||||
'id' => 'product_variation_notice_shipping_tab',
|
||||
'blockName' => 'woocommerce/product-has-variations-notice',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'content' => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
|
||||
'buttonText' => __( 'Go to Variations', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
],
|
||||
]
|
||||
);
|
||||
// Virtual section.
|
||||
$shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-virtual-section',
|
||||
'order' => 10,
|
||||
]
|
||||
)->add_block(
|
||||
[
|
||||
'id' => 'product-virtual',
|
||||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'property' => 'virtual',
|
||||
'checkedValue' => false,
|
||||
'uncheckedValue' => true,
|
||||
'label' => __( 'This product requires shipping or pickup', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Shipping Section.
|
||||
$product_fee_and_dimensions_section = $shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-fee-and-dimensions-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Fees & dimensions', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
|
||||
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$product_fee_and_dimensions_section->add_block(
|
||||
[
|
||||
'id' => 'product-shipping-class',
|
||||
'blockName' => 'woocommerce/product-shipping-class-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
$product_fee_and_dimensions_section->add_block(
|
||||
[
|
||||
'id' => 'product-shipping-dimensions',
|
||||
'blockName' => 'woocommerce/product-shipping-dimensions-fields',
|
||||
'order' => 20,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the variation group blocks to the template.
|
||||
*/
|
||||
private function add_variation_group_blocks() {
|
||||
$variation_group = $this->get_group_by_id( $this::GROUP_IDS['VARIATIONS'] );
|
||||
if ( ! $variation_group ) {
|
||||
return;
|
||||
}
|
||||
$variation_fields = $variation_group->add_block(
|
||||
[
|
||||
'id' => 'product_variation-field-group',
|
||||
'blockName' => 'woocommerce/product-variations-fields',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Sell your product in multiple variations like size or color. strong opening tag. %2$s: Sell your product in multiple variations like size or color. strong closing tag.*/
|
||||
__( '%1$sSell your product in multiple variations like size or color.%2$s Get started by adding options for the buyers to choose on the product page.', 'woocommerce' ),
|
||||
'<strong>',
|
||||
'</strong>'
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
$variation_options_section = $variation_fields->add_block(
|
||||
[
|
||||
'id' => 'product-variation-options-section',
|
||||
'blockName' => 'woocommerce/product-section',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'title' => __( 'Variation options', 'woocommerce' ),
|
||||
'description' => __( 'Add and manage attributes used for product options, such as size and color.', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
$variation_options_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-options',
|
||||
'blockName' => 'woocommerce/product-variations-options-field',
|
||||
]
|
||||
);
|
||||
$variation_section = $variation_fields->add_block(
|
||||
[
|
||||
'id' => 'product-variation-section',
|
||||
'blockName' => 'woocommerce/product-section',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'title' => __( 'Variations', 'woocommerce' ),
|
||||
'description' => __( 'Manage individual product combinations created from options.', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$variation_section->add_block(
|
||||
[
|
||||
'id' => 'product-variation-items',
|
||||
'blockName' => 'woocommerce/product-variation-items-field',
|
||||
'order' => 10,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,16 @@ class RedirectionController {
|
||||
*/
|
||||
protected function is_product_supported( $product_id ): bool {
|
||||
$product = $product_id ? wc_get_product( $product_id ) : null;
|
||||
return $product && in_array( $product->get_type(), $this->supported_post_types, true );
|
||||
$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' ) ) {
|
||||
return true;
|
||||
}
|
||||
return ! $digital_product;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,6 +133,7 @@ class RedirectionController {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redirect non supported product types to legacy editor.
|
||||
*/
|
||||
|
||||
@@ -496,27 +496,6 @@ class WC_Admin_Notes_Selling_Online_Courses extends DeprecatedClassFacade {
|
||||
protected static $deprecated_in_version = '4.8.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_Admin_Notes_Test_Checkout.
|
||||
*
|
||||
* @deprecated since 4.8.0, use TestCheckout
|
||||
*/
|
||||
class WC_Admin_Notes_Test_Checkout extends DeprecatedClassFacade {
|
||||
/**
|
||||
* The name of the non-deprecated class that this facade covers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $facade_over_classname = 'Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout';
|
||||
|
||||
/**
|
||||
* The version that this class was deprecated in.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $deprecated_in_version = '4.8.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* WC_Admin_Notes_Tracking_Opt_In.
|
||||
*
|
||||
|
||||
@@ -450,6 +450,10 @@ class PageController {
|
||||
$options['path'] = self::PAGE_ROOT . '&path=' . $options['path'];
|
||||
}
|
||||
|
||||
if ( null !== $options['position'] ) {
|
||||
$options['position'] = intval( round( $options['position'] ) );
|
||||
}
|
||||
|
||||
if ( is_null( $options['parent'] ) ) {
|
||||
add_menu_page(
|
||||
$options['title'],
|
||||
@@ -458,7 +462,7 @@ class PageController {
|
||||
$options['path'],
|
||||
array( __CLASS__, 'page_wrapper' ),
|
||||
$options['icon'],
|
||||
intval( round( $options['position'] ) )
|
||||
$options['position']
|
||||
);
|
||||
} else {
|
||||
$parent_path = $this->get_path_from_id( $options['parent'] );
|
||||
|
||||
@@ -186,11 +186,12 @@ class PluginsHelper {
|
||||
include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php';
|
||||
include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php';
|
||||
|
||||
$existing_plugins = self::get_installed_plugins_paths();
|
||||
$installed_plugins = array();
|
||||
$results = array();
|
||||
$time = array();
|
||||
$errors = new WP_Error();
|
||||
$existing_plugins = self::get_installed_plugins_paths();
|
||||
$installed_plugins = array();
|
||||
$results = array();
|
||||
$time = array();
|
||||
$errors = new WP_Error();
|
||||
$install_start_time = time();
|
||||
|
||||
foreach ( $plugins as $plugin ) {
|
||||
$slug = sanitize_key( $plugin );
|
||||
@@ -305,8 +306,6 @@ class PluginsHelper {
|
||||
$logger && $logger->installed( $plugin, $time[ $plugin ] );
|
||||
}
|
||||
|
||||
$logger && $logger->complete();
|
||||
|
||||
$data = array(
|
||||
'installed' => $installed_plugins,
|
||||
'results' => $results,
|
||||
@@ -314,6 +313,8 @@ class PluginsHelper {
|
||||
'time' => $time,
|
||||
);
|
||||
|
||||
$logger && $logger->complete( array_merge( $data, array( 'start_time' => $install_start_time ) ) );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -135,14 +135,97 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger {
|
||||
/**
|
||||
* Record completed_time.
|
||||
*
|
||||
* @param array $data return data from install_plugins().
|
||||
* @return void
|
||||
*/
|
||||
public function complete() {
|
||||
public function complete( $data = array() ) {
|
||||
$option = $this->get();
|
||||
|
||||
$option['complete_time'] = time();
|
||||
$option['status'] = 'complete';
|
||||
|
||||
$this->track( $data );
|
||||
$this->update( $option );
|
||||
}
|
||||
|
||||
private function get_plugin_track_key( $id ) {
|
||||
$slug = explode( ':', $id )[0];
|
||||
$key = preg_match( '/^woocommerce(-|_)payments$/', $slug )
|
||||
? 'wcpay'
|
||||
: explode( ':', str_replace( '-', '_', $slug ) )[0];
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns time frame for a given time in milliseconds.
|
||||
*
|
||||
* @param int $timeInMs - time in milliseconds
|
||||
*
|
||||
* @return string - Time frame.
|
||||
*/
|
||||
function get_timeframe( $timeInMs ) {
|
||||
$time_frames = [
|
||||
[
|
||||
'name' => '0-2s',
|
||||
'max' => 2,
|
||||
],
|
||||
[
|
||||
'name' => '2-5s',
|
||||
'max' => 5,
|
||||
],
|
||||
[
|
||||
'name' => '5-10s',
|
||||
'max' => 10,
|
||||
],
|
||||
[
|
||||
'name' => '10-15s',
|
||||
'max' => 15,
|
||||
],
|
||||
[
|
||||
'name' => '15-20s',
|
||||
'max' => 20,
|
||||
],
|
||||
[
|
||||
'name' => '20-30s',
|
||||
'max' => 30,
|
||||
],
|
||||
[
|
||||
'name' => '30-60s',
|
||||
'max' => 60,
|
||||
],
|
||||
[ 'name' => '>60s' ],
|
||||
];
|
||||
|
||||
foreach ( $time_frames as $time_frame ) {
|
||||
if ( ! isset( $time_frame['max'] ) ) {
|
||||
return $time_frame['name'];
|
||||
}
|
||||
if ( $timeInMs < $time_frame['max'] * 1000 ) {
|
||||
return $time_frame['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function track( $data ) {
|
||||
$track_data = array(
|
||||
'success' => true,
|
||||
'installed_extensions' => array_map(
|
||||
function( $extension ) {
|
||||
return $this->get_plugin_track_key( $extension );
|
||||
},
|
||||
$data['installed']
|
||||
),
|
||||
'total_time' => $this->get_timeframe( ( time() - $data['start_time'] ) * 1000 ),
|
||||
);
|
||||
|
||||
foreach ( $data['installed'] as $plugin ) {
|
||||
if ( ! isset( $data['time'][ $plugin ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$track_data[ 'install_time_' . $this->get_plugin_track_key( $plugin ) ] = $this->get_timeframe( $data['time'][ $plugin ] );
|
||||
}
|
||||
|
||||
wc_admin_record_tracks_event( 'coreprofiler_store_extensions_installed_and_activated', $track_data );
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,9 @@ interface PluginsInstallLogger {
|
||||
/**
|
||||
* Called when all plugins are processed.
|
||||
*
|
||||
* @param array $data return data from install_plugins().
|
||||
* @return mixed
|
||||
*/
|
||||
public function complete();
|
||||
public function complete( $data = array() );
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,16 @@ class ComparisonOperation {
|
||||
return ! in_array( $right_operand, $left_operand, true );
|
||||
}
|
||||
return strpos( $right_operand, $left_operand ) === false;
|
||||
case 'in':
|
||||
if ( is_array( $right_operand ) && is_string( $left_operand ) ) {
|
||||
return in_array( $left_operand, $right_operand, true );
|
||||
}
|
||||
return strpos( $left_operand, $right_operand ) !== false;
|
||||
case '!in':
|
||||
if ( is_array( $right_operand ) && is_string( $left_operand ) ) {
|
||||
return ! in_array( $left_operand, $right_operand, true );
|
||||
}
|
||||
return strpos( $left_operand, $right_operand ) === false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,14 @@ defined( 'ABSPATH' ) || exit;
|
||||
* Rule processor that negates the rules in the rule's operand.
|
||||
*/
|
||||
class NotRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The rule evaluator to use.
|
||||
*
|
||||
* @var RuleEvaluator
|
||||
*/
|
||||
protected $rule_evaluator;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -11,6 +11,14 @@ defined( 'ABSPATH' ) || exit;
|
||||
* Rule processor for publishing based on the number of orders.
|
||||
*/
|
||||
class OrderCountRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The orders provider.
|
||||
*
|
||||
* @var OrdersProvider
|
||||
*/
|
||||
protected $orders_provider;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,14 @@ use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
|
||||
* Rule processor for sending when the provided plugins are activated.
|
||||
*/
|
||||
class PluginsActivatedRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The plugins provider.
|
||||
*
|
||||
* @var PluginsProviderInterface
|
||||
*/
|
||||
protected $plugins_provider;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,14 @@ defined( 'ABSPATH' ) || exit;
|
||||
* products.
|
||||
*/
|
||||
class ProductCountRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The product query.
|
||||
*
|
||||
* @var WC_Product_Query
|
||||
*/
|
||||
protected $product_query;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,14 @@ use Automattic\WooCommerce\Admin\DateTimeProvider\CurrentDateTimeProvider;
|
||||
* Rule processor for sending after a specified date/time.
|
||||
*/
|
||||
class PublishAfterTimeRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The DateTime provider.
|
||||
*
|
||||
* @var DateTimeProviderInterface
|
||||
*/
|
||||
protected $date_time_provider;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,14 @@ use Automattic\WooCommerce\Admin\DateTimeProvider\CurrentDateTimeProvider;
|
||||
* Rule processor for sending before a specified date/time.
|
||||
*/
|
||||
class PublishBeforeTimeRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* The DateTime provider.
|
||||
*
|
||||
* @var DateTimeProviderInterface
|
||||
*/
|
||||
protected $date_time_provider;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
|
||||
@@ -13,6 +13,14 @@ defined( 'ABSPATH' ) || exit;
|
||||
* given number of seconds.
|
||||
*/
|
||||
class WCAdminActiveForRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
/**
|
||||
* Provides the amount of time wcadmin has been active for.
|
||||
*
|
||||
* @var WCAdminActiveForProvider
|
||||
*/
|
||||
protected $wcadmin_active_for_provider;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
|
||||
@@ -20,6 +20,28 @@ if ( ! class_exists( 'WC_Email', false ) ) {
|
||||
* ReportCSVEmail Class.
|
||||
*/
|
||||
class ReportCSVEmail extends \WC_Email {
|
||||
|
||||
/**
|
||||
* Report labels.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $report_labels;
|
||||
|
||||
/**
|
||||
* Report type (e.g. 'customers').
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $report_type;
|
||||
|
||||
/**
|
||||
* Download URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $download_url;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,7 @@ class WPCacheEngine implements CacheEngine {
|
||||
*/
|
||||
public function cache_object( string $key, $object, int $expiration, string $group = '' ): bool {
|
||||
$prefixed_key = self::get_prefixed_key( $key, $group );
|
||||
return wp_cache_set( $prefixed_key, $object, $group, $expiration );
|
||||
return false !== wp_cache_set( $prefixed_key, $object, $group, $expiration );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ class WPCacheEngine implements CacheEngine {
|
||||
*/
|
||||
public function delete_cached_object( string $key, string $group = '' ): bool {
|
||||
$prefixed_key = self::get_prefixed_key( $key, $group );
|
||||
return wp_cache_delete( $prefixed_key, $group );
|
||||
return false !== wp_cache_delete( $prefixed_key, $group );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +71,6 @@ class WPCacheEngine implements CacheEngine {
|
||||
* @return bool True if the group is deleted successfully, false otherwise.
|
||||
*/
|
||||
public function delete_cache_group( string $group = '' ): bool {
|
||||
return self::invalidate_cache_group( $group );
|
||||
return false !== self::invalidate_cache_group( $group );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\Downlo
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\FeaturesServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\MarketingServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\MarketplaceServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersControllersServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderAdminServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderMetaBoxServiceProvider;
|
||||
@@ -24,6 +25,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;
|
||||
|
||||
/**
|
||||
* PSR11 compliant dependency injection container for WooCommerce.
|
||||
@@ -67,6 +69,8 @@ final class Container {
|
||||
OrderAdminServiceProvider::class,
|
||||
FeaturesServiceProvider::class,
|
||||
MarketingServiceProvider::class,
|
||||
MarketplaceServiceProvider::class,
|
||||
BlockTemplatesServiceProvider::class,
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use WP_CLI;
|
||||
|
||||
/**
|
||||
@@ -60,6 +61,8 @@ class CLIRunner {
|
||||
WP_CLI::add_command( 'wc cot migrate', array( $this, 'migrate' ) );
|
||||
WP_CLI::add_command( 'wc cot sync', array( $this, 'sync' ) );
|
||||
WP_CLI::add_command( 'wc cot verify_cot_data', array( $this, 'verify_cot_data' ) );
|
||||
WP_CLI::add_command( 'wc cot enable', array( $this, 'enable' ) );
|
||||
WP_CLI::add_command( 'wc cot disable', array( $this, 'disable' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +73,7 @@ class CLIRunner {
|
||||
* @return bool Whether the COT feature is enabled.
|
||||
*/
|
||||
private function is_enabled( $log = true ) : bool {
|
||||
if ( ! $this->controller->is_feature_visible() ) {
|
||||
if ( ! $this->controller->custom_orders_table_usage_is_enabled() ) {
|
||||
if ( $log ) {
|
||||
WP_CLI::log(
|
||||
sprintf(
|
||||
@@ -82,14 +85,7 @@ class CLIRunner {
|
||||
}
|
||||
}
|
||||
|
||||
return $this->controller->is_feature_visible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to log warning that feature is not yet production ready.
|
||||
*/
|
||||
private function log_production_warning() {
|
||||
WP_CLI::log( __( 'This feature is not production ready yet. Make sure you are not running these commands in your production environment.', 'woocommerce' ) );
|
||||
return $this->controller->custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,10 +102,6 @@ class CLIRunner {
|
||||
* @return int The number of orders to be migrated.*
|
||||
*/
|
||||
public function count_unmigrated( $args = array(), $assoc_args = array() ) : int {
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$order_count = $this->synchronizer->get_current_orders_pending_sync_count();
|
||||
|
||||
@@ -156,9 +148,14 @@ class CLIRunner {
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function sync( $args = array(), $assoc_args = array() ) {
|
||||
$this->log_production_warning();
|
||||
if ( ! $this->is_enabled() ) {
|
||||
return;
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::warning( __( 'Custom order tables does not exist, creating...', 'woocommerce' ) );
|
||||
$this->synchronizer->create_database_tables();
|
||||
if ( $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::success( __( 'Custom order tables were created successfully.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::error( __( 'Custom order tables could not be created.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
$order_count = $this->count_unmigrated();
|
||||
@@ -244,6 +241,7 @@ class CLIRunner {
|
||||
}
|
||||
|
||||
/**
|
||||
* [Deprecated] Use `wp wc cot sync` instead.
|
||||
* Copy order data into the postmeta table.
|
||||
*
|
||||
* Note that this could dramatically increase the size of your postmeta table, but is recommended
|
||||
@@ -266,7 +264,6 @@ class CLIRunner {
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function migrate( $args = array(), $assoc_args = array() ) {
|
||||
$this->log_production_warning();
|
||||
WP_CLI::log( __( 'Migrate command is deprecated. Please use `sync` instead.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
@@ -319,8 +316,9 @@ class CLIRunner {
|
||||
*/
|
||||
public function verify_cot_data( $args = array(), $assoc_args = array() ) {
|
||||
global $wpdb;
|
||||
$this->log_production_warning();
|
||||
if ( ! $this->is_enabled() ) {
|
||||
|
||||
if ( ! $this->synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::error( __( 'Orders table does not exist.', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -583,11 +581,19 @@ 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 is set by the CPT datastore but no longer used anywhere.
|
||||
'_edit_lock',
|
||||
);
|
||||
|
||||
global $wpdb;
|
||||
if ( ! count( $order_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
$excluded_columns = $this->post_to_cot_migrator->get_migrated_meta_keys();
|
||||
$excluded_columns = array_merge(
|
||||
$this->post_to_cot_migrator->get_migrated_meta_keys(),
|
||||
$meta_keys_to_ignore
|
||||
);
|
||||
$excluded_columns_placeholder = implode( ', ', array_fill( 0, count( $excluded_columns ), '%s' ) );
|
||||
$order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
|
||||
$meta_table = OrdersTableDataStore::get_meta_table_name();
|
||||
@@ -671,4 +677,188 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
return $clubbed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom order tables (HPOS) to authoritative if: 1). HPOS and posts tables are in sync, or, 2). This is a new shop (in this case also create tables). Additionally, all installed WC plugins should be compatible.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--for-new-shop]
|
||||
* : Enable only if this is a new shop, irrespective of whether tables are in sync.
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* [--with-sync]
|
||||
* : Also enables sync (if it's currently not enabled).
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* ### EXAMPLES
|
||||
*
|
||||
* # Enable HPOS on new shops.
|
||||
* wp wc cot enable --for-new-shop
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enable( array $args = array(), array $assoc_args = array() ) {
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'for-new-shop' => false,
|
||||
'with-sync' => false,
|
||||
)
|
||||
);
|
||||
|
||||
$enable_hpos = true;
|
||||
WP_CLI::log( __( 'Running pre-enable checks...', 'woocommerce' ) );
|
||||
|
||||
$is_new_shop = \WC_Install::is_new_install();
|
||||
if ( $assoc_args['for-new-shop'] && ! $is_new_shop ) {
|
||||
WP_CLI::error( __( '[Failed] This is not a new shop, but --for-new-shop flag was passed.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
/** Feature controller instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
$plugin_info = $feature_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
|
||||
WP_CLI::warning( __( '[Failed] Some installed plugins are incompatible. Please review the plugins by going to WooCommerce > Settings > Advanced > Features and see the "Order data storage" section.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
|
||||
/** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
$pending_orders = $data_synchronizer->get_total_pending_count();
|
||||
$table_exists = $data_synchronizer->check_orders_table_exists();
|
||||
|
||||
if ( ! $table_exists ) {
|
||||
WP_CLI::warning( __( 'Orders table does not exist. Creating...', 'woocommerce' ) );
|
||||
if ( $is_new_shop || 0 === $pending_orders ) {
|
||||
$data_synchronizer->create_database_tables();
|
||||
if ( $data_synchronizer->check_orders_table_exists() ) {
|
||||
WP_CLI::log( __( 'Orders table created.', 'woocommerce' ) );
|
||||
$table_exists = true;
|
||||
} else {
|
||||
WP_CLI::warning( __( '[Failed] Orders table could not be created.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
} else {
|
||||
WP_CLI::warning( __( '[Failed] The orders table does not exist and this is not a new shop. Please create the table by going to WooCommerce > Settings > Advanced > Features and enabling sync.', 'woocommerce' ) );
|
||||
$enable_hpos = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $pending_orders > 0 ) {
|
||||
WP_CLI::warning(
|
||||
sprintf(
|
||||
// translators: %s is the command to run (wp wc cot sync).
|
||||
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
|
||||
'wp wc cot sync',
|
||||
)
|
||||
);
|
||||
$enable_hpos = false;
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] && $table_exists ) {
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'Sync is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, true );
|
||||
WP_CLI::success( __( 'Sync enabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $enable_hpos ) {
|
||||
WP_CLI::error( __( 'HPOS pre-checks failed, please see the errors above', 'woocommerce' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
/** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
|
||||
$cot_status = wc_get_container()->get( CustomOrdersTableController::class );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'HPOS is already enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( 'custom_order_tables', true );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::success( __( 'HPOS enabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::error( __( 'HPOS could not be enabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables custom order tables (HPOS) and posts to authoritative if HPOS and post tables are in sync.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--with-sync]
|
||||
* : Also disables sync (if it's currently enabled).
|
||||
* ---
|
||||
* default: false
|
||||
* ---
|
||||
*
|
||||
* ### EXAMPLES
|
||||
*
|
||||
* # Disable HPOS.
|
||||
* wp wc cot disable
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function disable( $args, $assoc_args ) {
|
||||
$assoc_args = wp_parse_args(
|
||||
$assoc_args,
|
||||
array(
|
||||
'with-sync' => false,
|
||||
)
|
||||
);
|
||||
|
||||
WP_CLI::log( __( 'Running pre-disable checks...', 'woocommerce' ) );
|
||||
|
||||
/** DataSynchronizer instance @var DataSynchronizer $data_synchronizer */
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
$pending_orders = $data_synchronizer->get_total_pending_count();
|
||||
if ( $pending_orders > 0 ) {
|
||||
return WP_CLI::error(
|
||||
sprintf(
|
||||
// translators: %s is the command to run (wp wc cot sync).
|
||||
__( '[Failed] There are orders pending sync. Please run `%s` to sync pending orders.', 'woocommerce' ),
|
||||
'wp wc cot sync',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** FeaturesController instance @var FeaturesController $feature_controller */
|
||||
$feature_controller = wc_get_container()->get( FeaturesController::class );
|
||||
|
||||
/** CustomOrdersTableController instance @var CustomOrdersTableController $cot_status */
|
||||
$cot_status = wc_get_container()->get( CustomOrdersTableController::class );
|
||||
if ( ! $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
WP_CLI::warning( __( 'HPOS is already disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
$feature_controller->change_feature_enable( 'custom_order_tables', false );
|
||||
if ( $cot_status->custom_orders_table_usage_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'HPOS could not be disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'HPOS disabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $assoc_args['with-sync'] ) {
|
||||
if ( ! $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync is already disabled.', 'woocommerce' ) );
|
||||
}
|
||||
$feature_controller->change_feature_enable( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, false );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
return WP_CLI::warning( __( 'Sync could not be disabled.', 'woocommerce' ) );
|
||||
} else {
|
||||
WP_CLI::success( __( 'Sync disabled.', 'woocommerce' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||
*/
|
||||
class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
|
||||
/**
|
||||
* Type of addresses being migrated, could be billing|shipping.
|
||||
* Type of addresses being migrated; 'billing' or 'shipping'.
|
||||
*
|
||||
* @var $type
|
||||
*/
|
||||
@@ -25,7 +25,7 @@ class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator {
|
||||
/**
|
||||
* PostToOrderAddressTableMigrator constructor.
|
||||
*
|
||||
* @param string $type Type of addresses being migrated, could be billing|shipping.
|
||||
* @param string $type Type of address being migrated; 'billing' or 'shipping'.
|
||||
*/
|
||||
public function __construct( $type ) {
|
||||
$this->type = $type;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Block configuration used to specify blocks in BlockTemplate.
|
||||
*/
|
||||
class AbstractBlock implements BlockInterface {
|
||||
use BlockFormattedTemplateTrait;
|
||||
|
||||
/**
|
||||
* The block name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* The block ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The block order.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $order = 10;
|
||||
|
||||
/**
|
||||
* The block attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attributes = [];
|
||||
|
||||
/**
|
||||
* The block hide conditions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $hide_conditions = [];
|
||||
|
||||
/**
|
||||
* The block hide conditions counter.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $hide_conditions_counter = 0;
|
||||
|
||||
/**
|
||||
* The block template that this block belongs to.
|
||||
*
|
||||
* @var BlockTemplate
|
||||
*/
|
||||
private $root_template;
|
||||
|
||||
/**
|
||||
* The parent container.
|
||||
*
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $parent;
|
||||
|
||||
/**
|
||||
* Block constructor.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param BlockContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
*/
|
||||
public function __construct( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
$this->validate( $config, $root_template, $parent );
|
||||
|
||||
$this->root_template = $root_template;
|
||||
$this->parent = is_null( $parent ) ? $root_template : $parent;
|
||||
|
||||
$this->name = $config[ self::NAME_KEY ];
|
||||
|
||||
if ( ! isset( $config[ self::ID_KEY ] ) ) {
|
||||
$this->id = $this->root_template->generate_block_id( $this->get_name() );
|
||||
} else {
|
||||
$this->id = $config[ self::ID_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) ) {
|
||||
$this->order = $config[ self::ORDER_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
$this->attributes = $config[ self::ATTRIBUTES_KEY ];
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::HIDE_CONDITIONS_KEY ] ) ) {
|
||||
foreach ( $config[ self::HIDE_CONDITIONS_KEY ] as $hide_condition ) {
|
||||
$this->add_hide_condition( $hide_condition['expression'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate block configuration.
|
||||
*
|
||||
* @param array $config The block configuration.
|
||||
* @param BlockTemplateInterface $root_template The block template that this block belongs to.
|
||||
* @param ContainerInterface|null $parent The parent block container.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If the parent block container does not belong to the same template as the block.
|
||||
*/
|
||||
protected function validate( array $config, BlockTemplateInterface &$root_template, ContainerInterface &$parent = null ) {
|
||||
if ( isset( $parent ) && ( $parent->get_root_template() !== $root_template ) ) {
|
||||
throw new \ValueError( 'The parent block must belong to the same template as the block.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $config[ self::NAME_KEY ] ) || ! is_string( $config[ self::NAME_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block name must be specified.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ORDER_KEY ] ) && ! is_int( $config[ self::ORDER_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block order must be an integer.' );
|
||||
}
|
||||
|
||||
if ( isset( $config[ self::ATTRIBUTES_KEY ] ) && ! is_array( $config[ self::ATTRIBUTES_KEY ] ) ) {
|
||||
throw new \ValueError( 'The block attributes must be an array.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block order.
|
||||
*/
|
||||
public function get_order(): int {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block order.
|
||||
*
|
||||
* @param int $order The block order.
|
||||
*/
|
||||
public function set_order( int $order ) {
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block attributes.
|
||||
*/
|
||||
public function get_attributes(): array {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the block attributes.
|
||||
*
|
||||
* @param array $attributes The block attributes.
|
||||
*/
|
||||
public function set_attributes( array $attributes ) {
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template that this block belongs to.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this->root_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent block container.
|
||||
*/
|
||||
public function &get_parent(): ContainerInterface {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the block from its parent.
|
||||
*/
|
||||
public function remove() {
|
||||
$this->parent->remove_block( $this->id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the block is detached from its parent block container or the template it belongs to.
|
||||
*
|
||||
* @return bool True if the block is detached from its parent block container or the template it belongs to.
|
||||
*/
|
||||
public function is_detached(): bool {
|
||||
$is_in_parent = $this->parent->get_block( $this->id ) === $this;
|
||||
$is_in_root_template = $this->get_root_template()->get_block( $this->id ) === $this;
|
||||
|
||||
return ! ( $is_in_parent && $is_in_root_template );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a hide condition to the block.
|
||||
*
|
||||
* The hide condition is a JavaScript-like expression that will be evaluated on the client to determine if the block should be hidden.
|
||||
* See [@woocommerce/expression-evaluation](https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/expression-evaluation/README.md) for more details.
|
||||
*
|
||||
* @param string $expression An expression, which if true, will hide the block.
|
||||
*/
|
||||
public function add_hide_condition( string $expression ): string {
|
||||
$key = 'k' . $this->hide_conditions_counter;
|
||||
$this->hide_conditions_counter++;
|
||||
|
||||
// Storing the expression in an array to allow for future expansion
|
||||
// (such as adding the plugin that added the condition).
|
||||
$this->hide_conditions[ $key ] = [
|
||||
'expression' => $expression,
|
||||
];
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a hide condition from the block.
|
||||
*
|
||||
* @param string $key The key of the hide condition to remove.
|
||||
*/
|
||||
public function remove_hide_condition( string $key ) {
|
||||
unset( $this->hide_conditions[ $key ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hide conditions of the block.
|
||||
*/
|
||||
public function get_hide_conditions(): array {
|
||||
return $this->hide_conditions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
abstract class AbstractBlockTemplate implements BlockTemplateInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
abstract public function get_id(): string;
|
||||
|
||||
/**
|
||||
* Get the template title.
|
||||
*/
|
||||
public function get_title(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template area.
|
||||
*/
|
||||
public function get_area(): string {
|
||||
return 'uncategorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* The block cache.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $block_cache = [];
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*/
|
||||
public function get_block( string $block_id ): ?BlockInterface {
|
||||
return $this->block_cache[ $block_id ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a block in the template. This is an internal method and should not be called directly
|
||||
* except for from the BlockContainerTrait's add_inner_block() method.
|
||||
*
|
||||
* @param BlockInterface $block The block to cache.
|
||||
*
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \ValueError If the block template that the block belongs to is not this template.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
public function cache_block( BlockInterface &$block ) {
|
||||
$id = $block->get_id();
|
||||
|
||||
if ( isset( $this->block_cache[ $id ] ) ) {
|
||||
throw new \ValueError( 'A block with the specified ID already exists in the template.' );
|
||||
}
|
||||
|
||||
if ( $block->get_root_template() !== $this ) {
|
||||
throw new \ValueError( 'The block template that the block belongs to must be the same as this template.' );
|
||||
}
|
||||
|
||||
$this->block_cache[ $id ] = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncaches a block in the template. This is an internal method and should not be called directly
|
||||
* except for from the BlockContainerTrait's remove_block() method.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
public function uncache_block( string $block_id ) {
|
||||
if ( isset( $this->block_cache[ $block_id ] ) ) {
|
||||
unset( $this->block_cache[ $block_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a block ID based on a base.
|
||||
*
|
||||
* @param string $id_base The base to use when generating an ID.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_block_id( string $id_base ): string {
|
||||
$instance_count = 0;
|
||||
|
||||
do {
|
||||
$instance_count++;
|
||||
$block_id = $id_base . '-' . $instance_count;
|
||||
} while ( isset( $this->block_cache[ $block_id ] ) );
|
||||
|
||||
return $block_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root template.
|
||||
*/
|
||||
public function &get_root_template(): BlockTemplateInterface {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
$inner_blocks_formatted_template = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
|
||||
return $inner_blocks_formatted_template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Generic block with container properties to be used in BlockTemplate.
|
||||
*/
|
||||
class Block extends AbstractBlock implements BlockContainerInterface {
|
||||
use BlockContainerTrait;
|
||||
|
||||
/**
|
||||
* Add an inner block to this block.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function &add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Trait for block containers.
|
||||
*/
|
||||
trait BlockContainerTrait {
|
||||
use BlockFormattedTemplateTrait {
|
||||
get_formatted_template as get_block_formatted_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* The inner blocks.
|
||||
*
|
||||
* @var BlockInterface[]
|
||||
*/
|
||||
private $inner_blocks = [];
|
||||
|
||||
// phpcs doesn't take into account exceptions thrown by called methods.
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Add a block to the block container.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @throws \ValueError If the block configuration is invalid.
|
||||
* @throws \ValueError If a block with the specified ID already exists in the template.
|
||||
* @throws \UnexpectedValueException If the block container is not the parent of the block.
|
||||
* @throws \UnexpectedValueException If the block container's root template is not the same as the block's root template.
|
||||
*/
|
||||
protected function &add_inner_block( BlockInterface $block ): BlockInterface {
|
||||
if ( $block->get_parent() !== $this ) {
|
||||
throw new \UnexpectedValueException( 'The block container is not the parent of the block.' );
|
||||
}
|
||||
|
||||
if ( $block->get_root_template() !== $this->get_root_template() ) {
|
||||
throw new \UnexpectedValueException( 'The block container\'s root template is not the same as the block\'s root template.' );
|
||||
}
|
||||
|
||||
$is_detached = method_exists( $this, 'is_detached' ) && $this->is_detached();
|
||||
if ( $is_detached ) {
|
||||
BlockTemplateLogger::get_instance()->warning(
|
||||
'Block added to detached container. Block will not be included in the template, since the container will not be included in the template.',
|
||||
[
|
||||
'block' => $block,
|
||||
'container' => $this,
|
||||
'template' => $this->get_root_template(),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$this->get_root_template()->cache_block( $block );
|
||||
}
|
||||
|
||||
$this->inner_blocks[] = &$block;
|
||||
|
||||
$this->do_after_add_block_action( $block );
|
||||
$this->do_after_add_specific_block_action( $block );
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Checks if a block is a descendant of the block container.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
private function is_block_descendant( BlockInterface $block ): bool {
|
||||
$parent = $block->get_parent();
|
||||
|
||||
if ( $parent === $this ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! $parent instanceof BlockInterface ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->is_block_descendant( $parent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a block by ID.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*/
|
||||
public function get_block( string $block_id ): ?BlockInterface {
|
||||
foreach ( $this->inner_blocks as $block ) {
|
||||
if ( $block->get_id() === $block_id ) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $this->inner_blocks as $block ) {
|
||||
if ( $block instanceof ContainerInterface ) {
|
||||
$block = $block->get_block( $block_id );
|
||||
|
||||
if ( $block ) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a block from the block container.
|
||||
*
|
||||
* @param string $block_id The block ID.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the block container is not an ancestor of the block.
|
||||
*/
|
||||
public function remove_block( string $block_id ) {
|
||||
$root_template = $this->get_root_template();
|
||||
|
||||
$block = $root_template->get_block( $block_id );
|
||||
|
||||
if ( ! $block ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->is_block_descendant( $block ) ) {
|
||||
throw new \UnexpectedValueException( 'The block container is not an ancestor of the block.' );
|
||||
}
|
||||
|
||||
// If the block is a container, remove all of its blocks.
|
||||
if ( $block instanceof ContainerInterface ) {
|
||||
$block->remove_blocks();
|
||||
}
|
||||
|
||||
$parent = $block->get_parent();
|
||||
$parent->remove_inner_block( $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all blocks from the block container.
|
||||
*/
|
||||
public function remove_blocks() {
|
||||
array_map(
|
||||
function ( BlockInterface $block ) {
|
||||
$this->remove_block( $block->get_id() );
|
||||
},
|
||||
$this->inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a block from the block container's inner blocks. This is an internal method and should not be called directly
|
||||
* except for from the BlockContainerTrait's remove_block() method.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
public function remove_inner_block( BlockInterface $block ) {
|
||||
// Remove block from root template's cache.
|
||||
$root_template = $this->get_root_template();
|
||||
$root_template->uncache_block( $block->get_id() );
|
||||
|
||||
$this->inner_blocks = array_filter(
|
||||
$this->inner_blocks,
|
||||
function ( BlockInterface $inner_block ) use ( $block ) {
|
||||
return $inner_block !== $block;
|
||||
}
|
||||
);
|
||||
|
||||
BlockTemplateLogger::get_instance()->info(
|
||||
'Block removed from template.',
|
||||
[
|
||||
'block' => $block,
|
||||
'template' => $root_template,
|
||||
]
|
||||
);
|
||||
|
||||
$this->do_after_remove_block_action( $block );
|
||||
$this->do_after_remove_specific_block_action( $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks sorted by order.
|
||||
*/
|
||||
private function get_inner_blocks_sorted_by_order(): array {
|
||||
$sorted_inner_blocks = $this->inner_blocks;
|
||||
|
||||
usort(
|
||||
$sorted_inner_blocks,
|
||||
function( BlockInterface $a, BlockInterface $b ) {
|
||||
return $a->get_order() <=> $b->get_order();
|
||||
}
|
||||
);
|
||||
|
||||
return $sorted_inner_blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inner blocks as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = $this->get_block_formatted_template();
|
||||
|
||||
$inner_blocks = $this->get_inner_blocks_sorted_by_order();
|
||||
|
||||
if ( ! empty( $inner_blocks ) ) {
|
||||
$arr[] = array_map(
|
||||
function( BlockInterface $block ) {
|
||||
return $block->get_formatted_template();
|
||||
},
|
||||
$inner_blocks
|
||||
);
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_template_after_add_block` action.
|
||||
* Handle exceptions thrown by the action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
private function do_after_add_block_action( BlockInterface $block ) {
|
||||
try {
|
||||
/**
|
||||
* Action called after a block is added to a block container.
|
||||
*
|
||||
* This action can be used to perform actions after a block is added to the block container,
|
||||
* such as adding a dependent block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_after_add_block', $block );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->handle_exception_doing_action(
|
||||
'Error after adding block to template.',
|
||||
'woocommerce_block_template_after_add_block',
|
||||
$block,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_template_area_{template_area}_after_add_block_{block_id}` action.
|
||||
* Handle exceptions thrown by the action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
private function do_after_add_specific_block_action( BlockInterface $block ) {
|
||||
try {
|
||||
/**
|
||||
* Action called after a specific block is added to a template with a specific area.
|
||||
*
|
||||
* This action can be used to perform actions after a specific block is added to a template with a specific area,
|
||||
* such as adding a dependent block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}", $block );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->handle_exception_doing_action(
|
||||
'Error after adding block to template.',
|
||||
"woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_add_block_{$block->get_id()}",
|
||||
$block,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_template_after_remove_block` action.
|
||||
* Handle exceptions thrown by the action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
private function do_after_remove_block_action( BlockInterface $block ) {
|
||||
try {
|
||||
/**
|
||||
* Action called after a block is removed from a block container.
|
||||
*
|
||||
* This action can be used to perform actions after a block is removed from the block container,
|
||||
* such as removing a dependent block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( 'woocommerce_block_template_after_remove_block', $block );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->handle_exception_doing_action(
|
||||
'Error after removing block from template.',
|
||||
'woocommerce_block_template_after_remove_block',
|
||||
$block,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the `woocommerce_block_template_area_{template_area}_after_remove_block_{block_id}` action.
|
||||
* Handle exceptions thrown by the action.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*/
|
||||
private function do_after_remove_specific_block_action( BlockInterface $block ) {
|
||||
try {
|
||||
/**
|
||||
* Action called after a specific block is removed from a template with a specific area.
|
||||
*
|
||||
* This action can be used to perform actions after a specific block is removed from a template with a specific area,
|
||||
* such as removing a dependent block.
|
||||
*
|
||||
* @param BlockInterface $block The block.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
do_action( "woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}", $block );
|
||||
} catch ( \Exception $e ) {
|
||||
$this->handle_exception_doing_action(
|
||||
'Error after removing block from template.',
|
||||
"woocommerce_block_template_area_{$this->get_root_template()->get_area()}_after_remove_block_{$block->get_id()}",
|
||||
$block,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an exception thrown by an action.
|
||||
*
|
||||
* @param string $message The message.
|
||||
* @param string $action_tag The action tag.
|
||||
* @param BlockInterface $block The block.
|
||||
* @param \Exception $e The exception.
|
||||
*/
|
||||
private function handle_exception_doing_action( string $message, string $action_tag, BlockInterface $block, \Exception $e ) {
|
||||
BlockTemplateLogger::get_instance()->error(
|
||||
$message,
|
||||
[
|
||||
'exception' => $e,
|
||||
'action' => $action_tag,
|
||||
'container' => $this,
|
||||
'block' => $block,
|
||||
'template' => $this->get_root_template(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
/**
|
||||
* Trait for block formatted template.
|
||||
*/
|
||||
trait BlockFormattedTemplateTrait {
|
||||
/**
|
||||
* Get the block configuration as a formatted template.
|
||||
*
|
||||
* @return array The block configuration as a formatted template.
|
||||
*/
|
||||
public function get_formatted_template(): array {
|
||||
$arr = [
|
||||
$this->get_name(),
|
||||
array_merge(
|
||||
$this->get_attributes(),
|
||||
[
|
||||
'_templateBlockId' => $this->get_id(),
|
||||
'_templateBlockOrder' => $this->get_order(),
|
||||
],
|
||||
! empty( $this->get_hide_conditions() ) ? [
|
||||
'_templateBlockHideConditions' => $this->get_formatted_hide_conditions(),
|
||||
] : []
|
||||
),
|
||||
];
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block hide conditions formatted for inclusion in a formatted template.
|
||||
*/
|
||||
private function get_formatted_hide_conditions(): array {
|
||||
$formatted_hide_conditions = array_map(
|
||||
function( $hide_condition ) {
|
||||
return [
|
||||
'expression' => $hide_condition['expression'],
|
||||
];
|
||||
},
|
||||
array_values( $this->get_hide_conditions() )
|
||||
);
|
||||
|
||||
return $formatted_hide_conditions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\ContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Block template class.
|
||||
*/
|
||||
class BlockTemplate extends AbstractBlockTemplate {
|
||||
/**
|
||||
* Get the template ID.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return 'woocommerce-block-template';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inner block to this template.
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface {
|
||||
$block = new Block( $block_config, $this->get_root_template(), $this );
|
||||
return $this->add_inner_block( $block );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\BlockTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockTemplateInterface;
|
||||
|
||||
/**
|
||||
* Logger for block template modifications.
|
||||
*/
|
||||
class BlockTemplateLogger {
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var BlockTemplateLogger
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Logger instance.
|
||||
*
|
||||
* @var \WC_Logger
|
||||
*/
|
||||
protected $logger = null;
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*/
|
||||
public static function get_instance(): BlockTemplateLogger {
|
||||
if ( ! self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
protected function __construct() {
|
||||
$this->logger = wc_get_logger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an informational message.
|
||||
*
|
||||
* @param string $message Message to log.
|
||||
* @param array $info Additional info to log.
|
||||
*/
|
||||
public function info( string $message, array $info = [] ) {
|
||||
$this->logger->info(
|
||||
$this->format_message( $message, $info ),
|
||||
[ 'source' => 'block_template' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message.
|
||||
*
|
||||
* @param string $message Message to log.
|
||||
* @param array $info Additional info to log.
|
||||
*/
|
||||
public function warning( string $message, array $info = [] ) {
|
||||
$this->logger->warning(
|
||||
$this->format_message( $message, $info ),
|
||||
[ 'source' => 'block_template' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message.
|
||||
*
|
||||
* @param string $message Message to log.
|
||||
* @param array $info Additional info to log.
|
||||
*/
|
||||
public function error( string $message, array $info = [] ) {
|
||||
$this->logger->error(
|
||||
$this->format_message( $message, $info ),
|
||||
[ 'source' => 'block_template' ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a message for logging.
|
||||
*
|
||||
* @param string $message Message to log.
|
||||
* @param array $info Additional info to log.
|
||||
*/
|
||||
private function format_message( string $message, array $info = [] ): string {
|
||||
$formatted_message = sprintf(
|
||||
"%s\n%s",
|
||||
$message,
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
print_r( $this->format_info( $info ), true ),
|
||||
);
|
||||
|
||||
return $formatted_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format info for logging.
|
||||
*
|
||||
* @param array $info Info to log.
|
||||
*/
|
||||
private function format_info( array $info ): array {
|
||||
$formatted_info = $info;
|
||||
|
||||
if ( isset( $info['exception'] ) && $info['exception'] instanceof \Exception ) {
|
||||
$formatted_info['exception'] = $this->format_exception( $info['exception'] );
|
||||
}
|
||||
|
||||
if ( isset( $info['container'] ) ) {
|
||||
if ( $info['container'] instanceof BlockContainerInterface ) {
|
||||
$formatted_info['container'] = $this->format_block( $info['container'] );
|
||||
} elseif ( $info['container'] instanceof BlockTemplateInterface ) {
|
||||
$formatted_info['container'] = $this->format_template( $info['container'] );
|
||||
} elseif ( $info['container'] instanceof BlockInterface ) {
|
||||
$formatted_info['container'] = $this->format_block( $info['container'] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $info['block'] ) && $info['block'] instanceof BlockInterface ) {
|
||||
$formatted_info['block'] = $this->format_block( $info['block'] );
|
||||
}
|
||||
|
||||
if ( isset( $info['template'] ) && $info['template'] instanceof BlockTemplateInterface ) {
|
||||
$formatted_info['template'] = $this->format_template( $info['template'] );
|
||||
}
|
||||
|
||||
return $formatted_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an exception for logging.
|
||||
*
|
||||
* @param \Exception $exception Exception to format.
|
||||
*/
|
||||
private function format_exception( \Exception $exception ): array {
|
||||
return [
|
||||
'message' => $exception->getMessage(),
|
||||
'source' => "{$exception->getFile()}: {$exception->getLine()}",
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
'trace' => print_r( $this->format_exception_trace( $exception->getTrace() ), true ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an exception trace for logging.
|
||||
*
|
||||
* @param array $trace Exception trace to format.
|
||||
*/
|
||||
private function format_exception_trace( array $trace ): array {
|
||||
$formatted_trace = [];
|
||||
|
||||
foreach ( $trace as $source ) {
|
||||
$formatted_trace[] = "{$source['file']}: {$source['line']}";
|
||||
}
|
||||
|
||||
return $formatted_trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a block template for logging.
|
||||
*
|
||||
* @param BlockTemplateInterface $template Template to format.
|
||||
*/
|
||||
private function format_template( BlockTemplateInterface $template ): string {
|
||||
return "{$template->get_id()} (area: {$template->get_area()})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a block for logging.
|
||||
*
|
||||
* @param BlockInterface $block Block to format.
|
||||
*/
|
||||
private function format_block( BlockInterface $block ): string {
|
||||
return "{$block->get_id()} (name: {$block->get_name()})";
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@ use Automattic\WooCommerce\Internal\Admin\Notes\PerformanceOnMobile;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\PersonalizeStore;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\RealTimeOrderAlerts;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\UnsecuredReportFiles;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
|
||||
@@ -92,7 +91,6 @@ class Events {
|
||||
PerformanceOnMobile::class,
|
||||
PersonalizeStore::class,
|
||||
RealTimeOrderAlerts::class,
|
||||
TestCheckout::class,
|
||||
TrackingOptIn::class,
|
||||
WooCommercePayments::class,
|
||||
WooCommerceSubscriptions::class,
|
||||
@@ -156,7 +154,7 @@ class Events {
|
||||
MerchantEmailNotifications::run();
|
||||
}
|
||||
|
||||
if ( Features::is_enabled( 'onboarding' ) ) {
|
||||
if ( Features::is_enabled( 'core-profiler' ) ) {
|
||||
( new MailchimpScheduler() )->run();
|
||||
}
|
||||
}
|
||||
@@ -180,7 +178,14 @@ class Events {
|
||||
$note = clone $note_from_db;
|
||||
$note->set_title( $note_from_class->get_title() );
|
||||
$note->set_content( $note_from_class->get_content() );
|
||||
$note->set_actions( $note_from_class->get_actions() );
|
||||
$actions = $note_from_class->get_actions();
|
||||
foreach ( $actions as $action ) {
|
||||
$matching_action = $note->get_action( $action->name );
|
||||
if ( $matching_action && $matching_action->id ) {
|
||||
$action->id = $matching_action->id;
|
||||
}
|
||||
}
|
||||
$note->set_actions( $actions );
|
||||
return $note;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -14,7 +14,6 @@ use Automattic\WooCommerce\Internal\Admin\Notes\WooSubscriptionsNotes;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\TrackingOptIn;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\WooCommercePayments;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\InstallJPAndWCSPlugins;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\TestCheckout;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\SellingOnlineCourses;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MerchantEmailNotifications;
|
||||
use Automattic\WooCommerce\Internal\Admin\Notes\MagentoMigration;
|
||||
@@ -177,7 +176,6 @@ class FeaturePlugin {
|
||||
new TrackingOptIn();
|
||||
new WooCommercePayments();
|
||||
new InstallJPAndWCSPlugins();
|
||||
new TestCheckout();
|
||||
new SellingOnlineCourses();
|
||||
new MagentoMigration();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -72,6 +73,7 @@ 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' ) );
|
||||
@@ -91,6 +93,8 @@ class Loader {
|
||||
remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
|
||||
|
||||
add_action( 'admin_init', array( __CLASS__, 'deactivate_wc_admin_plugin' ) );
|
||||
|
||||
add_action( 'load-themes.php', array( __CLASS__, 'add_appearance_theme_view_tracks_event' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,7 +177,7 @@ class Loader {
|
||||
}
|
||||
|
||||
$classes = explode( ' ', trim( $admin_body_class ) );
|
||||
$classes[] = 'woocommerce-page';
|
||||
$classes[] = 'woocommerce-admin-page';
|
||||
if ( PageController::is_embed_page() ) {
|
||||
$classes[] = 'woocommerce-embed-page';
|
||||
}
|
||||
@@ -332,9 +336,7 @@ class Loader {
|
||||
}
|
||||
|
||||
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
|
||||
if ( class_exists( 'Jetpack' ) ) {
|
||||
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
|
||||
}
|
||||
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
|
||||
if ( ! empty( $preload_data_endpoints ) ) {
|
||||
$preload_data = array_reduce(
|
||||
array_values( $preload_data_endpoints ),
|
||||
@@ -569,4 +571,11 @@ class Loader {
|
||||
delete_option( 'woocommerce_onboarding_homepage_post_id' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appearance_theme_view Tracks event.
|
||||
*/
|
||||
public static function add_appearance_theme_view_tracks_event() {
|
||||
wc_admin_record_tracks_event( 'appearance_theme_view', array() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,36 +136,27 @@ class MarketingSpecs {
|
||||
/**
|
||||
* Load knowledge base posts from WooCommerce.com
|
||||
*
|
||||
* @param string|null $category Category of posts to retrieve.
|
||||
* @param string|null $topic The topic of marketing knowledgebase to retrieve.
|
||||
* @return array
|
||||
*/
|
||||
public function get_knowledge_base_posts( ?string $category ): array {
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT;
|
||||
|
||||
$categories = array(
|
||||
'marketing' => 1744,
|
||||
'coupons' => 25202,
|
||||
);
|
||||
|
||||
// Default to marketing category (if no category set on the kb component).
|
||||
if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) {
|
||||
$category_id = $categories[ $category ];
|
||||
$kb_transient = $kb_transient . '_' . strtolower( $category );
|
||||
} else {
|
||||
$category_id = $categories['marketing'];
|
||||
public function get_knowledge_base_posts( ?string $topic ): array {
|
||||
// Default to the marketing topic (if no topic is set on the kb component).
|
||||
if ( empty( $topic ) ) {
|
||||
$topic = 'marketing';
|
||||
}
|
||||
|
||||
$kb_transient = self::KNOWLEDGE_BASE_TRANSIENT . '_' . strtolower( $topic );
|
||||
|
||||
$posts = get_transient( $kb_transient );
|
||||
|
||||
if ( false === $posts ) {
|
||||
$request_url = add_query_arg(
|
||||
array(
|
||||
'categories' => $category_id,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
'page' => 1,
|
||||
'per_page' => 8,
|
||||
'_embed' => 1,
|
||||
),
|
||||
'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product'
|
||||
'https://woocommerce.com/wp-json/wccom/marketing-knowledgebase/v1/posts/' . $topic
|
||||
);
|
||||
|
||||
$request = wp_remote_get(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Marketplace.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin;
|
||||
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
|
||||
/**
|
||||
* Contains backend logic for the Marketplace feature.
|
||||
*/
|
||||
class Marketplace {
|
||||
|
||||
/**
|
||||
* Class initialization, to be executed when the class is resolved by the container.
|
||||
*/
|
||||
final public function init() {
|
||||
if ( FeaturesUtil::feature_is_enabled( 'marketplace' ) ) {
|
||||
add_action( 'admin_menu', array( $this, 'register_pages' ), 70 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers report pages.
|
||||
*/
|
||||
public function register_pages() {
|
||||
$marketplace_pages = self::get_marketplace_pages();
|
||||
foreach ( $marketplace_pages as $marketplace_page ) {
|
||||
if ( ! is_null( $marketplace_page ) ) {
|
||||
wc_admin_register_page( $marketplace_page );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get report pages.
|
||||
*/
|
||||
public static function get_marketplace_pages() {
|
||||
$marketplace_pages = array(
|
||||
array(
|
||||
'id' => 'woocommerce-marketplace',
|
||||
'parent' => 'woocommerce',
|
||||
'title' => __( 'Extensions', 'woocommerce' ),
|
||||
'path' => '/extensions',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The marketplace items used in the menu.
|
||||
*
|
||||
* @since 8.0
|
||||
*/
|
||||
return apply_filters( 'woocommerce_marketplace_menu_items', $marketplace_pages );
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Admin Test Checkout.
|
||||
*
|
||||
* Adds a note to remind the user to test their store checkout.
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Notes;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
|
||||
/**
|
||||
* Test_Checkout
|
||||
*/
|
||||
class TestCheckout {
|
||||
/**
|
||||
* Note traits.
|
||||
*/
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-admin-test-checkout';
|
||||
|
||||
/**
|
||||
* Completed tasks option name.
|
||||
*/
|
||||
const TASK_LIST_TRACKED_TASKS = 'woocommerce_task_list_tracked_completed_tasks';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'update_option_' . self::TASK_LIST_TRACKED_TASKS, array( $this, 'possibly_add_note' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*
|
||||
* @return Note|null
|
||||
*/
|
||||
public static function get_note() {
|
||||
$onboarding_profile = get_option( 'woocommerce_onboarding_profile', array() );
|
||||
|
||||
// Confirm that $onboarding_profile is set.
|
||||
if ( empty( $onboarding_profile ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that the person who filled out the OBW was not setting up
|
||||
// the store for their customer/client.
|
||||
if (
|
||||
! isset( $onboarding_profile['setup_client'] ) ||
|
||||
$onboarding_profile['setup_client']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure payments task was completed.
|
||||
$completed_tasks = get_option( self::TASK_LIST_TRACKED_TASKS, array() );
|
||||
if ( ! in_array( 'payments', $completed_tasks, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that products were added within the previous 1/2 hour.
|
||||
$query = new \WC_Product_Query(
|
||||
array(
|
||||
'limit' => 1,
|
||||
'status' => 'publish',
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$products = $query->get_products();
|
||||
if ( 0 === count( $products ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldest_product_timestamp = $products[0]->get_date_created()->getTimestamp();
|
||||
$half_hour_in_seconds = 30 * MINUTE_IN_SECONDS;
|
||||
if ( ( time() - $oldest_product_timestamp ) > $half_hour_in_seconds ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = __( 'Make sure that your checkout is working properly before you launch your store. Go through your checkout process in its entirety: from adding a product to your cart, choosing a shipping location, and making a payment.', 'woocommerce' );
|
||||
|
||||
$note = new Note();
|
||||
$note->set_title( __( 'Don\'t forget to test your checkout', 'woocommerce' ) );
|
||||
$note->set_content( $content );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-admin' );
|
||||
$note->add_action( 'test-checkout', __( 'Test checkout', 'woocommerce' ), wc_get_page_permalink( 'shop' ) );
|
||||
return $note;
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,10 @@ class COTRedirectionController {
|
||||
$params['_wpnonce'] = wp_create_nonce( 'bulk-posts' );
|
||||
}
|
||||
|
||||
// If an `order` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['order'] ) && is_array( $params['order'] ) ) {
|
||||
$params['post'] = $params['order'];
|
||||
unset( $params['order'] );
|
||||
// If an `id` array parameter is present, rename as `post`.
|
||||
if ( isset( $params['id'] ) && is_array( $params['id'] ) ) {
|
||||
$params['post'] = $params['id'];
|
||||
unset( $params['id'] );
|
||||
}
|
||||
|
||||
$params['post_type'] = 'shop_order';
|
||||
|
||||
@@ -208,7 +208,6 @@ class Edit {
|
||||
* @return void
|
||||
*/
|
||||
public function handle_order_update() {
|
||||
global $theorder;
|
||||
if ( ! isset( $this->order ) ) {
|
||||
return;
|
||||
}
|
||||
@@ -233,6 +232,8 @@ class Edit {
|
||||
*/
|
||||
do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order );
|
||||
|
||||
$this->custom_meta_box->handle_metadata_changes($this->order);
|
||||
|
||||
// Order updated message.
|
||||
$this->message = 1;
|
||||
|
||||
@@ -388,6 +389,7 @@ class Edit {
|
||||
<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
|
||||
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
|
||||
<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
|
||||
<input type="hidden" id="post_ID" name="post_ID" value="<?php echo esc_attr( $this->order->get_id() ); ?>"/>
|
||||
<div id="poststuff">
|
||||
<div id="post-body"
|
||||
class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
|
||||
|
||||
@@ -99,8 +99,10 @@ class EditLock {
|
||||
return $response;
|
||||
}
|
||||
|
||||
unset( $response['wp-refresh-post-lock'] );
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
if ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
if ( ! $order || ( ! current_user_can( get_post_type_object( $order->get_type() )->cap->edit_post, $order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,17 @@ class ListTable extends WP_List_Table {
|
||||
$title = esc_html( $post_type->labels->name );
|
||||
$add_new = esc_html( $post_type->labels->add_new );
|
||||
$new_page_link = $this->page_controller->get_new_page_url( $this->order_type );
|
||||
$search_label = '';
|
||||
|
||||
if ( ! empty( $this->order_query_args['s'] ) ) {
|
||||
$search_label = '<span class="subtitle">';
|
||||
$search_label .= sprintf(
|
||||
/* translators: %s: Search query. */
|
||||
__( 'Search results for: %s', 'woocommerce' ),
|
||||
'<strong>' . esc_html( $this->order_query_args['s'] ) . '</strong>'
|
||||
);
|
||||
$search_label .= '</span>';
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo wp_kses_post(
|
||||
@@ -237,6 +248,7 @@ class ListTable extends WP_List_Table {
|
||||
<div class='wrap'>
|
||||
<h1 class='wp-heading-inline'>{$title}</h1>
|
||||
<a href='" . esc_url( $new_page_link ) . "' class='page-title-action'>{$add_new}</a>
|
||||
{$search_label}
|
||||
<hr class='wp-header-end'>"
|
||||
);
|
||||
|
||||
@@ -711,20 +723,15 @@ class ListTable extends WP_List_Table {
|
||||
global $wpdb;
|
||||
|
||||
$orders_table = esc_sql( OrdersTableDataStore::get_orders_table_name() );
|
||||
$utc_offset = wc_timezone_offset();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$order_dates = $wpdb->get_results(
|
||||
"
|
||||
SELECT DISTINCT YEAR( date_created_gmt ) AS year,
|
||||
MONTH( date_created_gmt ) AS month
|
||||
|
||||
FROM $orders_table
|
||||
|
||||
WHERE status NOT IN (
|
||||
'trash'
|
||||
)
|
||||
|
||||
ORDER BY year DESC, month DESC;
|
||||
SELECT DISTINCT YEAR( t.date_created_local ) AS year,
|
||||
MONTH( t.date_created_local ) AS month
|
||||
FROM ( SELECT DATE_ADD( date_created_gmt, INTERVAL $utc_offset SECOND ) AS date_created_local FROM $orders_table WHERE status != 'trash' ) t
|
||||
ORDER BY year DESC, month DESC
|
||||
"
|
||||
);
|
||||
|
||||
@@ -867,7 +874,7 @@ class ListTable extends WP_List_Table {
|
||||
public function column_cb( $item ) {
|
||||
ob_start();
|
||||
?>
|
||||
<input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="<?php echo esc_attr( $this->_args['singular'] ); ?>[]" value="<?php echo esc_attr( $item->get_id() ); ?>" />
|
||||
<input id="cb-select-<?php echo esc_attr( $item->get_id() ); ?>" type="checkbox" name="id[]" value="<?php echo esc_attr( $item->get_id() ); ?>" />
|
||||
|
||||
<div class="locked-indicator">
|
||||
<span class="locked-indicator-icon" aria-hidden="true"></span>
|
||||
@@ -1197,7 +1204,7 @@ class ListTable extends WP_List_Table {
|
||||
|
||||
$action = 'delete';
|
||||
} else {
|
||||
$ids = isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['order'] ) ) : array();
|
||||
$ids = isset( $_REQUEST['id'] ) ? array_reverse( array_map( 'absint', (array) $_REQUEST['id'] ) ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1340,13 +1347,11 @@ 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 {
|
||||
$orders_store = wc_get_container()->get( OrdersTableDataStore::class );
|
||||
$delete_args = $force_delete ? array( 'force_delete' => true ) : array();
|
||||
$changed = 0;
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$order = wc_get_order( $id );
|
||||
$orders_store->delete( $order, $delete_args );
|
||||
$order->delete( $force_delete );
|
||||
$updated_order = wc_get_order( $id );
|
||||
|
||||
if ( ( $force_delete && false === $updated_order ) || ( ! $force_delete && $updated_order->get_status() === 'trash' ) ) {
|
||||
|
||||
@@ -242,21 +242,22 @@ class CustomMetaBox {
|
||||
* @return void
|
||||
*/
|
||||
private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) {
|
||||
$order_data_store = WC_Data_Store::load( 'order' );
|
||||
$count = 0;
|
||||
$count = 0;
|
||||
if ( is_protected_meta( $meta_key ) ) {
|
||||
wp_send_json_error( 'protected_meta' );
|
||||
wp_die();
|
||||
}
|
||||
$meta_id = $order_data_store->add_meta(
|
||||
$order,
|
||||
new WC_Meta_Data(
|
||||
array(
|
||||
'key' => $meta_key,
|
||||
'value' => $meta_value,
|
||||
)
|
||||
)
|
||||
);
|
||||
$metas_for_current_key = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) );
|
||||
$meta_ids = wp_list_pluck( $metas_for_current_key, 'id' );
|
||||
$order->add_meta_data( $meta_key, $meta_value );
|
||||
$order->save_meta_data();
|
||||
$metas_for_current_key_with_new = wp_list_filter( $order->get_meta_data(), array( 'key' => $meta_key ) );
|
||||
$meta_id = 0;
|
||||
$new_meta_ids = wp_list_pluck( $metas_for_current_key_with_new, 'id' );
|
||||
$new_meta_ids = array_values( array_diff( $new_meta_ids, $meta_ids ) );
|
||||
if ( count( $new_meta_ids ) > 0 ) {
|
||||
$meta_id = $new_meta_ids[0];
|
||||
}
|
||||
$response = new WP_Ajax_Response(
|
||||
array(
|
||||
'what' => 'meta',
|
||||
@@ -305,16 +306,9 @@ class CustomMetaBox {
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$order_data_store = WC_Data_Store::load( 'order' );
|
||||
$count = 0;
|
||||
$meta_object = new WC_Meta_Data(
|
||||
array(
|
||||
'id' => $mid,
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
)
|
||||
);
|
||||
$order_data_store->update_meta( $order, $meta_object );
|
||||
$count = 0;
|
||||
$order->update_meta_data( $key, $value, $mid );
|
||||
$order->save_meta_data();
|
||||
$response = new WP_Ajax_Response(
|
||||
array(
|
||||
'what' => 'meta',
|
||||
@@ -415,4 +409,52 @@ class CustomMetaBox {
|
||||
}
|
||||
wp_die( 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the possible changes in order metadata coming from an order edit page in admin
|
||||
* (labeled "custom fields" in the UI).
|
||||
*
|
||||
* This method expects the $_POST array to contain a 'meta' key that is an associative
|
||||
* array of [meta item id => [ 'key' => meta item name, 'value' => meta item value ];
|
||||
* and also to contain (possibly empty) 'metakeyinput' and 'metavalue' keys.
|
||||
*
|
||||
* @param WC_Order $order The order to handle.
|
||||
*/
|
||||
public function handle_metadata_changes( $order ) {
|
||||
$has_meta_changes = false;
|
||||
|
||||
$order_meta = $order->get_meta_data();
|
||||
|
||||
$order_meta =
|
||||
array_combine(
|
||||
array_map( fn( $meta ) => $meta->id, $order_meta ),
|
||||
$order_meta
|
||||
);
|
||||
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
|
||||
|
||||
foreach ( ( $_POST['meta'] ?? array() ) as $request_meta_id => $request_meta_data ) {
|
||||
$request_meta_id = wp_unslash( $request_meta_id );
|
||||
$request_meta_key = wp_unslash( $request_meta_data['key'] );
|
||||
$request_meta_value = wp_unslash( $request_meta_data['value'] );
|
||||
if ( array_key_exists( $request_meta_id, $order_meta ) &&
|
||||
( $order_meta[ $request_meta_id ]->key !== $request_meta_key || $order_meta[ $request_meta_id ]->value !== $request_meta_value ) ) {
|
||||
$order->update_meta_data( $request_meta_key, $request_meta_value, $request_meta_id );
|
||||
$has_meta_changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
$request_new_key = wp_unslash( $_POST['metakeyinput'] ?? '' );
|
||||
$request_new_value = wp_unslash( $_POST['metavalue'] ?? '' );
|
||||
if ( '' !== $request_new_key ) {
|
||||
$order->add_meta_data( $request_new_key, $request_new_value );
|
||||
$has_meta_changes = true;
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( $has_meta_changes ) {
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,11 +290,6 @@ class PageController {
|
||||
switch ( $this->current_action ) {
|
||||
case 'edit_order':
|
||||
case 'new_order':
|
||||
if ( ! isset( $this->order_edit_form ) ) {
|
||||
$this->order_edit_form = new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
}
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
$this->order_edit_form->display();
|
||||
break;
|
||||
case 'list_orders':
|
||||
@@ -340,6 +335,22 @@ class PageController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the order edit form for creating or editing an order.
|
||||
*
|
||||
* @see \Automattic\WooCommerce\Internal\Admin\Orders\Edit.
|
||||
* @since 8.1.0
|
||||
*/
|
||||
private function prepare_order_edit_form(): void {
|
||||
if ( ! $this->order || ! in_array( $this->current_action, array( 'new_order', 'edit_order' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->order_edit_form = $this->order_edit_form ?? new Edit();
|
||||
$this->order_edit_form->setup( $this->order );
|
||||
$this->order_edit_form->set_current_action( $this->current_action );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles initialization of the orders edit form.
|
||||
*
|
||||
@@ -351,6 +362,8 @@ class PageController {
|
||||
$this->verify_edit_permission();
|
||||
$this->handle_edit_lock();
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,6 +384,7 @@ class PageController {
|
||||
$this->order = new $order_class_name();
|
||||
$this->order->set_object_read( false );
|
||||
$this->order->set_status( 'auto-draft' );
|
||||
$this->order->set_created_via( 'admin' );
|
||||
$this->order->save();
|
||||
$this->handle_edit_lock();
|
||||
|
||||
@@ -380,6 +394,8 @@ class PageController {
|
||||
}
|
||||
|
||||
$theorder = $this->order;
|
||||
|
||||
$this->prepare_order_edit_form();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,7 @@ class PostsRedirectionController {
|
||||
$new_url = add_query_arg(
|
||||
array(
|
||||
'action' => $action,
|
||||
'order' => $posts,
|
||||
'id' => $posts,
|
||||
'_wp_http_referer' => $this->page_controller->get_orders_url(),
|
||||
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
|
||||
),
|
||||
|
||||
@@ -76,6 +76,7 @@ class DefaultFreeExtensions {
|
||||
self::get_plugin( 'mailpoet' ),
|
||||
self::get_plugin( 'google-listings-and-ads' ),
|
||||
self::get_plugin( 'woocommerce-services:tax' ),
|
||||
self::get_plugin( 'tiktok-for-business' ),
|
||||
)
|
||||
),
|
||||
),
|
||||
@@ -400,6 +401,16 @@ class DefaultFreeExtensions {
|
||||
'value' => 'SE',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'JP',
|
||||
'operation' => '=',
|
||||
),
|
||||
array(
|
||||
'type' => 'base_location_country',
|
||||
'value' => 'AE',
|
||||
'operation' => '=',
|
||||
),
|
||||
),
|
||||
),
|
||||
DefaultPaymentGateways::get_rules_for_cbd( false ),
|
||||
@@ -875,9 +886,9 @@ class DefaultFreeExtensions {
|
||||
'install_priority' => 3,
|
||||
),
|
||||
'jetpack' => array(
|
||||
'label' => __( 'Enhance security with Jetpack', 'woocommerce' ),
|
||||
'label' => __( 'Boost content creation with Jetpack AI Assistant', 'woocommerce' ),
|
||||
'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ),
|
||||
'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ),
|
||||
'description' => __( 'Save time on content creation — unlock high-quality blog posts and pages using AI.', 'woocommerce' ),
|
||||
'learn_more_link' => 'https://woocommerce.com/products/jetpack',
|
||||
'install_priority' => 8,
|
||||
),
|
||||
|
||||
@@ -40,7 +40,7 @@ class OrdersScheduler extends ImportScheduler {
|
||||
|
||||
// Order and refund data must be run on these hooks to ensure meta data is set.
|
||||
add_action( 'woocommerce_update_order', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
add_action( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
add_filter( 'woocommerce_create_order', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
add_action( 'woocommerce_refund_created', array( __CLASS__, 'possibly_schedule_import' ) );
|
||||
|
||||
OrdersStatsDataStore::init();
|
||||
@@ -210,13 +210,15 @@ AND status NOT IN ( 'wc-auto-draft', 'trash', 'auto-draft' )
|
||||
* @param int $order_id Post ID.
|
||||
*
|
||||
* @internal
|
||||
* @returns int The order id
|
||||
*/
|
||||
public static function possibly_schedule_import( $order_id ) {
|
||||
if ( ! OrderUtil::is_order( $order_id, array( 'shop_order' ) ) && 'woocommerce_refund_created' !== current_filter() ) {
|
||||
return;
|
||||
return $order_id;
|
||||
}
|
||||
|
||||
self::schedule_action( 'import', array( $order_id ) );
|
||||
return $order_id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ use Automattic\WooCommerce\Admin\API\Plugins;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Admin\API\Reports\Orders\DataStore as OrdersDataStore;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCPayPromotion\Init as WCPayPromotionInit;
|
||||
use Automattic\WooCommerce\Utilities\FeaturesUtil;
|
||||
use WC_Marketplace_Suggestions;
|
||||
|
||||
@@ -137,9 +138,7 @@ class Settings {
|
||||
|
||||
//phpcs:ignore
|
||||
$preload_data_endpoints = apply_filters( 'woocommerce_component_settings_preload_endpoints', array() );
|
||||
if ( class_exists( 'Jetpack' ) ) {
|
||||
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
|
||||
}
|
||||
$preload_data_endpoints['jetpackStatus'] = '/jetpack/v4/connection';
|
||||
if ( ! empty( $preload_data_endpoints ) ) {
|
||||
$preload_data = array_reduce(
|
||||
array_values( $preload_data_endpoints ),
|
||||
@@ -236,6 +235,8 @@ class Settings {
|
||||
|
||||
$settings['features'] = $this->get_features();
|
||||
|
||||
$settings['isWooPayEligible'] = WCPayPromotionInit::is_woopay_eligible();
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ class Translations {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! isset( $chunk_data['comment']['reference'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reference_file = $chunk_data['comment']['reference'];
|
||||
|
||||
// Only combine "app" files (not scripts registered with WP).
|
||||
|
||||
@@ -272,6 +272,7 @@ class WCAdminAssets {
|
||||
'wc-store-data',
|
||||
'wc-currency',
|
||||
'wc-navigation',
|
||||
'wc-block-templates',
|
||||
'wc-product-editor',
|
||||
);
|
||||
|
||||
@@ -297,6 +298,23 @@ class WCAdminAssets {
|
||||
$script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' );
|
||||
$script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename;
|
||||
|
||||
global $wp_version;
|
||||
if ( 'app' === $script_path_name && version_compare( $wp_version, '6.3', '<' ) ) {
|
||||
// Remove wp-router dependency for WordPress versions < 6.3 because wp-router is not included in those versions. We only use wp-router in customize store pages and the feature is only available in WordPress 6.3+.
|
||||
// We can remove this once our minimum support is WP 6.3.
|
||||
$script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-router' ) );
|
||||
}
|
||||
|
||||
// Remove wp-editor dependency if we're not on a customize store page since we don't use wp-editor in other pages.
|
||||
$is_customize_store_page = (
|
||||
PageController::is_admin_page() &&
|
||||
isset( $_GET['path'] ) &&
|
||||
str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' )
|
||||
);
|
||||
if ( ! $is_customize_store_page && WC_ADMIN_APP === $script ) {
|
||||
$script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-editor' ) );
|
||||
}
|
||||
|
||||
wp_register_script(
|
||||
$script,
|
||||
self::get_url( $script_path_name . '/index', 'js' ),
|
||||
@@ -330,6 +348,14 @@ class WCAdminAssets {
|
||||
);
|
||||
wp_style_add_data( 'wc-components', 'rtl', 'replace' );
|
||||
|
||||
wp_register_style(
|
||||
'wc-block-templates',
|
||||
self::get_url( 'block-templates/style', 'css' ),
|
||||
array(),
|
||||
$css_file_version
|
||||
);
|
||||
wp_style_add_data( 'wc-block-templates', 'rtl', 'replace' );
|
||||
|
||||
wp_register_style(
|
||||
'wc-product-editor',
|
||||
self::get_url( 'product-editor/style', 'css' ),
|
||||
@@ -398,6 +424,7 @@ class WCAdminAssets {
|
||||
'wc-date',
|
||||
'wc-components',
|
||||
'wc-tracks',
|
||||
'wc-block-templates',
|
||||
'wc-product-editor',
|
||||
];
|
||||
foreach ( $handles_for_injection as $handle ) {
|
||||
|
||||
@@ -141,6 +141,15 @@ class Init {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merchant WooPay eligibility.
|
||||
*/
|
||||
public static function is_woopay_eligible() {
|
||||
$wcpay_promotion = self::get_wc_pay_promotion_spec();
|
||||
|
||||
return $wcpay_promotion && 'woocommerce_payments:woopay' === $wcpay_promotion->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specs transient.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,8 @@ use Automattic\WooCommerce\Admin\PageController;
|
||||
* @package Automattic\WooCommerce\Admin\Features
|
||||
*/
|
||||
class WcPayWelcomePage {
|
||||
const TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
|
||||
const CACHE_TRANSIENT_NAME = 'wcpay_welcome_page_incentive';
|
||||
const HAD_WCPAY_OPTION_NAME = 'wcpay_was_in_use';
|
||||
|
||||
/**
|
||||
* Plugin instance.
|
||||
@@ -44,6 +45,7 @@ class WcPayWelcomePage {
|
||||
add_action( 'admin_menu', [ $this, 'register_payments_welcome_page' ] );
|
||||
add_filter( 'woocommerce_admin_shared_settings', [ $this, 'shared_settings' ] );
|
||||
add_filter( 'woocommerce_admin_allowed_promo_notes', [ $this, 'allowed_promo_notes' ] );
|
||||
add_filter( 'woocommerce_admin_woopayments_onboarding_task_badge', [ $this, 'onboarding_task_badge' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +54,11 @@ class WcPayWelcomePage {
|
||||
* @return boolean
|
||||
*/
|
||||
public function must_be_visible(): bool {
|
||||
// The WooPayments plugin must not be active.
|
||||
if ( $this->is_wcpay_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Suggestions not disabled via a setting.
|
||||
if ( get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) === 'no' ) {
|
||||
return false;
|
||||
@@ -68,12 +75,7 @@ class WcPayWelcomePage {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The WooPayments plugin must not be active.
|
||||
if ( $this->is_wcpay_active() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Incentive is available.
|
||||
// An incentive must be available.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
return false;
|
||||
}
|
||||
@@ -133,11 +135,17 @@ class WcPayWelcomePage {
|
||||
}
|
||||
|
||||
// Add badge.
|
||||
$badge = ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
foreach ( $menu as $index => $menu_item ) {
|
||||
if ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] ) {
|
||||
//phpcs:ignore
|
||||
$menu[ $index ][0] .= ' <span class="wcpay-menu-badge awaiting-mod count-1"><span class="plugin-count">1</span></span>';
|
||||
// Only add the badge markup if not already present and the menu item is the WooPayments menu item.
|
||||
if ( false === strpos( $menu_item[0], $badge )
|
||||
&& ( 'wc-admin&path=/wc-pay-welcome-page' === $menu_item[2]
|
||||
|| 'admin.php?page=wc-admin&path=/wc-pay-welcome-page' === $menu_item[2] )
|
||||
) {
|
||||
$menu[ $index ][0] .= $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
|
||||
// One menu item with a badge is more than enough.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,8 +162,8 @@ class WcPayWelcomePage {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -171,8 +179,8 @@ class WcPayWelcomePage {
|
||||
* @return array
|
||||
*/
|
||||
public function allowed_promo_notes( $promo_notes = [] ): array {
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $promo_notes;
|
||||
}
|
||||
|
||||
@@ -183,20 +191,50 @@ class WcPayWelcomePage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WooPayments payment gateway is active and set up,
|
||||
* Adds the WooPayments incentive badge to the onboarding task.
|
||||
*
|
||||
* @param string $badge Current badge.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function onboarding_task_badge( string $badge ): string {
|
||||
// Return early if the incentive must not be visible.
|
||||
if ( ! $this->must_be_visible() ) {
|
||||
return $badge;
|
||||
}
|
||||
|
||||
return $this->get_incentive()['task_badge'] ?? $badge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the WooPayments payment gateway is active and set up or was at some point,
|
||||
* or there are orders processed with it, at some moment.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function has_wcpay(): bool {
|
||||
// First, get the stored value, if it exists.
|
||||
// This way we avoid costly DB queries and API calls.
|
||||
// Basically, we only want to know if WooPayments was in use in the past.
|
||||
// Since the past can't be changed, neither can this value.
|
||||
$had_wcpay = get_option( self::HAD_WCPAY_OPTION_NAME );
|
||||
if ( false !== $had_wcpay ) {
|
||||
return $had_wcpay === 'yes';
|
||||
}
|
||||
|
||||
// We need to determine the value.
|
||||
// Start with the assumption that the store didn't have WooPayments in use.
|
||||
$had_wcpay = false;
|
||||
|
||||
// We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache.
|
||||
// This implies that WooPayments is or was active at some point and that it was connected.
|
||||
// This implies that WooPayments was active at some point and that it was connected.
|
||||
// If WooPayments is active right now, we will not get to this point since the plugin is active check is done first.
|
||||
if ( $this->has_wcpay_account_data() ) {
|
||||
return true;
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
// If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
|
||||
if ( ! empty(
|
||||
if ( false === $had_wcpay && ! empty(
|
||||
wc_get_orders(
|
||||
[
|
||||
'payment_method' => 'woocommerce_payments',
|
||||
@@ -205,10 +243,13 @@ class WcPayWelcomePage {
|
||||
]
|
||||
)
|
||||
) ) {
|
||||
return true;
|
||||
$had_wcpay = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Store the value for future use.
|
||||
update_option( self::HAD_WCPAY_OPTION_NAME, $had_wcpay ? 'yes' : 'no' );
|
||||
|
||||
return $had_wcpay;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,14 +281,21 @@ class WcPayWelcomePage {
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_incentive_dismissed(): bool {
|
||||
$dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] );
|
||||
|
||||
// If there are no dismissed incentives, return early.
|
||||
if ( empty( $dismissed_incentives ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return early if there is no eligible incentive.
|
||||
if ( empty( $this->get_incentive() ) ) {
|
||||
$incentive = $this->get_incentive();
|
||||
if ( empty( $incentive ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$dismissed_incentives = get_option( 'wcpay_welcome_page_incentives_dismissed', [] );
|
||||
|
||||
if ( in_array( $this->get_incentive()['id'], $dismissed_incentives, true ) ) {
|
||||
// Search the incentive ID in the dismissed incentives list.
|
||||
if ( in_array( $incentive['id'], $dismissed_incentives, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -265,6 +313,19 @@ class WcPayWelcomePage {
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Get the cached data.
|
||||
$cache = get_transient( self::CACHE_TRANSIENT_NAME );
|
||||
|
||||
// If the cached data is not expired and it's a WP_Error,
|
||||
// it means there was an API error previously and we should not retry just yet.
|
||||
if ( is_wp_error( $cache ) ) {
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Gather the store context data.
|
||||
$store_context = [
|
||||
// Store ISO-2 country code, e.g. `US`.
|
||||
'country' => WC()->countries->get_base_country(),
|
||||
@@ -293,25 +354,16 @@ class WcPayWelcomePage {
|
||||
|
||||
// Use the transient cached incentive if it exists, it is not expired,
|
||||
// and the store context hasn't changed since we last requested from the WooPayments API (based on context hash).
|
||||
$transient_cache = get_transient( self::TRANSIENT_NAME );
|
||||
if ( false !== $transient_cache ) {
|
||||
if ( is_null( $transient_cache ) ) {
|
||||
// This means there was an error and we shouldn't retry just yet.
|
||||
// Initialize the in-memory cache.
|
||||
$this->incentive = [];
|
||||
} elseif ( ! empty( $transient_cache['context_hash'] ) && is_string( $transient_cache['context_hash'] )
|
||||
&& hash_equals( $store_context_hash, $transient_cache['context_hash'] ) ) {
|
||||
if ( false !== $cache
|
||||
&& ! empty( $cache['context_hash'] ) && is_string( $cache['context_hash'] )
|
||||
&& hash_equals( $store_context_hash, $cache['context_hash'] ) ) {
|
||||
|
||||
// We have a store context hash and it matches with the current context one.
|
||||
// We can use the cached incentive data.
|
||||
// Store the incentive in the in-memory cache.
|
||||
$this->incentive = $transient_cache['incentive'] ?? [];
|
||||
}
|
||||
// We have a store context hash and it matches with the current context one.
|
||||
// We can use the cached incentive data.
|
||||
// Store the incentive in the in-memory cache and return it.
|
||||
$this->incentive = $cache['incentive'] ?? [];
|
||||
|
||||
// If the in-memory cache has been set, return it.
|
||||
if ( isset( $this->incentive ) ) {
|
||||
return $this->incentive;
|
||||
}
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// By this point, we have an expired transient or the store context has changed.
|
||||
@@ -323,16 +375,22 @@ class WcPayWelcomePage {
|
||||
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
array(
|
||||
[
|
||||
'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ),
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
// Return early if there is an error, waiting 6 hours before the next attempt.
|
||||
if ( is_wp_error( $response ) ) {
|
||||
// Store a null value in the transient so we know this is due to an API error.
|
||||
set_transient( self::TRANSIENT_NAME, null, HOUR_IN_SECONDS * 6 );
|
||||
// Initialize the in-memory cache.
|
||||
// Store a trimmed down, lightweight error.
|
||||
$error = new \WP_Error(
|
||||
$response->get_error_code(),
|
||||
$response->get_error_message(),
|
||||
wp_remote_retrieve_response_code( $response )
|
||||
);
|
||||
// Store the error in the transient so we know this is due to an API error.
|
||||
set_transient( self::CACHE_TRANSIENT_NAME, $error, HOUR_IN_SECONDS * 6 );
|
||||
// Initialize the in-memory cache and return it.
|
||||
$this->incentive = [];
|
||||
|
||||
return $this->incentive;
|
||||
@@ -362,19 +420,21 @@ class WcPayWelcomePage {
|
||||
// Skip transient cache if `cache-for` header equals zero.
|
||||
if ( '0' === $cache_for ) {
|
||||
// If we have a transient cache that is not expired, delete it so there are no leftovers.
|
||||
if ( false !== $transient_cache ) {
|
||||
delete_transient( self::TRANSIENT_NAME );
|
||||
if ( false !== $cache ) {
|
||||
delete_transient( self::CACHE_TRANSIENT_NAME );
|
||||
}
|
||||
|
||||
return $this->incentive;
|
||||
}
|
||||
|
||||
// Store incentive in transient cache (together with the context hash) for the given number of seconds or 24h.
|
||||
// Store incentive in transient cache (together with the context hash) for the given number of seconds
|
||||
// or 1 day in seconds. Also attach a timestamp to the transient data so we know when we last fetched.
|
||||
set_transient(
|
||||
self::TRANSIENT_NAME,
|
||||
self::CACHE_TRANSIENT_NAME,
|
||||
[
|
||||
'incentive' => $this->incentive,
|
||||
'context_hash' => $store_context_hash,
|
||||
'timestamp' => time(),
|
||||
],
|
||||
! empty( $cache_for ) ? (int) $cache_for : DAY_IN_SECONDS
|
||||
);
|
||||
|
||||
@@ -99,14 +99,27 @@ class BatchProcessingController {
|
||||
* @param bool $unique Whether to make the action unique.
|
||||
*/
|
||||
private function schedule_watchdog_action( bool $with_delay = false, bool $unique = false ): void {
|
||||
$time = $with_delay ? time() + HOUR_IN_SECONDS : time();
|
||||
as_schedule_single_action(
|
||||
$time,
|
||||
self::WATCHDOG_ACTION_NAME,
|
||||
array(),
|
||||
self::ACTION_GROUP,
|
||||
$unique
|
||||
);
|
||||
$time = time();
|
||||
if ( $with_delay ) {
|
||||
/**
|
||||
* Modify the delay interval for the batch processor's watchdog events.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param int $delay Time, in seconds, before the watchdog process will run. Defaults to 3600 (1 hour).
|
||||
*/
|
||||
$time += apply_filters( 'woocommerce_batch_processor_watchdog_delay_seconds', HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
if ( ! as_has_scheduled_action( self::WATCHDOG_ACTION_NAME ) ) {
|
||||
as_schedule_single_action(
|
||||
$time,
|
||||
self::WATCHDOG_ACTION_NAME,
|
||||
array(),
|
||||
self::ACTION_GROUP,
|
||||
$unique
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,7 +81,7 @@ abstract class CustomMetaDataStore {
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_meta( &$object, $meta ) {
|
||||
public function delete_meta( &$object, $meta ) : bool {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! isset( $meta->id ) ) {
|
||||
@@ -99,7 +99,8 @@ abstract class CustomMetaDataStore {
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing ->key and ->value).
|
||||
* @return int meta ID
|
||||
*
|
||||
* @return int|false meta ID
|
||||
*/
|
||||
public function add_meta( &$object, $meta ) {
|
||||
global $wpdb;
|
||||
@@ -132,7 +133,7 @@ abstract class CustomMetaDataStore {
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update_meta( &$object, $meta ) {
|
||||
public function update_meta( &$object, $meta ) : bool {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! isset( $meta->id ) || empty( $meta->key ) ) {
|
||||
@@ -194,4 +195,41 @@ abstract class CustomMetaDataStore {
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves metadata by meta key.
|
||||
*
|
||||
* @param \WC_Abstract_Order $object Object ID.
|
||||
* @param string $meta_key Meta key.
|
||||
*
|
||||
* @return \stdClass|bool Metadata object or FALSE if not found.
|
||||
*/
|
||||
public function get_metadata_by_key( &$object, string $meta_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$db_info = $this->get_db_info();
|
||||
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$meta = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT {$db_info['meta_id_field']}, meta_key, meta_value, {$db_info['object_id_field']} FROM {$db_info['table']} WHERE meta_key = %s AND {$db_info['object_id_field']} = %d",
|
||||
$meta_key,
|
||||
$object->get_id(),
|
||||
)
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
|
||||
if ( empty( $meta ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ( $meta as $row ) {
|
||||
if ( isset( $row->meta_value ) ) {
|
||||
$row->meta_value = maybe_unserialize( $row->meta_value );
|
||||
}
|
||||
}
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,20 +5,19 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Caches\OrderCache;
|
||||
use Automattic\WooCommerce\Caches\OrderCacheController;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use ActionScheduler;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* This is the main class that controls the custom orders tables feature. Its responsibilities are:
|
||||
*
|
||||
* - Allowing to enable and disable the feature while it's in development (show_feature method)
|
||||
* - Displaying UI components (entries in the tools page and in settings)
|
||||
* - Providing the proper data store for orders via 'woocommerce_order_data_store' hook
|
||||
*
|
||||
@@ -28,6 +27,8 @@ class CustomOrdersTableController {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
private const SYNC_QUERY_ARG = 'wc_hpos_sync_now';
|
||||
|
||||
/**
|
||||
* The name of the option for enabling the usage of the custom orders tables
|
||||
*/
|
||||
@@ -117,10 +118,10 @@ class CustomOrdersTableController {
|
||||
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
|
||||
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
|
||||
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
|
||||
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_data_sync_option_changed' ), 10, 1 );
|
||||
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
|
||||
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enabled_changed' ), 10, 2 );
|
||||
self::add_action( 'woocommerce_feature_setting', array( $this, 'get_hpos_feature_setting' ), 10, 2 );
|
||||
self::add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) );
|
||||
self::add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) );
|
||||
self::add_action( 'woocommerce_register_feature_definitions', array( $this, 'add_feature_definition' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,33 +157,6 @@ class CustomOrdersTableController {
|
||||
$this->plugin_util = $plugin_util;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the feature is visible (so that dedicated entries will be added to the debug tools page).
|
||||
*
|
||||
* @return bool True if the feature is visible.
|
||||
*/
|
||||
public function is_feature_visible(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the feature visible, so that dedicated entries will be added to the debug tools page.
|
||||
*
|
||||
* This method shouldn't be used anymore, see the FeaturesController class.
|
||||
*/
|
||||
public function show_feature() {
|
||||
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
|
||||
wc_doing_it_wrong(
|
||||
$class_and_method,
|
||||
sprintf(
|
||||
// translators: %1$s the name of the class and method used.
|
||||
__( '%1$s: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.', 'woocommerce' ),
|
||||
$class_and_method
|
||||
),
|
||||
'7.0'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the custom orders table usage enabled via settings?
|
||||
* This can be true only if the feature is enabled and a table regeneration has been completed.
|
||||
@@ -190,7 +164,7 @@ class CustomOrdersTableController {
|
||||
* @return bool True if the custom orders table usage is enabled
|
||||
*/
|
||||
public function custom_orders_table_usage_is_enabled(): bool {
|
||||
return $this->is_feature_visible() && get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
|
||||
return get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,23 +250,6 @@ class CustomOrdersTableController {
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the custom orders tables in response to the user pressing the tool button.
|
||||
*
|
||||
* @param bool $verify_nonce True to perform the nonce verification, false to skip it.
|
||||
*
|
||||
* @throws \Exception Can't create the tables.
|
||||
*/
|
||||
private function create_custom_orders_tables( bool $verify_nonce = true ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
if ( $verify_nonce && ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) ) {
|
||||
throw new \Exception( 'Invalid nonce' );
|
||||
}
|
||||
|
||||
$this->data_synchronizer->create_database_tables();
|
||||
update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the custom orders tables and any related options and data in response to the user pressing the tool button.
|
||||
*
|
||||
@@ -336,12 +293,19 @@ class CustomOrdersTableController {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) {
|
||||
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || 'no' === $value ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$this->order_cache->flush();
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
$this->data_synchronizer->create_database_tables();
|
||||
}
|
||||
|
||||
$tables_created = get_option( DataSynchronizer::ORDERS_TABLE_CREATED ) === 'yes';
|
||||
if ( ! $tables_created ) {
|
||||
return 'no';
|
||||
}
|
||||
/**
|
||||
* Re-enable the following code once the COT to posts table sync is implemented (it's currently commented out to ease testing).
|
||||
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
|
||||
@@ -354,47 +318,32 @@ class CustomOrdersTableController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the all settings updated hook.
|
||||
* Callback to trigger a sync immediately by clicking a button on the Features screen.
|
||||
*
|
||||
* @param string $feature_id Feature ID.
|
||||
* @return void
|
||||
*/
|
||||
private function handle_data_sync_option_changed( string $feature_id ) {
|
||||
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION !== $feature_id ) {
|
||||
private function sync_now() {
|
||||
$section = filter_input( INPUT_GET, 'section' );
|
||||
if ( 'features' !== $section ) {
|
||||
return;
|
||||
}
|
||||
$data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled();
|
||||
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
$this->data_synchronizer->create_database_tables();
|
||||
}
|
||||
|
||||
// Enabling/disabling the sync implies starting/stopping it too, if needed.
|
||||
// We do this check here, and not in process_pre_update_option, so that if for some reason
|
||||
// the setting is enabled but no sync is in process, sync will start by just saving the
|
||||
// settings even without modifying them (and the opposite: sync will be stopped if for
|
||||
// some reason it was ongoing while it was disabled).
|
||||
if ( $data_sync_is_enabled ) {
|
||||
if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) {
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
} else {
|
||||
$this->batch_processing_controller->remove_processor( DataSynchronizer::class );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the 'woocommerce_feature_enabled_changed' action,
|
||||
* if the custom orders table feature is enabled create the database tables if they don't exist.
|
||||
* Tell WP Admin to remove the sync query arg from the URL.
|
||||
*
|
||||
* @param string $feature_id The id of the feature that is being enabled or disabled.
|
||||
* @param bool $is_enabled True if the feature is being enabled, false if it's being disabled.
|
||||
* @param array $query_args The query args that are removable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function handle_feature_enabled_changed( $feature_id, $is_enabled ): void {
|
||||
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $feature_id || ! $is_enabled ) {
|
||||
return;
|
||||
}
|
||||
private function register_removable_query_arg( $query_args ) {
|
||||
$query_args[] = self::SYNC_QUERY_ARG;
|
||||
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
$this->create_custom_orders_tables( false );
|
||||
}
|
||||
return $query_args;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,105 +378,160 @@ class CustomOrdersTableController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HPOS setting for rendering in Features section of the settings page.
|
||||
* Add the definition for the HPOS feature.
|
||||
*
|
||||
* @param array $feature_setting HPOS feature value as defined in the feature controller.
|
||||
* @param string $feature_id ID of the feature.
|
||||
* @param FeaturesController $features_controller The instance of FeaturesController.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
* @return void
|
||||
*/
|
||||
private function get_hpos_feature_setting( array $feature_setting, string $feature_id ) {
|
||||
if ( ! in_array( $feature_id, array( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, 'custom_order_tables' ), true ) ) {
|
||||
return $feature_setting;
|
||||
}
|
||||
private function add_feature_definition( $features_controller ) {
|
||||
$definition = array(
|
||||
'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
'is_experimental' => false,
|
||||
'enabled_by_default' => false,
|
||||
'order' => 50,
|
||||
'setting' => $this->get_hpos_setting_for_feature(),
|
||||
'additional_settings' => array(
|
||||
$this->get_hpos_setting_for_sync(),
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'yes' === get_transient( 'wc_installing' ) ) {
|
||||
return $feature_setting;
|
||||
}
|
||||
|
||||
$sync_status = $this->data_synchronizer->get_sync_status();
|
||||
switch ( $feature_id ) {
|
||||
case self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
|
||||
return $this->get_hpos_setting_for_feature( $sync_status );
|
||||
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
|
||||
return $this->get_hpos_setting_for_sync( $sync_status );
|
||||
case 'custom_order_tables':
|
||||
return array();
|
||||
}
|
||||
$features_controller->add_feature_definition(
|
||||
'custom_order_tables',
|
||||
__( 'High-Performance order storage', 'woocommerce' ),
|
||||
$definition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page.
|
||||
*
|
||||
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
*/
|
||||
private function get_hpos_setting_for_feature( $sync_status ) {
|
||||
$hpos_enabled = $this->custom_orders_table_usage_is_enabled();
|
||||
$plugin_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
$plugin_incompat_warning = $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_info );
|
||||
$sync_complete = 0 === $sync_status['current_pending_count'];
|
||||
$disabled_option = array();
|
||||
if ( count( array_merge( $plugin_info['uncertain'], $plugin_info['incompatible'] ) ) > 0 ) {
|
||||
$disabled_option = array( 'yes' );
|
||||
}
|
||||
if ( ! $sync_complete ) {
|
||||
$disabled_option = array( 'yes', 'no' );
|
||||
private function get_hpos_setting_for_feature() {
|
||||
if ( 'yes' === get_transient( 'wc_installing' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$get_value = function() {
|
||||
return $this->custom_orders_table_usage_is_enabled() ? 'yes' : 'no';
|
||||
};
|
||||
|
||||
/**
|
||||
* ⚠️The FeaturesController instance must only be accessed from within the callback functions. Otherwise it
|
||||
* gets called while it's still being instantiated and creates and endless loop.
|
||||
*/
|
||||
|
||||
$get_desc = function() {
|
||||
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
|
||||
return $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_compatibility );
|
||||
};
|
||||
|
||||
$get_disabled = function() {
|
||||
$plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true );
|
||||
$sync_status = $this->data_synchronizer->get_sync_status();
|
||||
$sync_complete = 0 === $sync_status['current_pending_count'];
|
||||
$disabled = array();
|
||||
// Changing something here? might also want to look at `enable|disable` functions in CLIRunner.
|
||||
if ( count( array_merge( $plugin_compatibility['uncertain'], $plugin_compatibility['incompatible'] ) ) > 0 ) {
|
||||
$disabled = array( 'yes' );
|
||||
}
|
||||
if ( ! $sync_complete ) {
|
||||
$disabled = array( 'yes', 'no' );
|
||||
}
|
||||
|
||||
return $disabled;
|
||||
};
|
||||
|
||||
return array(
|
||||
'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
'title' => __( 'Data storage for orders', 'woocommerce' ),
|
||||
'title' => __( 'Order data storage', 'woocommerce' ),
|
||||
'type' => 'radio',
|
||||
'options' => array(
|
||||
'no' => __( 'WordPress post tables', 'woocommerce' ),
|
||||
'yes' => __( 'High performance order storage (new)', 'woocommerce' ),
|
||||
'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ),
|
||||
'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ),
|
||||
),
|
||||
'value' => $hpos_enabled ? 'yes' : 'no',
|
||||
'disabled' => $disabled_option,
|
||||
'desc' => $plugin_incompat_warning,
|
||||
'value' => $get_value,
|
||||
'disabled' => $get_disabled,
|
||||
'desc' => $get_desc,
|
||||
'desc_at_end' => true,
|
||||
'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the setting for rendering sync enabling setting block in Features section of the settings page.
|
||||
*
|
||||
* @param array $sync_status Details of sync status, includes pending count, and count when sync started.
|
||||
*
|
||||
* @return array Feature setting object.
|
||||
*/
|
||||
private function get_hpos_setting_for_sync( $sync_status ) {
|
||||
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
|
||||
$sync_enabled = get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
|
||||
$sync_message = '';
|
||||
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_message = sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
__( 'Currently syncing orders... %d pending', 'woocommerce' ),
|
||||
$sync_status['current_pending_count']
|
||||
);
|
||||
} elseif ( $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_message = sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
_n(
|
||||
'Sync %d pending order. You can switch data storage for orders only when posts and orders table are in sync.',
|
||||
'Sync %d pending orders. You can switch data storage for orders only when posts and orders table are in sync.',
|
||||
$sync_status['current_pending_count'],
|
||||
'woocommerce'
|
||||
),
|
||||
$sync_status['current_pending_count'],
|
||||
);
|
||||
private function get_hpos_setting_for_sync() {
|
||||
if ( 'yes' === get_transient( 'wc_installing' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$get_value = function() {
|
||||
return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION );
|
||||
};
|
||||
|
||||
$get_sync_message = function() {
|
||||
$sync_status = $this->data_synchronizer->get_sync_status();
|
||||
$sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) );
|
||||
$sync_enabled = $this->data_synchronizer->data_sync_is_enabled();
|
||||
$sync_message = array();
|
||||
|
||||
if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) {
|
||||
$sync_message[] = __( 'Background sync is enabled.', 'woocommerce' );
|
||||
}
|
||||
|
||||
if ( $sync_in_progress && $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_message[] = sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
__( 'Currently syncing orders... %d pending', 'woocommerce' ),
|
||||
$sync_status['current_pending_count']
|
||||
);
|
||||
} elseif ( $sync_status['current_pending_count'] > 0 ) {
|
||||
$sync_now_url = add_query_arg(
|
||||
array(
|
||||
self::SYNC_QUERY_ARG => true,
|
||||
),
|
||||
wc_get_container()->get( FeaturesController::class )->get_features_page_url()
|
||||
);
|
||||
|
||||
$sync_message[] = wp_kses_data(
|
||||
__(
|
||||
'You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>.',
|
||||
'woocommerce'
|
||||
)
|
||||
);
|
||||
|
||||
$sync_message[] = sprintf(
|
||||
'<a href="%1$s" class="button button-link">%2$s</a>',
|
||||
esc_url( $sync_now_url ),
|
||||
sprintf(
|
||||
// translators: %d: number of pending orders.
|
||||
_n(
|
||||
'Sync %s pending order',
|
||||
'Sync %s pending orders',
|
||||
$sync_status['current_pending_count'],
|
||||
'woocommerce'
|
||||
),
|
||||
number_format_i18n( $sync_status['current_pending_count'] )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return implode( '<br />', $sync_message );
|
||||
};
|
||||
|
||||
return array(
|
||||
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
'title' => '',
|
||||
'type' => 'checkbox',
|
||||
'desc' => __( 'Keep the posts and orders tables in sync (compatibility mode).', 'woocommerce' ),
|
||||
'value' => $sync_enabled,
|
||||
'desc_tip' => $sync_message,
|
||||
'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
'title' => '',
|
||||
'type' => 'checkbox',
|
||||
'desc' => __( 'Enable compatibility mode (synchronizes orders to the posts table).', 'woocommerce' ),
|
||||
'value' => $get_value,
|
||||
'desc_tip' => $get_sync_message,
|
||||
'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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\BatchProcessing\BatchProcessorInterface;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\{ BatchProcessingController, BatchProcessorInterface };
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
@@ -45,6 +45,13 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
public const ID_TYPE_DELETED_FROM_ORDERS_TABLE = 3;
|
||||
public const ID_TYPE_DELETED_FROM_POSTS_TABLE = 4;
|
||||
|
||||
public const BACKGROUND_SYNC_MODE_OPTION = 'woocommerce_custom_orders_table_background_sync_mode';
|
||||
public const BACKGROUND_SYNC_INTERVAL_OPTION = 'woocommerce_custom_orders_table_background_sync_interval';
|
||||
public const BACKGROUND_SYNC_MODE_INTERVAL = 'interval';
|
||||
public const BACKGROUND_SYNC_MODE_CONTINUOUS = 'continuous';
|
||||
public const BACKGROUND_SYNC_MODE_OFF = 'off';
|
||||
public const BACKGROUND_SYNC_EVENT_HOOK = 'woocommerce_custom_orders_table_background_sync';
|
||||
|
||||
/**
|
||||
* The data store object to use.
|
||||
*
|
||||
@@ -80,6 +87,13 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
*/
|
||||
private $order_cache_controller;
|
||||
|
||||
/**
|
||||
* The batch processing controller.
|
||||
*
|
||||
* @var BatchProcessingController
|
||||
*/
|
||||
private $batch_processing_controller;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
@@ -89,6 +103,13 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
self::add_action( 'woocommerce_refund_created', array( $this, 'handle_updated_order' ), 100 );
|
||||
self::add_action( 'woocommerce_update_order', array( $this, 'handle_updated_order' ), 100 );
|
||||
self::add_action( 'wp_scheduled_auto_draft_delete', array( $this, 'delete_auto_draft_orders' ), 9 );
|
||||
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
|
||||
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 2 );
|
||||
self::add_filter( 'deleted_option', array( $this, 'process_deleted_option' ), 999 );
|
||||
self::add_action( self::BACKGROUND_SYNC_EVENT_HOOK, array( $this, 'handle_interval_background_sync' ) );
|
||||
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS === $this->get_background_sync_mode() ) {
|
||||
self::add_action( 'shutdown', array( $this, 'handle_continuous_background_sync' ) );
|
||||
}
|
||||
|
||||
self::add_filter( 'woocommerce_feature_description_tip', array( $this, 'handle_feature_description_tip' ), 10, 3 );
|
||||
}
|
||||
@@ -101,6 +122,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
* @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
|
||||
* @param LegacyProxy $legacy_proxy The legacy proxy instance to use.
|
||||
* @param OrderCacheController $order_cache_controller The order cache controller instance to use.
|
||||
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
|
||||
* @internal
|
||||
*/
|
||||
final public function init(
|
||||
@@ -108,13 +130,15 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
DatabaseUtil $database_util,
|
||||
PostsToOrdersMigrationController $posts_to_cot_migrator,
|
||||
LegacyProxy $legacy_proxy,
|
||||
OrderCacheController $order_cache_controller
|
||||
OrderCacheController $order_cache_controller,
|
||||
BatchProcessingController $batch_processing_controller
|
||||
) {
|
||||
$this->data_store = $data_store;
|
||||
$this->database_util = $database_util;
|
||||
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
|
||||
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
|
||||
$this->order_cache_controller = $order_cache_controller;
|
||||
$this->data_store = $data_store;
|
||||
$this->database_util = $database_util;
|
||||
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
|
||||
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
|
||||
$this->order_cache_controller = $order_cache_controller;
|
||||
$this->batch_processing_controller = $batch_processing_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,11 +175,19 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the custom orders database tables.
|
||||
* Create the custom orders database tables and log an error if that's not possible.
|
||||
*
|
||||
* @return bool True if all the tables were successfully created, false otherwise.
|
||||
*/
|
||||
public function create_database_tables() {
|
||||
$this->database_util->dbdelta( $this->data_store->get_database_schema() );
|
||||
$this->check_orders_table_exists();
|
||||
$success = $this->check_orders_table_exists();
|
||||
if ( ! $success ) {
|
||||
$missing_tables = $this->database_util->get_missing_tables( $this->data_store->get_database_schema() );
|
||||
$missing_tables = implode( ', ', $missing_tables );
|
||||
$this->error_logger->error( "HPOS tables are missing in the database and couldn't be created. The missing tables are: $missing_tables" );
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +203,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the data sync between old and new tables currently enabled?
|
||||
* Is the real-time data sync between old and new tables currently enabled?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -179,6 +211,181 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current background data sync mode.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_background_sync_mode(): string {
|
||||
$default = $this->data_sync_is_enabled() ? self::BACKGROUND_SYNC_MODE_INTERVAL : self::BACKGROUND_SYNC_MODE_OFF;
|
||||
|
||||
return get_option( self::BACKGROUND_SYNC_MODE_OPTION, $default );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the background data sync between old and new tables currently enabled?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function background_sync_is_enabled(): bool {
|
||||
$enabled_modes = array( self::BACKGROUND_SYNC_MODE_INTERVAL, self::BACKGROUND_SYNC_MODE_CONTINUOUS );
|
||||
$mode = $this->get_background_sync_mode();
|
||||
|
||||
return in_array( $mode, $enabled_modes, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an option change for specific keys.
|
||||
*
|
||||
* @param string $option_key The option key.
|
||||
* @param string $old_value The previous value.
|
||||
* @param string $new_value The new value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_updated_option( $option_key, $old_value, $new_value ) {
|
||||
$sync_option_keys = array( self::ORDERS_DATA_SYNC_ENABLED_OPTION, self::BACKGROUND_SYNC_MODE_OPTION );
|
||||
if ( ! in_array( $option_key, $sync_option_keys, true ) || $new_value === $old_value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self::BACKGROUND_SYNC_MODE_OPTION === $option_key ) {
|
||||
$mode = $new_value;
|
||||
} else {
|
||||
$mode = $this->get_background_sync_mode();
|
||||
}
|
||||
switch ( $mode ) {
|
||||
case self::BACKGROUND_SYNC_MODE_INTERVAL:
|
||||
$this->schedule_background_sync();
|
||||
break;
|
||||
|
||||
case self::BACKGROUND_SYNC_MODE_CONTINUOUS:
|
||||
case self::BACKGROUND_SYNC_MODE_OFF:
|
||||
default:
|
||||
$this->unschedule_background_sync();
|
||||
break;
|
||||
}
|
||||
|
||||
if ( self::ORDERS_DATA_SYNC_ENABLED_OPTION === $option_key ) {
|
||||
if ( ! $this->check_orders_table_exists() ) {
|
||||
$this->create_database_tables();
|
||||
}
|
||||
|
||||
if ( $this->data_sync_is_enabled() ) {
|
||||
$this->batch_processing_controller->enqueue_processor( self::class );
|
||||
} else {
|
||||
$this->batch_processing_controller->remove_processor( self::class );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an option change when the key didn't exist before.
|
||||
*
|
||||
* @param string $option_key The option key.
|
||||
* @param string $value The new value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_added_option( $option_key, $value ) {
|
||||
$this->process_updated_option( $option_key, false, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an option deletion for specific keys.
|
||||
*
|
||||
* @param string $option_key The option key.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_deleted_option( $option_key ) {
|
||||
if ( self::BACKGROUND_SYNC_MODE_OPTION !== $option_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->unschedule_background_sync();
|
||||
$this->batch_processing_controller->remove_processor( self::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time interval, in seconds, between background syncs.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_background_sync_interval(): int {
|
||||
$interval = filter_var(
|
||||
get_option( self::BACKGROUND_SYNC_INTERVAL_OPTION, HOUR_IN_SECONDS ),
|
||||
FILTER_VALIDATE_INT,
|
||||
array(
|
||||
'options' => array(
|
||||
'default' => HOUR_IN_SECONDS,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an event to run background sync when the mode is set to interval.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function schedule_background_sync() {
|
||||
$interval = $this->get_background_sync_interval();
|
||||
|
||||
// Calling Action Scheduler directly because WC_Action_Queue doesn't support the unique parameter yet.
|
||||
as_schedule_recurring_action(
|
||||
time() + $interval,
|
||||
$interval,
|
||||
self::BACKGROUND_SYNC_EVENT_HOOK,
|
||||
array(),
|
||||
'',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any pending background sync events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function unschedule_background_sync() {
|
||||
WC()->queue()->cancel_all( self::BACKGROUND_SYNC_EVENT_HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to check for pending syncs and enqueue the background data sync processor when in interval mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_interval_background_sync() {
|
||||
if ( self::BACKGROUND_SYNC_MODE_INTERVAL !== $this->get_background_sync_mode() ) {
|
||||
$this->unschedule_background_sync();
|
||||
return;
|
||||
}
|
||||
|
||||
$pending_count = $this->get_total_pending_count();
|
||||
if ( $pending_count > 0 ) {
|
||||
$this->batch_processing_controller->enqueue_processor( self::class );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to keep the background data sync processor enqueued when in continuous mode.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_continuous_background_sync() {
|
||||
if ( self::BACKGROUND_SYNC_MODE_CONTINUOUS !== $this->get_background_sync_mode() ) {
|
||||
$this->batch_processing_controller->remove_processor( self::class );
|
||||
return;
|
||||
}
|
||||
|
||||
// This method already checks if a processor is enqueued before adding it to avoid duplication.
|
||||
$this->batch_processing_controller->enqueue_processor( self::class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current sync process status.
|
||||
* The information is meaningful only if pending_data_sync_is_in_progress return true.
|
||||
@@ -251,12 +458,15 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
}
|
||||
|
||||
if ( $this->custom_orders_table_is_authoritative() ) {
|
||||
$missing_orders_count_sql = "
|
||||
$missing_orders_count_sql = $wpdb->prepare(
|
||||
"
|
||||
SELECT COUNT(1) FROM $wpdb->posts posts
|
||||
INNER JOIN $orders_table orders ON posts.id=orders.id
|
||||
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
|
||||
AND orders.status not in ( 'auto-draft' )
|
||||
";
|
||||
AND orders.type IN ($order_post_type_placeholder)",
|
||||
$order_post_types
|
||||
);
|
||||
$operator = '>';
|
||||
} else {
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_post_type_placeholder is prepared.
|
||||
@@ -374,13 +584,16 @@ ORDER BY posts.ID ASC",
|
||||
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
break;
|
||||
case self::ID_TYPE_MISSING_IN_POSTS_TABLE:
|
||||
$sql = "
|
||||
$sql = $wpdb->prepare(
|
||||
"
|
||||
SELECT posts.ID FROM $wpdb->posts posts
|
||||
INNER JOIN $orders_table orders ON posts.id=orders.id
|
||||
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
|
||||
AND orders.status not in ( 'auto-draft' )
|
||||
ORDER BY posts.id ASC
|
||||
";
|
||||
AND orders.type IN ($order_post_type_placeholders)
|
||||
ORDER BY posts.id ASC",
|
||||
$order_post_types
|
||||
);
|
||||
break;
|
||||
case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
|
||||
$operator = $this->custom_orders_table_is_authoritative() ? '>' : '<';
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Caches\OrderCache;
|
||||
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
||||
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
@@ -23,12 +24,19 @@ defined( 'ABSPATH' ) || exit;
|
||||
class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements \WC_Object_Data_Store_Interface, \WC_Order_Data_Store_Interface {
|
||||
|
||||
/**
|
||||
* Order IDs for which we are checking read on sync in the current request.
|
||||
* Order IDs for which we are checking sync on read in the current request. In WooCommerce, using wc_get_order is a very common pattern, to avoid performance issues, we only sync on read once per request per order. This works because we consider out of sync orders to be an anomaly, so we don't recommend running HPOS with incompatible plugins.
|
||||
*
|
||||
* @var array.
|
||||
*/
|
||||
private static $reading_order_ids = array();
|
||||
|
||||
/**
|
||||
* Keep track of order IDs that are actively being backfilled. We use this to prevent further read on sync from add_|update_|delete_postmeta etc hooks. If we allow this, then we would end up syncing the same order multiple times as it is being backfilled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $backfilling_order_ids = array();
|
||||
|
||||
/**
|
||||
* Data stored in meta keys, but not considered "meta" for an order.
|
||||
*
|
||||
@@ -509,24 +517,33 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
return $this->all_order_column_mapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get alias for order table, this is used in select query.
|
||||
*
|
||||
* @return string Alias.
|
||||
*/
|
||||
private function get_order_table_alias() : string {
|
||||
return 'o';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get alias for op table, this is used in select query.
|
||||
*
|
||||
* @return string Alias.
|
||||
*/
|
||||
private function get_op_table_alias() : string {
|
||||
return 'order_operational_data';
|
||||
return 'p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get alias for address table, this is used in select query.
|
||||
*
|
||||
* @param string $type Address type.
|
||||
* @param string $type Type of address; 'billing' or 'shipping'.
|
||||
*
|
||||
* @return string Alias.
|
||||
*/
|
||||
private function get_address_table_alias( string $type ) : string {
|
||||
return "address_$type";
|
||||
return 'billing' === $type ? 'b' : 's';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -562,7 +579,18 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
return;
|
||||
}
|
||||
|
||||
$cpt_data_store->update_order_from_object( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$this->update_order_meta_from_object( $order );
|
||||
$order_class = get_class( $order );
|
||||
$post_order = new $order_class();
|
||||
$post_order->set_id( $order->get_id() );
|
||||
$cpt_data_store->read( $post_order );
|
||||
|
||||
// This compares the order data to the post data and set changes array for props that are changed.
|
||||
$post_order->set_props( $order->get_data() );
|
||||
|
||||
$cpt_data_store->update_order_from_object( $post_order );
|
||||
|
||||
foreach ( $cpt_data_store->get_internal_data_store_key_getters() as $key => $getter_name ) {
|
||||
if (
|
||||
is_callable( array( $cpt_data_store, "set_$getter_name" ) ) &&
|
||||
@@ -580,6 +608,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
);
|
||||
}
|
||||
}
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -762,6 +791,47 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements
|
||||
$this->set_stock_reduced( $order, $set );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token ids for an order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
public function get_payment_token_ids( $order ) {
|
||||
/**
|
||||
* We don't store _payment_tokens in props to preserve backward compatibility. In CPT data store, `_payment_tokens` is always fetched directly from DB instead of from prop.
|
||||
*/
|
||||
$payment_tokens = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
|
||||
if ( $payment_tokens ) {
|
||||
$payment_tokens = $payment_tokens[0]->meta_value;
|
||||
}
|
||||
if ( ! $payment_tokens && version_compare( $order->get_version(), '8.0.0', '<' ) ) {
|
||||
// Before 8.0 we were incorrectly storing payment_tokens in the order meta. So we need to check there too.
|
||||
$payment_tokens = get_post_meta( $order->get_id(), '_payment_tokens', true );
|
||||
}
|
||||
return array_filter( (array) $payment_tokens );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update token ids for an order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $token_ids Payment token ids.
|
||||
*/
|
||||
public function update_payment_token_ids( $order, $token_ids ) {
|
||||
$meta = new \WC_Meta_Data();
|
||||
$meta->key = '_payment_tokens';
|
||||
$meta->value = $token_ids;
|
||||
$existing_meta = $this->data_store_meta->get_metadata_by_key( $order, '_payment_tokens' );
|
||||
if ( $existing_meta ) {
|
||||
$existing_meta = $existing_meta[0];
|
||||
$meta->id = $existing_meta->id;
|
||||
$this->data_store_meta->update_meta( $order, $meta );
|
||||
} else {
|
||||
$this->data_store_meta->add_meta( $order, $meta );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amount already refunded.
|
||||
*
|
||||
@@ -1051,8 +1121,20 @@ WHERE
|
||||
}
|
||||
|
||||
$data_sync_enabled = $data_synchronizer->data_sync_is_enabled();
|
||||
$load_posts_for = array_diff( $order_ids, self::$reading_order_ids );
|
||||
$post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();
|
||||
if ( $data_sync_enabled ) {
|
||||
/**
|
||||
* Allow opportunity to disable sync on read, while keeping sync on write enabled. This adds another step as a large shop progresses from full sync to no sync with HPOS authoritative.
|
||||
* This filter is only executed if data sync is enabled from settings in the first place as it's meant to be a step between full sync -> no sync, rather than be a control for enabling just the sync on read. Sync on read without sync on write is problematic as any update will reset on the next read, but sync on write without sync on read is fine.
|
||||
*
|
||||
* @param bool $read_on_sync_enabled Whether to sync on read.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
$data_sync_enabled = apply_filters( 'woocommerce_hpos_enable_sync_on_read', $data_sync_enabled );
|
||||
}
|
||||
|
||||
$load_posts_for = array_diff( $order_ids, array_merge( self::$reading_order_ids, self::$backfilling_order_ids ) );
|
||||
$post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array();
|
||||
|
||||
foreach ( $data as $order_data ) {
|
||||
$order_id = absint( $order_data->id );
|
||||
@@ -1446,14 +1528,19 @@ WHERE
|
||||
* @return \stdClass[]|object|null DB Order objects or error.
|
||||
*/
|
||||
protected function get_order_data_for_ids( $ids ) {
|
||||
if ( ! $ids ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! $ids || empty( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
if ( empty( $ids ) ) {
|
||||
return array();
|
||||
}
|
||||
$table_aliases = array(
|
||||
'orders' => $this->get_order_table_alias(),
|
||||
'billing_address' => $this->get_address_table_alias( 'billing' ),
|
||||
'shipping_address' => $this->get_address_table_alias( 'shipping' ),
|
||||
'operational_data' => $this->get_op_table_alias(),
|
||||
);
|
||||
$order_table_alias = $table_aliases['orders'];
|
||||
$order_table_query = $this->get_order_table_select_statement();
|
||||
$id_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
|
||||
$order_meta_table = self::get_meta_table_name();
|
||||
@@ -1461,7 +1548,7 @@ WHERE
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_table_query is autogenerated and should already be prepared.
|
||||
$table_data = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"$order_table_query WHERE wc_order.id in ( $id_placeholder )",
|
||||
"$order_table_query WHERE $order_table_alias.id in ( $id_placeholder )",
|
||||
$ids
|
||||
)
|
||||
);
|
||||
@@ -1476,9 +1563,27 @@ WHERE
|
||||
$ids
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $table_data as $table_datum ) {
|
||||
$order_data[ $table_datum->id ] = $table_datum;
|
||||
$order_data[ $table_datum->id ]->meta_data = array();
|
||||
$id = $table_datum->{"{$order_table_alias}_id"};
|
||||
$order_data[ $id ] = new \stdClass();
|
||||
foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mappings ) {
|
||||
$table_alias = $table_aliases[ $table_name ];
|
||||
// This remapping is required to keep the query length small enough to be supported by implementations such as HyperDB (i.e. fetching some tables in join via alias.*, while others via full name). We can revert this commit if HyperDB starts supporting SRTM for query length more than 3076 characters.
|
||||
foreach ( $column_mappings as $field => $map ) {
|
||||
$field_name = $map['name'] ?? "{$table_name}_$field";
|
||||
if ( property_exists( $table_datum, $field_name ) ) {
|
||||
$field_value = $table_datum->{ $field_name }; // Unique column, field name is different prop name.
|
||||
} elseif ( property_exists( $table_datum, "{$table_alias}_$field" ) ) {
|
||||
$field_value = $table_datum->{"{$table_alias}_$field"}; // Non-unique column (billing, shipping etc).
|
||||
} else {
|
||||
$field_value = $table_datum->{ $field }; // Unique column, field name is same as prop name.
|
||||
}
|
||||
$order_data[ $id ]->{$field_name} = $field_value;
|
||||
}
|
||||
}
|
||||
$order_data[ $id ]->id = $id;
|
||||
$order_data[ $id ]->meta_data = array();
|
||||
}
|
||||
|
||||
foreach ( $meta_data as $meta_datum ) {
|
||||
@@ -1500,8 +1605,7 @@ WHERE
|
||||
*/
|
||||
private function get_order_table_select_statement() {
|
||||
$order_table = $this::get_orders_table_name();
|
||||
$order_table_alias = 'wc_order';
|
||||
$select_clause = $this->generate_select_clause_for_props( $order_table_alias, $this->order_column_mapping );
|
||||
$order_table_alias = $this->get_order_table_alias();
|
||||
$billing_address_table_alias = $this->get_address_table_alias( 'billing' );
|
||||
$shipping_address_table_alias = $this->get_address_table_alias( 'shipping' );
|
||||
$op_data_table_alias = $this->get_op_table_alias();
|
||||
@@ -1509,8 +1613,12 @@ WHERE
|
||||
$shipping_address_clauses = $this->join_shipping_address_table_to_order_query( $order_table_alias, $shipping_address_table_alias );
|
||||
$operational_data_clauses = $this->join_operational_data_table_to_order_query( $order_table_alias, $op_data_table_alias );
|
||||
|
||||
/**
|
||||
* We fully spell out address table columns because they have duplicate columns for billing and shipping and would be overwritten if we don't spell them out. There is not such duplication in the operational data table and orders table, so select with `alias`.* is fine.
|
||||
* We do spell ID columns manually, as they are duplicate.
|
||||
*/
|
||||
return "
|
||||
SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']}
|
||||
SELECT $order_table_alias.id as o_id, $op_data_table_alias.id as p_id, $order_table_alias.*, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, $op_data_table_alias.*
|
||||
FROM $order_table $order_table_alias
|
||||
LEFT JOIN {$billing_address_clauses['join']}
|
||||
LEFT JOIN {$shipping_address_clauses['join']}
|
||||
@@ -1558,7 +1666,7 @@ FROM $order_meta_table
|
||||
/**
|
||||
* Helper method to generate join and select query for address table.
|
||||
*
|
||||
* @param string $address_type Type of address. Typically will be `billing` or `shipping`.
|
||||
* @param string $address_type Type of address; 'billing' or 'shipping'.
|
||||
* @param string $order_table_alias Alias of order table to use.
|
||||
* @param string $address_table_alias Alias for address table to use.
|
||||
*
|
||||
@@ -1656,9 +1764,11 @@ FROM $order_meta_table
|
||||
if ( 'create' === $context ) {
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
||||
'post_status' => 'draft',
|
||||
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
|
||||
'post_type' => $data_sync->data_sync_is_enabled() ? $order->get_type() : $data_sync::PLACEHOLDER_ORDER_POST_TYPE,
|
||||
'post_status' => 'draft',
|
||||
'post_parent' => $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0,
|
||||
'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ),
|
||||
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1796,8 +1906,20 @@ FROM $order_meta_table
|
||||
if ( $row ) {
|
||||
$result[] = array(
|
||||
'table' => self::get_orders_table_name(),
|
||||
'data' => array_merge( $row['data'], array( 'id' => $order->get_id() ) ),
|
||||
'format' => array_merge( $row['format'], array( 'id' => '%d' ) ),
|
||||
'data' => array_merge(
|
||||
$row['data'],
|
||||
array(
|
||||
'id' => $order->get_id(),
|
||||
'type' => $order->get_type(),
|
||||
)
|
||||
),
|
||||
'format' => array_merge(
|
||||
$row['format'],
|
||||
array(
|
||||
'id' => '%d',
|
||||
'type' => '%s',
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1866,8 +1988,6 @@ FROM $order_meta_table
|
||||
protected function get_db_row_from_order( $order, $column_mapping, $only_changes = false ) {
|
||||
$changes = $only_changes ? $order->get_changes() : array_merge( $order->get_data(), $order->get_changes() );
|
||||
|
||||
$changes['type'] = $order->get_type();
|
||||
|
||||
// Make sure 'status' is correctly prefixed.
|
||||
if ( array_key_exists( 'status', $column_mapping ) && array_key_exists( 'status', $changes ) ) {
|
||||
$changes['status'] = $this->get_post_status( $order );
|
||||
@@ -2101,16 +2221,6 @@ FROM $order_meta_table
|
||||
'_wp_trash_meta_time' => time(),
|
||||
);
|
||||
|
||||
foreach ( $trash_metadata as $meta_key => $meta_value ) {
|
||||
$this->add_meta(
|
||||
$order,
|
||||
(object) array(
|
||||
'key' => $meta_key,
|
||||
'value' => $meta_value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$wpdb->update(
|
||||
self::get_orders_table_name(),
|
||||
array(
|
||||
@@ -2124,6 +2234,16 @@ FROM $order_meta_table
|
||||
|
||||
$order->set_status( 'trash' );
|
||||
|
||||
foreach ( $trash_metadata as $meta_key => $meta_value ) {
|
||||
$this->add_meta(
|
||||
$order,
|
||||
(object) array(
|
||||
'key' => $meta_key,
|
||||
'value' => $meta_value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$data_synchronizer = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_synchronizer->data_sync_is_enabled() ) {
|
||||
wp_trash_post( $order->get_id() );
|
||||
@@ -2255,6 +2375,11 @@ FROM $order_meta_table
|
||||
|
||||
$this->persist_save( $order );
|
||||
|
||||
// Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility.
|
||||
if ( 'auto-draft' === $order->get_status( 'edit' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when a new order is created.
|
||||
*
|
||||
@@ -2287,15 +2412,22 @@ FROM $order_meta_table
|
||||
$order->set_date_created( time() );
|
||||
}
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
if ( ! $order->get_date_modified( 'edit' ) ) {
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
$this->persist_order_to_db( $order, $force_all_fields );
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
|
||||
$order->save_meta_data();
|
||||
$order->apply_changes();
|
||||
|
||||
if ( $backfill ) {
|
||||
$this->maybe_backfill_post_record( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
|
||||
$this->maybe_backfill_post_record( $r_order );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
$this->clear_caches( $order );
|
||||
}
|
||||
@@ -2306,6 +2438,9 @@ FROM $order_meta_table
|
||||
* @param \WC_Order $order Order object.
|
||||
*/
|
||||
public function update( &$order ) {
|
||||
$previous_status = ArrayUtil::get_value_or_default( $order->get_data(), 'status' );
|
||||
$changes = $order->get_changes();
|
||||
|
||||
// Before updating, ensure date paid is set if missing.
|
||||
if (
|
||||
! $order->get_date_paid( 'edit' )
|
||||
@@ -2339,6 +2474,18 @@ FROM $order_meta_table
|
||||
$order->apply_changes();
|
||||
$this->clear_caches( $order );
|
||||
|
||||
// For backwards compatibility, moving an auto-draft order to a valid status triggers the 'woocommerce_new_order' hook.
|
||||
if ( ! empty( $changes['status'] ) && 'auto-draft' === $previous_status ) {
|
||||
do_action( 'woocommerce_new_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
return;
|
||||
}
|
||||
|
||||
// For backwards compat with CPT, trashing/untrashing and changing previously datastore-level props does not trigger the update hook.
|
||||
if ( ( ! empty( $changes['status'] ) && in_array( 'trash', array( $changes['status'], $previous_status ), true ) )
|
||||
|| ( ! empty( $changes ) && ! array_diff_key( $changes, array_flip( $this->get_post_data_store_for_backfill()->get_internal_data_store_key_getters() ) ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'woocommerce_update_order', $order->get_id(), $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
}
|
||||
|
||||
@@ -2370,19 +2517,33 @@ FROM $order_meta_table
|
||||
$changes = $order->get_changes();
|
||||
|
||||
if ( ! isset( $changes['date_modified'] ) ) {
|
||||
$order->set_date_modified( time() );
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
$this->persist_order_to_db( $order );
|
||||
$order->save_meta_data();
|
||||
|
||||
if ( $backfill ) {
|
||||
$this->maybe_backfill_post_record( $order );
|
||||
self::$backfilling_order_ids[] = $order->get_id();
|
||||
$this->clear_caches( $order );
|
||||
$r_order = wc_get_order( $order->get_id() ); // Refresh order to account for DB changes from post hooks.
|
||||
$this->maybe_backfill_post_record( $r_order );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $order->get_id() ) );
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check whether to backfill post record.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_backfill_post_record() {
|
||||
$data_sync = wc_get_container()->get( DataSynchronizer::class );
|
||||
return $data_sync->data_sync_is_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to decide whether to backfill post record.
|
||||
*
|
||||
@@ -2391,8 +2552,7 @@ FROM $order_meta_table
|
||||
* @return void
|
||||
*/
|
||||
private function maybe_backfill_post_record( $order ) {
|
||||
$data_sync = wc_get_container()->get( DataSynchronizer::class );
|
||||
if ( $data_sync->data_sync_is_enabled() ) {
|
||||
if ( $this->should_backfill_post_record() ) {
|
||||
$this->backfill_post_record( $order );
|
||||
}
|
||||
}
|
||||
@@ -2421,8 +2581,10 @@ FROM $order_meta_table
|
||||
private function update_address_index_meta( $order, $changes ) {
|
||||
// If address changed, store concatenated version to make searches faster.
|
||||
foreach ( array( 'billing', 'shipping' ) as $address_type ) {
|
||||
if ( isset( $changes[ $address_type ] ) ) {
|
||||
$order->update_meta_data( "_{$address_type}_address_index", implode( ' ', $order->get_address( $address_type ) ) );
|
||||
$index_meta_key = "_{$address_type}_address_index";
|
||||
|
||||
if ( isset( $changes[ $address_type ] ) || ( is_a( $order, 'WC_Order' ) && empty( $order->get_meta( $index_meta_key ) ) ) ) {
|
||||
$order->update_meta_data( $index_meta_key, implode( ' ', $order->get_address( $address_type ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2584,6 +2746,10 @@ FROM $order_meta_table
|
||||
$operational_data_table_name = $this->get_operational_data_table_name();
|
||||
$meta_table = $this->get_meta_table_name();
|
||||
|
||||
$max_index_length = $this->database_util->get_max_index_length();
|
||||
$composite_meta_value_index_length = max( $max_index_length - 8 - 100 - 1, 20 ); // 8 for order_id, 100 for meta_key, 10 minimum for meta_value.
|
||||
$composite_customer_id_email_length = max( $max_index_length - 20, 20 ); // 8 for customer_id, 20 minimum for email.
|
||||
|
||||
$sql = "
|
||||
CREATE TABLE $orders_table_name (
|
||||
id bigint(20) unsigned,
|
||||
@@ -2606,9 +2772,9 @@ CREATE TABLE $orders_table_name (
|
||||
PRIMARY KEY (id),
|
||||
KEY status (status),
|
||||
KEY date_created (date_created_gmt),
|
||||
KEY customer_id_billing_email (customer_id, billing_email),
|
||||
KEY billing_email (billing_email),
|
||||
KEY type_status (type, status),
|
||||
KEY customer_id_billing_email (customer_id, billing_email({$composite_customer_id_email_length})),
|
||||
KEY billing_email (billing_email($max_index_length)),
|
||||
KEY type_status_date (type, status, date_created_gmt),
|
||||
KEY parent_order_id (parent_order_id),
|
||||
KEY date_updated (date_updated_gmt)
|
||||
) $collate;
|
||||
@@ -2629,7 +2795,7 @@ CREATE TABLE $addresses_table_name (
|
||||
phone varchar(100) null,
|
||||
KEY order_id (order_id),
|
||||
UNIQUE KEY address_type_order_id (address_type, order_id),
|
||||
KEY email (email),
|
||||
KEY email (email($max_index_length)),
|
||||
KEY phone (phone)
|
||||
) $collate;
|
||||
CREATE TABLE $operational_data_table_name (
|
||||
@@ -2646,10 +2812,10 @@ CREATE TABLE $operational_data_table_name (
|
||||
order_stock_reduced tinyint(1) NULL,
|
||||
date_paid_gmt datetime NULL,
|
||||
date_completed_gmt datetime NULL,
|
||||
shipping_tax_amount decimal(26, 8) NULL,
|
||||
shipping_total_amount decimal(26, 8) NULL,
|
||||
discount_tax_amount decimal(26, 8) NULL,
|
||||
discount_total_amount decimal(26, 8) NULL,
|
||||
shipping_tax_amount decimal(26,8) NULL,
|
||||
shipping_total_amount decimal(26,8) NULL,
|
||||
discount_tax_amount decimal(26,8) NULL,
|
||||
discount_total_amount decimal(26,8) NULL,
|
||||
recorded_sales tinyint(1) NULL,
|
||||
UNIQUE KEY order_id (order_id),
|
||||
KEY order_key (order_key)
|
||||
@@ -2659,8 +2825,8 @@ CREATE TABLE $meta_table (
|
||||
order_id bigint(20) unsigned null,
|
||||
meta_key varchar(255),
|
||||
meta_value text null,
|
||||
KEY meta_key_value (meta_key, meta_value(100)),
|
||||
KEY order_id_meta_key_meta_value (order_id, meta_key, meta_value(100))
|
||||
KEY meta_key_value (meta_key(100), meta_value($composite_meta_value_index_length)),
|
||||
KEY order_id_meta_key_meta_value (order_id, meta_key(100), meta_value($composite_meta_value_index_length))
|
||||
) $collate;
|
||||
";
|
||||
|
||||
@@ -2681,16 +2847,28 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Deletes meta based on meta ID.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing at least ->id).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing at least ->id).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_meta( &$object, $meta ) {
|
||||
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
|
||||
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
|
||||
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
|
||||
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
|
||||
if ( $db_meta ) {
|
||||
$meta->key = $db_meta->meta_key;
|
||||
$meta->value = $db_meta->meta_value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
$delete_meta = $this->data_store_meta->delete_meta( $object, $meta );
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $delete_meta;
|
||||
@@ -2699,16 +2877,20 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Add new piece of meta.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing ->key and ->value).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing ->key and ->value).
|
||||
*
|
||||
* @return int|bool meta ID or false on failure
|
||||
*/
|
||||
public function add_meta( &$object, $meta ) {
|
||||
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
|
||||
$add_meta = $this->data_store_meta->add_meta( $object, $meta );
|
||||
$meta->id = $add_meta;
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
add_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $add_meta;
|
||||
@@ -2717,18 +2899,66 @@ CREATE TABLE $meta_table (
|
||||
/**
|
||||
* Update meta.
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param stdClass $meta (containing ->id, ->key and ->value).
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param \stdClass $meta (containing ->id, ->key and ->value).
|
||||
*
|
||||
* @return bool
|
||||
* @return bool The number of rows updated, or false on error.
|
||||
*/
|
||||
public function update_meta( &$object, $meta ) {
|
||||
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
|
||||
$update_meta = $this->data_store_meta->update_meta( $object, $meta );
|
||||
$changes_applied = $this->after_meta_change( $object, $meta );
|
||||
|
||||
if ( $object instanceof WC_Abstract_Order ) {
|
||||
$this->maybe_backfill_post_record( $object );
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
update_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
return $update_meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform after meta change operations, including updating the date_modified field, clearing caches and applying changes.
|
||||
*
|
||||
* @param WC_Abstract_Order $order Order object.
|
||||
* @param \WC_Meta_Data $meta Metadata object.
|
||||
*
|
||||
* @return bool True if changes were applied, false otherwise.
|
||||
*/
|
||||
protected function after_meta_change( &$order, $meta ) {
|
||||
method_exists( $meta, 'apply_changes' ) && $meta->apply_changes();
|
||||
|
||||
// Prevent this happening multiple time in same request.
|
||||
if ( $this->should_save_after_meta_change( $order, $meta ) ) {
|
||||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
$order->save();
|
||||
return true;
|
||||
} else {
|
||||
$order_cache = wc_get_container()->get( OrderCache::class );
|
||||
$order_cache->remove( $order->get_id() );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check whether the modified date needs to be updated after a meta save.
|
||||
*
|
||||
* This method prevents order->save() call multiple times in the same request after any meta update by checking if:
|
||||
* 1. Order modified date is already the current date, no updates needed in this case.
|
||||
* 2. If there are changes already queued for order object, then we don't need to update the modified date as it will be updated ina subsequent save() call.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param \WC_Meta_Data|null $meta Metadata object.
|
||||
*
|
||||
* @return bool Whether the modified date needs to be updated.
|
||||
*/
|
||||
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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,57 @@ class OrdersTableQuery {
|
||||
unset( $this->args['customer_note'], $this->args['name'] );
|
||||
|
||||
$this->build_query();
|
||||
$this->run_query();
|
||||
if ( ! $this->maybe_override_query() ) {
|
||||
$this->run_query();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets the `woocommerce_hpos_pre_query` filter override the query.
|
||||
*
|
||||
* @return boolean Whether the query was overridden or not.
|
||||
*/
|
||||
private function maybe_override_query(): bool {
|
||||
/**
|
||||
* Filters the orders array before the query takes place.
|
||||
*
|
||||
* Return a non-null value to bypass the HPOS default order queries.
|
||||
*
|
||||
* If the query includes limits via the `limit`, `page`, or `offset` arguments, we
|
||||
* encourage the `found_orders` and `max_num_pages` properties to also be set.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*
|
||||
* @param array|null $order_data {
|
||||
* An array of order data.
|
||||
* @type int[] $orders Return an array of order IDs data to short-circuit the HPOS query,
|
||||
* or null to allow HPOS to run its normal query.
|
||||
* @type int $found_orders The number of orders found.
|
||||
* @type int $max_num_pages The number of pages.
|
||||
* }
|
||||
* @param OrdersTableQuery $query The OrdersTableQuery instance.
|
||||
* @param string $sql The OrdersTableQuery instance.
|
||||
*/
|
||||
$pre_query = apply_filters( 'woocommerce_hpos_pre_query', null, $this, $this->sql );
|
||||
if ( ! $pre_query || ! isset( $pre_query[0] ) || ! is_array( $pre_query[0] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the filter set the orders, make sure the others values are set as well and skip running the query.
|
||||
list( $this->orders, $this->found_orders, $this->max_num_pages ) = $pre_query;
|
||||
|
||||
if ( ! is_int( $this->found_orders ) || $this->found_orders < 1 ) {
|
||||
$this->found_orders = count( $this->orders );
|
||||
}
|
||||
|
||||
if ( ! is_int( $this->max_num_pages ) || $this->max_num_pages < 1 ) {
|
||||
if ( ! $this->arg_isset( 'limit' ) || ! is_int( $this->args['limit'] ) || $this->args['limit'] < 1 ) {
|
||||
$this->args['limit'] = 10;
|
||||
}
|
||||
$this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,22 +337,22 @@ class OrdersTableQuery {
|
||||
* YYYY-MM-DD queries have 'day' precision for backwards compatibility.
|
||||
*
|
||||
* @param mixed $date The date. Can be a {@see \WC_DateTime}, a timestamp or a string.
|
||||
* @param string $timezone The timezone to use for the date.
|
||||
* @return array An array with keys 'year', 'month', 'day' and possibly 'hour', 'minute' and 'second'.
|
||||
*/
|
||||
private function date_to_date_query_arg( $date, $timezone ): array {
|
||||
private function date_to_date_query_arg( $date ): array {
|
||||
$result = array(
|
||||
'year' => '',
|
||||
'month' => '',
|
||||
'day' => '',
|
||||
);
|
||||
$precision = 'second';
|
||||
|
||||
if ( is_numeric( $date ) ) {
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( $timezone ) );
|
||||
$date = new \WC_DateTime( "@{$date}", new \DateTimeZone( 'UTC' ) );
|
||||
$precision = 'second';
|
||||
} elseif ( ! is_a( $date, 'WC_DateTime' ) ) {
|
||||
// YYYY-MM-DD queries have 'day' precision for backwards compat.
|
||||
$date = wc_string_to_datetime( $date );
|
||||
// For backwards compat (see https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date)
|
||||
// only YYYY-MM-DD is considered for date values. Timestamps do support second precision.
|
||||
$date = wc_string_to_datetime( date( 'Y-m-d', strtotime( $date ) ) );
|
||||
$precision = 'day';
|
||||
}
|
||||
|
||||
@@ -319,6 +369,80 @@ class OrdersTableQuery {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns UTC-based date query arguments for a combination of local time dates and a date shorthand operator.
|
||||
*
|
||||
* @param array $dates_raw Array of dates (in local time) to use in combination with the operator.
|
||||
* @param string $operator One of the operators supported by date queries (<, <=, =, ..., >, >=).
|
||||
* @return array Partial date query arg with relevant dates now UTC-based.
|
||||
*
|
||||
* @since 8.2.0
|
||||
*/
|
||||
private function local_time_to_gmt_date_query( $dates_raw, $operator ) {
|
||||
$result = array();
|
||||
|
||||
// Convert YYYY-MM-DD to UTC timestamp. Per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date only date is relevant (time is ignored).
|
||||
foreach ( $dates_raw as &$raw_date ) {
|
||||
$raw_date = is_numeric( $raw_date ) ? $raw_date : strtotime( get_gmt_from_date( date( 'Y-m-d', strtotime( $raw_date ) ) ) );
|
||||
}
|
||||
|
||||
$date1 = end( $dates_raw );
|
||||
|
||||
switch ( $operator ) {
|
||||
case '>':
|
||||
$result = array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => true,
|
||||
);
|
||||
break;
|
||||
case '>=':
|
||||
$result = array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => true,
|
||||
);
|
||||
break;
|
||||
case '=':
|
||||
$result = array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'after' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => true,
|
||||
),
|
||||
array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => false,
|
||||
)
|
||||
);
|
||||
break;
|
||||
case '<=':
|
||||
$result = array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 + DAY_IN_SECONDS ),
|
||||
'inclusive' => false,
|
||||
);
|
||||
break;
|
||||
case '<':
|
||||
$result = array(
|
||||
'before' => $this->date_to_date_query_arg( $date1 ),
|
||||
'inclusive' => false,
|
||||
);
|
||||
break;
|
||||
case '...':
|
||||
$result = array(
|
||||
'relation' => 'AND',
|
||||
$this->local_time_to_gmt_date_query( array( $dates_raw[1] ), '<=' ),
|
||||
$this->local_time_to_gmt_date_query( array( $dates_raw[0] ), '>=' ),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $result ) {
|
||||
throw new \Exception( 'Please specify a valid date shorthand operator.' );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes date-related query args and merges the result into 'date_query'.
|
||||
*
|
||||
@@ -347,27 +471,45 @@ class OrdersTableQuery {
|
||||
$date_keys = array_filter( $valid_date_keys, array( $this, 'arg_isset' ) );
|
||||
|
||||
foreach ( $date_keys as $date_key ) {
|
||||
$is_local = in_array( $date_key, $local_date_keys, true );
|
||||
$date_value = $this->args[ $date_key ];
|
||||
|
||||
$operator = '=';
|
||||
$dates_raw = array();
|
||||
$dates = array();
|
||||
$timezone = in_array( $date_key, $gmt_date_keys, true ) ? '+0000' : wc_timezone_string();
|
||||
|
||||
if ( is_string( $date_value ) && preg_match( self::REGEX_SHORTHAND_DATES, $date_value, $matches ) ) {
|
||||
$operator = in_array( $matches[2], $valid_operators, true ) ? $matches[2] : '';
|
||||
|
||||
if ( ! empty( $matches[1] ) ) {
|
||||
$dates[] = $this->date_to_date_query_arg( $matches[1], $timezone );
|
||||
$dates_raw[] = $matches[1];
|
||||
}
|
||||
|
||||
$dates[] = $this->date_to_date_query_arg( $matches[3], $timezone );
|
||||
$dates_raw[] = $matches[3];
|
||||
} else {
|
||||
$dates[] = $this->date_to_date_query_arg( $date_value, $timezone );
|
||||
$dates_raw[] = $date_value;
|
||||
}
|
||||
|
||||
if ( empty( $dates ) || ! $operator || ( '...' === $operator && count( $dates ) < 2 ) ) {
|
||||
if ( empty( $dates_raw ) || ! $operator || ( '...' === $operator && count( $dates_raw ) < 2 ) ) {
|
||||
throw new \Exception( 'Invalid date_query' );
|
||||
}
|
||||
|
||||
if ( $is_local ) {
|
||||
$date_key = $local_to_gmt_date_keys[ $date_key ];
|
||||
|
||||
if ( ! is_numeric( $dates_raw[0] ) && ( ! isset( $dates_raw[1] ) || ! is_numeric( $dates_raw[1] ) ) ) {
|
||||
// Only non-numeric args can be considered local time. Timestamps are assumed to be UTC per https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date.
|
||||
$date_queries[] = array_merge(
|
||||
array(
|
||||
'column' => $date_key,
|
||||
),
|
||||
$this->local_time_to_gmt_date_query( $dates_raw, $operator )
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$operator_to_keys = array();
|
||||
|
||||
if ( in_array( $operator, array( '>', '>=', '...' ), true ) ) {
|
||||
@@ -378,7 +520,7 @@ class OrdersTableQuery {
|
||||
$operator_to_keys[] = 'before';
|
||||
}
|
||||
|
||||
$date_key = in_array( $date_key, $local_date_keys, true ) ? $local_to_gmt_date_keys[ $date_key ] : $date_key;
|
||||
$dates = array_map( array( $this, 'date_to_date_query_arg' ), $dates_raw );
|
||||
$date_queries[] = array_merge(
|
||||
array(
|
||||
'column' => $date_key,
|
||||
@@ -470,7 +612,7 @@ class OrdersTableQuery {
|
||||
$op = isset( $query['after'] ) ? 'after' : 'before';
|
||||
$date_value_local = $query[ $op ];
|
||||
$date_value_gmt = wc_string_to_timestamp( get_gmt_from_date( wc_string_to_datetime( $date_value_local ) ) );
|
||||
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt, 'UTC' );
|
||||
$query[ $op ] = $this->date_to_date_query_arg( $date_value_gmt );
|
||||
}
|
||||
|
||||
return $query;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use \WC_Cache_Helper;
|
||||
use \WC_Meta_Data;
|
||||
|
||||
/**
|
||||
* Class OrdersTableRefundDataStore.
|
||||
*/
|
||||
@@ -75,6 +78,9 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||
return;
|
||||
}
|
||||
|
||||
$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $refund->get_parent_id();
|
||||
wp_cache_delete( $refund_cache_key, 'orders' );
|
||||
|
||||
$this->delete_order_data_from_custom_order_tables( $refund_id );
|
||||
$refund->set_id( 0 );
|
||||
|
||||
@@ -159,8 +165,17 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
||||
|
||||
$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
|
||||
foreach ( $props_to_update as $meta_key => $prop ) {
|
||||
$value = $refund->{"get_$prop"}( 'edit' );
|
||||
$refund->update_meta_data( $meta_key, $value );
|
||||
$meta_object = new WC_Meta_Data();
|
||||
$meta_object->key = $meta_key;
|
||||
$meta_object->value = $refund->{"get_$prop"}( 'edit' );
|
||||
$existing_meta = $this->data_store_meta->get_metadata_by_key( $refund, $meta_key );
|
||||
if ( $existing_meta ) {
|
||||
$existing_meta = $existing_meta[0];
|
||||
$meta_object->id = $existing_meta->id;
|
||||
$this->update_meta( $refund, $meta_object );
|
||||
} else {
|
||||
$this->add_meta( $refund, $meta_object );
|
||||
}
|
||||
$updated_props[] = $prop;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* MarketplaceServiceProvider class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||
use Automattic\WooCommerce\Internal\Admin\Marketplace;
|
||||
|
||||
/**
|
||||
* Service provider for the Marketplace namespace.
|
||||
*/
|
||||
class MarketplaceServiceProvider extends AbstractServiceProvider {
|
||||
|
||||
/**
|
||||
* The classes/interfaces that are serviced by this service provider.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $provides = array(
|
||||
Marketplace::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the classes.
|
||||
*/
|
||||
public function register() {
|
||||
$this->share( Marketplace::class );
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
|
||||
PostsToOrdersMigrationController::class,
|
||||
LegacyProxy::class,
|
||||
OrderCacheController::class,
|
||||
BatchProcessingController::class,
|
||||
)
|
||||
);
|
||||
$this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Features;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Analytics;
|
||||
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
|
||||
use Automattic\WooCommerce\Admin\Features\NewProductManagementExperience;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
|
||||
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Proxies\LegacyProxy;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
@@ -34,28 +32,21 @@ class FeaturesController {
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $features;
|
||||
private $features = array();
|
||||
|
||||
/**
|
||||
* The registered compatibility info for WooCommerce plugins, with plugin names as keys.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $compatibility_info_by_plugin;
|
||||
|
||||
/**
|
||||
* Ids of the legacy features (they existed before the features engine was implemented).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $legacy_feature_ids;
|
||||
private $compatibility_info_by_plugin = array();
|
||||
|
||||
/**
|
||||
* The registered compatibility info for WooCommerce plugins, with feature ids as keys.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $compatibility_info_by_feature;
|
||||
private $compatibility_info_by_feature = array();
|
||||
|
||||
/**
|
||||
* The LegacyProxy instance to use.
|
||||
@@ -91,61 +82,6 @@ class FeaturesController {
|
||||
* Creates a new instance of the class.
|
||||
*/
|
||||
public function __construct() {
|
||||
$hpos_enable_sync = DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
|
||||
$hpos_authoritative = CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
|
||||
$features = array(
|
||||
'analytics' => array(
|
||||
'name' => __( 'Analytics', 'woocommerce' ),
|
||||
'description' => __( 'Enables WooCommerce Analytics', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'enabled_by_default' => true,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
'new_navigation' => array(
|
||||
'name' => __( 'Navigation', 'woocommerce' ),
|
||||
'description' => __( 'Adds the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
'product_block_editor' => array(
|
||||
'name' => __( 'New product editor', 'woocommerce' ),
|
||||
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'disable_ui' => false,
|
||||
),
|
||||
// Options for HPOS features are added in CustomOrdersTableController to keep the logic in same place.
|
||||
'custom_order_tables' => array( // This exists for back-compat only, otherwise it's value is superseded by $hpos_authoritative option.
|
||||
'name' => __( 'High-Performance order storage (COT)', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'enabled_by_default' => false,
|
||||
),
|
||||
$hpos_authoritative => array(
|
||||
'name' => __( 'High performance order storage', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
),
|
||||
$hpos_enable_sync => array(
|
||||
'name' => '',
|
||||
'is_experimental' => true,
|
||||
),
|
||||
'cart_checkout_blocks' => array(
|
||||
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
|
||||
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'disable_ui' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$this->legacy_feature_ids = array(
|
||||
'analytics',
|
||||
'new_navigation',
|
||||
'product_block_editor',
|
||||
// Compatibility for COT is determined by `custom_order_tables'.
|
||||
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
);
|
||||
|
||||
$this->init_features( $features );
|
||||
|
||||
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
|
||||
self::add_filter( 'added_option', array( $this, 'process_added_option' ), 999, 3 );
|
||||
self::add_filter( 'woocommerce_get_sections_advanced', array( $this, 'add_features_section' ), 10, 1 );
|
||||
@@ -162,22 +98,139 @@ class FeaturesController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the class according to the existing features.
|
||||
* Register a feature.
|
||||
*
|
||||
* @param array $features Information about the existing features.
|
||||
* This should be called during the `woocommerce_register_feature_definitions` action hook.
|
||||
*
|
||||
* @param string $slug The ID slug of the feature.
|
||||
* @param string $name The name of the feature that will appear on the Features screen and elsewhere.
|
||||
* @param array $args {
|
||||
* Optional. Properties that make up the feature definition. Each of these properties can also be set as a
|
||||
* callback function, as long as that function returns the specified type.
|
||||
*
|
||||
* @type array[] $additional_settings An array of definitions for additional settings controls related to
|
||||
* the feature that will display on the Features screen. See the Settings API
|
||||
* for the schema of these props.
|
||||
* @type string $description A brief description of the feature, used as an input label if the feature
|
||||
* setting is a checkbox.
|
||||
* @type bool $disabled True to disable the setting field for this feature on the Features screen,
|
||||
* so it can't be changed.
|
||||
* @type bool $disable_ui Set to true to hide the setting field for this feature on the
|
||||
* Features screen. Defaults to false.
|
||||
* @type bool $enabled_by_default Set to true to have this feature by opt-out instead of opt-in.
|
||||
* Defaults to false.
|
||||
* @type bool $is_experimental Set to true to display this feature under the "Experimental" heading on
|
||||
* the Features screen. Features set to experimental are also omitted from
|
||||
* the features list in some cases. Defaults to true.
|
||||
* @type bool $is_legacy Set to true if this feature existed before the FeaturesController class
|
||||
* was introduced. Features set to legacy also do not produce warnings about
|
||||
* incompatible plugins. Defaults to false.
|
||||
* @type string $option_key The key name for the option that enables/disables the feature.
|
||||
* @type int $order The order that the feature will appear in the list on the Features screen.
|
||||
* Higher number = higher in the list. Defaults to 10.
|
||||
* @type array $setting The properties used by the Settings API to render the setting control on
|
||||
* the Features screen. See the Settings API for the schema of these props.
|
||||
* }
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_features( array $features ) {
|
||||
$this->compatibility_info_by_plugin = array();
|
||||
$this->compatibility_info_by_feature = array();
|
||||
public function add_feature_definition( $slug, $name, array $args = array() ) {
|
||||
$defaults = array(
|
||||
'disable_ui' => false,
|
||||
'enabled_by_default' => false,
|
||||
'is_experimental' => true,
|
||||
'is_legacy' => false,
|
||||
'name' => $name,
|
||||
'order' => 10,
|
||||
);
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$this->features = $features;
|
||||
$this->features[ $slug ] = $args;
|
||||
}
|
||||
|
||||
foreach ( array_keys( $this->features ) as $feature_id ) {
|
||||
$this->compatibility_info_by_feature[ $feature_id ] = array(
|
||||
'compatible' => array(),
|
||||
'incompatible' => array(),
|
||||
/**
|
||||
* Generate and cache the feature definitions.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
private function get_feature_definitions() {
|
||||
if ( empty( $this->features ) ) {
|
||||
$legacy_features = array(
|
||||
'analytics' => array(
|
||||
'name' => __( 'Analytics', 'woocommerce' ),
|
||||
'description' => __( 'Enable WooCommerce Analytics', 'woocommerce' ),
|
||||
'option_key' => Analytics::TOGGLE_OPTION_NAME,
|
||||
'is_experimental' => false,
|
||||
'enabled_by_default' => true,
|
||||
'disable_ui' => false,
|
||||
'is_legacy' => true,
|
||||
),
|
||||
'new_navigation' => array(
|
||||
'name' => __( 'Navigation', 'woocommerce' ),
|
||||
'description' => __( 'Add the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
|
||||
'option_key' => Init::TOGGLE_OPTION_NAME,
|
||||
'is_experimental' => false,
|
||||
'disable_ui' => false,
|
||||
'is_legacy' => true,
|
||||
),
|
||||
'product_block_editor' => array(
|
||||
'name' => __( 'New product editor', 'woocommerce' ),
|
||||
'description' => __( 'Try the new product editor (Beta)', 'woocommerce' ),
|
||||
'is_experimental' => true,
|
||||
'disable_ui' => false,
|
||||
'is_legacy' => true,
|
||||
'disabled' => function() {
|
||||
return version_compare( get_bloginfo( 'version' ), '6.2', '<' );
|
||||
},
|
||||
'desc_tip' => function() {
|
||||
$string = '';
|
||||
if ( version_compare( get_bloginfo( 'version' ), '6.2', '<' ) ) {
|
||||
$string = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
|
||||
}
|
||||
return $string;
|
||||
},
|
||||
),
|
||||
'cart_checkout_blocks' => array(
|
||||
'name' => __( 'Cart & Checkout Blocks', 'woocommerce' ),
|
||||
'description' => __( 'Optimize for faster checkout', 'woocommerce' ),
|
||||
'is_experimental' => false,
|
||||
'disable_ui' => true,
|
||||
),
|
||||
'marketplace' => array(
|
||||
'name' => __( 'Marketplace', 'woocommerce' ),
|
||||
'description' => __(
|
||||
'New, faster way to find extensions and themes for your WooCommerce store',
|
||||
'woocommerce'
|
||||
),
|
||||
'is_experimental' => false,
|
||||
'enabled_by_default' => true,
|
||||
'disable_ui' => false,
|
||||
'is_legacy' => true,
|
||||
),
|
||||
);
|
||||
|
||||
foreach ( $legacy_features as $slug => $definition ) {
|
||||
$this->add_feature_definition( $slug, $definition['name'], $definition );
|
||||
}
|
||||
|
||||
/**
|
||||
* The action for registering features.
|
||||
*
|
||||
* @since 8.3.0
|
||||
*
|
||||
* @param FeaturesController $features_controller The instance of FeaturesController.
|
||||
*/
|
||||
do_action( 'woocommerce_register_feature_definitions', $this );
|
||||
|
||||
foreach ( array_keys( $this->features ) as $feature_id ) {
|
||||
$this->compatibility_info_by_feature[ $feature_id ] = array(
|
||||
'compatible' => array(),
|
||||
'incompatible' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->features;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +262,7 @@ class FeaturesController {
|
||||
* @returns array An array of information about existing features.
|
||||
*/
|
||||
public function get_features( bool $include_experimental = false, bool $include_enabled_info = false ): array {
|
||||
$features = $this->features;
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
if ( ! $include_experimental ) {
|
||||
$features = array_filter(
|
||||
@@ -253,7 +306,9 @@ class FeaturesController {
|
||||
* @return boolean TRUE if the feature is enabled by default, FALSE otherwise.
|
||||
*/
|
||||
private function feature_is_enabled_by_default( string $feature_id ): bool {
|
||||
return ! empty( $this->features[ $feature_id ]['enabled_by_default'] );
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
return ! empty( $features[ $feature_id ]['enabled_by_default'] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +390,9 @@ class FeaturesController {
|
||||
* @return bool True if the feature exists.
|
||||
*/
|
||||
private function feature_exists( string $feature_id ): bool {
|
||||
return isset( $this->features[ $feature_id ] );
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
return isset( $features[ $feature_id ] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +407,7 @@ class FeaturesController {
|
||||
public function get_compatible_features_for_plugin( string $plugin_name, bool $enabled_features_only = false ) : array {
|
||||
$this->verify_did_woocommerce_init( __FUNCTION__ );
|
||||
|
||||
$features = $this->features;
|
||||
$features = $this->get_feature_definitions();
|
||||
if ( $enabled_features_only ) {
|
||||
$features = array_filter(
|
||||
$features,
|
||||
@@ -422,25 +479,22 @@ class FeaturesController {
|
||||
|
||||
/**
|
||||
* Get the name of the option that enables/disables a given feature.
|
||||
* Note that it doesn't check if the feature actually exists.
|
||||
*
|
||||
* @param string $feature_id The id of the feature.
|
||||
* Note that it doesn't check if the feature actually exists. Instead it
|
||||
* defaults to "woocommerce_feature_{$feature_id}_enabled" if a different
|
||||
* name isn't specified in the feature registration.
|
||||
*
|
||||
* @param string $feature_id The id of the feature.
|
||||
* @return string The option that enables or disables the feature.
|
||||
*/
|
||||
public function feature_enable_option_name( string $feature_id ): string {
|
||||
switch ( $feature_id ) {
|
||||
case 'analytics':
|
||||
return Analytics::TOGGLE_OPTION_NAME;
|
||||
case 'new_navigation':
|
||||
return Init::TOGGLE_OPTION_NAME;
|
||||
case 'custom_order_tables':
|
||||
case CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION:
|
||||
return CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION;
|
||||
case DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION:
|
||||
return DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION;
|
||||
default:
|
||||
return "woocommerce_feature_{$feature_id}_enabled";
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
if ( ! empty( $features[ $feature_id ]['option_key'] ) ) {
|
||||
return $features[ $feature_id ]['option_key'];
|
||||
}
|
||||
|
||||
return "woocommerce_feature_{$feature_id}_enabled";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -451,7 +505,9 @@ class FeaturesController {
|
||||
* @return bool True if the id corresponds to a legacy feature.
|
||||
*/
|
||||
public function is_legacy_feature( string $feature_id ): bool {
|
||||
return in_array( $feature_id, $this->legacy_feature_ids, true );
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
return ! empty( $features[ $feature_id ]['is_legacy'] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,23 +543,24 @@ class FeaturesController {
|
||||
*
|
||||
* It fires FEATURE_ENABLED_CHANGED_ACTION when a feature is enabled or disabled.
|
||||
*
|
||||
* @param string $option The option that has been modified.
|
||||
* @param string $option The option that has been modified.
|
||||
* @param mixed $old_value The old value of the option.
|
||||
* @param mixed $value The new value of the option.
|
||||
* @param mixed $value The new value of the option.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function process_updated_option( string $option, $old_value, $value ) {
|
||||
$matches = array();
|
||||
$success = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
|
||||
|
||||
$known_features = array(
|
||||
Analytics::TOGGLE_OPTION_NAME,
|
||||
Init::TOGGLE_OPTION_NAME,
|
||||
NewProductManagementExperience::TOGGLE_OPTION_NAME,
|
||||
DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION,
|
||||
CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION,
|
||||
$matches = array();
|
||||
$is_default_key = preg_match( '/^woocommerce_feature_([a-zA-Z0-9_]+)_enabled$/', $option, $matches );
|
||||
$features_with_custom_keys = array_filter(
|
||||
$this->get_feature_definitions(),
|
||||
function( $feature ) {
|
||||
return ! empty( $feature['option_key'] );
|
||||
}
|
||||
);
|
||||
$custom_keys = wp_list_pluck( $features_with_custom_keys, 'option_key' );
|
||||
|
||||
if ( ! $success && ! in_array( $option, $known_features, true ) ) {
|
||||
if ( ! $is_default_key && ! in_array( $option, $custom_keys, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -511,14 +568,15 @@ class FeaturesController {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( Analytics::TOGGLE_OPTION_NAME === $option ) {
|
||||
$feature_id = 'analytics';
|
||||
} elseif ( Init::TOGGLE_OPTION_NAME === $option ) {
|
||||
$feature_id = 'new_navigation';
|
||||
} elseif ( in_array( $option, $known_features, true ) ) {
|
||||
$feature_id = $option;
|
||||
} else {
|
||||
$feature_id = '';
|
||||
if ( $is_default_key ) {
|
||||
$feature_id = $matches[1];
|
||||
} elseif ( in_array( $option, $custom_keys, true ) ) {
|
||||
$feature_id = array_search( $option, $custom_keys, true );
|
||||
}
|
||||
|
||||
if ( ! $feature_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -562,28 +620,21 @@ class FeaturesController {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/**
|
||||
* Filter allowing WooCommerce Admin to be disabled.
|
||||
*
|
||||
* @param bool $disabled False.
|
||||
*/
|
||||
$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
|
||||
$feature_settings =
|
||||
$feature_settings = array(
|
||||
array(
|
||||
array(
|
||||
'title' => __( 'Features', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'desc' => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
|
||||
'id' => 'features_options',
|
||||
),
|
||||
);
|
||||
'title' => __( 'Features', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'desc' => __( 'Start using new features that are being progressively rolled out to improve the store management experience.', 'woocommerce' ),
|
||||
'id' => 'features_options',
|
||||
),
|
||||
);
|
||||
|
||||
$features = $this->get_features( true );
|
||||
|
||||
$feature_ids = array_keys( $features );
|
||||
usort( $feature_ids, function( $feature_id_a, $feature_id_b ) use ( $features ) {
|
||||
return ( $features[ $feature_id_b ]['order'] ?? 0 ) <=> ( $features[ $feature_id_a ]['order'] ?? 0 );
|
||||
} );
|
||||
$experimental_feature_ids = array_filter(
|
||||
$feature_ids,
|
||||
function( $feature_id ) use ( $features ) {
|
||||
@@ -624,7 +675,12 @@ class FeaturesController {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ], $admin_features_disabled );
|
||||
$feature_settings[] = $this->get_setting_for_feature( $id, $features[ $id ] );
|
||||
|
||||
$additional_settings = $features[ $id ]['additional_settings'] ?? array();
|
||||
if ( count( $additional_settings ) > 0 ) {
|
||||
$feature_settings = array_merge( $feature_settings, $additional_settings );
|
||||
}
|
||||
}
|
||||
|
||||
$feature_settings[] = array(
|
||||
@@ -632,6 +688,20 @@ class FeaturesController {
|
||||
'id' => empty( $experimental_feature_ids ) ? 'features_options' : 'experimental_features_options',
|
||||
);
|
||||
|
||||
// Allow feature setting properties to be determined dynamically just before being rendered.
|
||||
$feature_settings = array_map(
|
||||
function( $feature_setting ) {
|
||||
foreach ( $feature_setting as $prop => $value ) {
|
||||
if ( is_callable( $value ) ) {
|
||||
$feature_setting[ $prop ] = call_user_func( $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $feature_setting;
|
||||
},
|
||||
$feature_settings
|
||||
);
|
||||
|
||||
return $feature_settings;
|
||||
}
|
||||
|
||||
@@ -640,15 +710,24 @@ class FeaturesController {
|
||||
*
|
||||
* @param string $feature_id The feature id.
|
||||
* @param array $feature The feature parameters, as returned by get_features.
|
||||
* @param bool $admin_features_disabled True if admin features have been disabled via 'woocommerce_admin_disabled' filter.
|
||||
* @return array The parameters to add to the settings array.
|
||||
*/
|
||||
private function get_setting_for_feature( string $feature_id, array $feature, bool $admin_features_disabled ): array {
|
||||
$description = $feature['description'] ?? '';
|
||||
$disabled = false;
|
||||
$desc_tip = '';
|
||||
$tooltip = $feature['tooltip'] ?? '';
|
||||
$type = $feature['type'] ?? 'checkbox';
|
||||
private function get_setting_for_feature( string $feature_id, array $feature ): array {
|
||||
$description = $feature['description'] ?? '';
|
||||
$disabled = false;
|
||||
$desc_tip = '';
|
||||
$tooltip = $feature['tooltip'] ?? '';
|
||||
$type = $feature['type'] ?? 'checkbox';
|
||||
$setting_definition = $feature['setting'] ?? array();
|
||||
|
||||
// phpcs:disable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
/**
|
||||
* Filter allowing WooCommerce Admin to be disabled.
|
||||
*
|
||||
* @param bool $disabled False.
|
||||
*/
|
||||
$admin_features_disabled = apply_filters( 'woocommerce_admin_disabled', false );
|
||||
// phpcs:enable WooCommerce.Commenting.CommentHooks.MissingSinceComment
|
||||
|
||||
if ( ( 'analytics' === $feature_id || 'new_navigation' === $feature_id ) && $admin_features_disabled ) {
|
||||
$disabled = true;
|
||||
@@ -658,13 +737,13 @@ class FeaturesController {
|
||||
|
||||
if ( $disabled ) {
|
||||
$update_text = sprintf(
|
||||
// translators: 1: line break tag.
|
||||
// translators: 1: line break tag.
|
||||
__( '%1$s The development of this feature is currently on hold.', 'woocommerce' ),
|
||||
'<br/>'
|
||||
);
|
||||
} else {
|
||||
$update_text = sprintf(
|
||||
// translators: 1: line break tag.
|
||||
// translators: 1: line break tag.
|
||||
__(
|
||||
'%1$s This navigation will soon become unavailable while we make necessary improvements.
|
||||
If you turn it off now, you will not be able to turn it back on.',
|
||||
@@ -677,7 +756,7 @@ class FeaturesController {
|
||||
$needs_update = version_compare( get_bloginfo( 'version' ), '5.6', '<' );
|
||||
if ( $needs_update && current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) {
|
||||
$update_text = sprintf(
|
||||
// translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag.
|
||||
// translators: 1: line break tag, 2: open link to WordPress update link, 3: close link tag.
|
||||
__( '%1$s %2$sUpdate WordPress to enable the new navigation%3$s', 'woocommerce' ),
|
||||
'<br/>',
|
||||
'<a href="' . self_admin_url( 'update-core.php' ) . '" target="_blank">',
|
||||
@@ -691,13 +770,6 @@ class FeaturesController {
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'product_block_editor' === $feature_id ) {
|
||||
$disabled = version_compare( get_bloginfo( 'version' ), '6.2', '<' );
|
||||
if ( $disabled ) {
|
||||
$desc_tip = __( '⚠ This feature is compatible with WordPress version 6.2 or higher.', 'woocommerce' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $this->is_legacy_feature( $feature_id ) && ! $disabled && $this->verify_did_woocommerce_init() ) {
|
||||
$disabled = ! $this->feature_is_enabled( $feature_id );
|
||||
$plugin_info_for_feature = $this->get_compatible_plugins_for_feature( $feature_id, true );
|
||||
@@ -716,7 +788,7 @@ class FeaturesController {
|
||||
*/
|
||||
$desc_tip = apply_filters( 'woocommerce_feature_description_tip', $desc_tip, $feature_id, $disabled );
|
||||
|
||||
$feature_setting = array(
|
||||
$feature_setting_defaults = array(
|
||||
'title' => $feature['name'],
|
||||
'desc' => $description,
|
||||
'type' => $type,
|
||||
@@ -727,6 +799,8 @@ class FeaturesController {
|
||||
'default' => $this->feature_is_enabled_by_default( $feature_id ) ? 'yes' : 'no',
|
||||
);
|
||||
|
||||
$feature_setting = wp_parse_args( $setting_definition, $feature_setting_defaults );
|
||||
|
||||
/**
|
||||
* Allows to modify feature setting that will be used to render in the feature page.
|
||||
*
|
||||
@@ -759,7 +833,6 @@ class FeaturesController {
|
||||
|
||||
$incompatibles = $this->compatibility_info_by_feature[ $feature ]['incompatible'];
|
||||
$this->compatibility_info_by_feature[ $feature ]['incompatible'] = array_diff( $incompatibles, array( $plugin_name ) );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,9 +881,11 @@ class FeaturesController {
|
||||
}
|
||||
|
||||
$compatibility = $this->get_compatible_features_for_plugin( $plugin_name );
|
||||
$incompatible_with = array_diff(
|
||||
$incompatible_with = array_filter(
|
||||
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
|
||||
$this->legacy_feature_ids
|
||||
function( $feature_id ) {
|
||||
return ! $this->is_legacy_feature( $feature_id );
|
||||
}
|
||||
);
|
||||
|
||||
if ( ( 'all' === $feature_id && ! empty( $incompatible_with ) ) || in_array( $feature_id, $incompatible_with, true ) ) {
|
||||
@@ -849,9 +924,11 @@ class FeaturesController {
|
||||
|
||||
foreach ( $this->plugin_util->get_woocommerce_aware_plugins( true ) as $plugin ) {
|
||||
$compatibility = $this->get_compatible_features_for_plugin( $plugin, true );
|
||||
$incompatible_with = array_diff(
|
||||
$incompatible_with = array_filter(
|
||||
array_merge( $compatibility['incompatible'], $compatibility['uncertain'] ),
|
||||
$this->legacy_feature_ids
|
||||
function( $feature_id ) {
|
||||
return ! $this->is_legacy_feature( $feature_id );
|
||||
}
|
||||
);
|
||||
|
||||
if ( $incompatible_with ) {
|
||||
@@ -904,7 +981,7 @@ class FeaturesController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification
|
||||
$features = $this->get_feature_definitions();
|
||||
$plugins_page_url = admin_url( 'plugins.php' );
|
||||
$features_page_url = $this->get_features_page_url();
|
||||
|
||||
@@ -914,7 +991,7 @@ class FeaturesController {
|
||||
: sprintf(
|
||||
/* translators: %s is a feature name. */
|
||||
__( "You are viewing the active plugins that are incompatible with the '%s' feature.", 'woocommerce' ),
|
||||
$this->features[ $feature_id ]['name']
|
||||
$features[ $feature_id ]['name']
|
||||
);
|
||||
|
||||
$message .= '<br />';
|
||||
@@ -975,6 +1052,7 @@ class FeaturesController {
|
||||
return;
|
||||
}
|
||||
|
||||
$features = $this->get_feature_definitions();
|
||||
$feature_compatibility_info = $this->get_compatible_features_for_plugin( $plugin_file, true );
|
||||
$incompatible_features = array_merge( $feature_compatibility_info['incompatible'], $feature_compatibility_info['uncertain'] );
|
||||
$incompatible_features = array_values(
|
||||
@@ -997,21 +1075,21 @@ class FeaturesController {
|
||||
$message = sprintf(
|
||||
/* translators: %s = printable plugin name */
|
||||
__( "⚠ This plugin is incompatible with the enabled WooCommerce feature '%s', it shouldn't be activated.", 'woocommerce' ),
|
||||
$this->features[ $incompatible_features[0] ]['name']
|
||||
$features[ $incompatible_features[0] ]['name']
|
||||
);
|
||||
} elseif ( 2 === $incompatible_features_count ) {
|
||||
/* translators: %1\$s, %2\$s = printable plugin names */
|
||||
$message = sprintf(
|
||||
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s' and '%2\$s', it shouldn't be activated.", 'woocommerce' ),
|
||||
$this->features[ $incompatible_features[0] ]['name'],
|
||||
$this->features[ $incompatible_features[1] ]['name']
|
||||
$features[ $incompatible_features[0] ]['name'],
|
||||
$features[ $incompatible_features[1] ]['name']
|
||||
);
|
||||
} else {
|
||||
/* translators: %1\$s, %2\$s = printable plugin names, %3\$d = plugins count */
|
||||
$message = sprintf(
|
||||
__( "⚠ This plugin is incompatible with the enabled WooCommerce features '%1\$s', '%2\$s' and %3\$d more, it shouldn't be activated.", 'woocommerce' ),
|
||||
$this->features[ $incompatible_features[0] ]['name'],
|
||||
$this->features[ $incompatible_features[1] ]['name'],
|
||||
$features[ $incompatible_features[0] ]['name'],
|
||||
$features[ $incompatible_features[1] ]['name'],
|
||||
$incompatible_features_count - 2
|
||||
);
|
||||
}
|
||||
@@ -1040,7 +1118,7 @@ class FeaturesController {
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_features_page_url(): string {
|
||||
public function get_features_page_url(): string {
|
||||
return admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' );
|
||||
}
|
||||
|
||||
@@ -1112,13 +1190,14 @@ class FeaturesController {
|
||||
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
|
||||
|
||||
$all_items = get_plugins();
|
||||
$features = $this->get_feature_definitions();
|
||||
|
||||
$incompatible_plugins_count = count( $this->filter_plugins_list( $all_items ) );
|
||||
$incompatible_text =
|
||||
'all' === $feature_id
|
||||
? __( 'Incompatible with WooCommerce features', 'woocommerce' )
|
||||
/* translators: %s = name of a WooCommerce feature */
|
||||
: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $this->features[ $feature_id ]['name'] );
|
||||
: sprintf( __( "Incompatible with '%s'", 'woocommerce' ), $features[ $feature_id ]['name'] );
|
||||
$incompatible_link = "<a href='plugins.php?plugin_status=incompatible_with_feature&feature_id={$feature_id}' class='current' aria-current='page'>{$incompatible_text} <span class='count'>({$incompatible_plugins_count})</span></a>";
|
||||
|
||||
$all_plugins_count = count( $all_items );
|
||||
@@ -1158,7 +1237,7 @@ class FeaturesController {
|
||||
|
||||
$query_params_to_remove = array( '_feature_nonce' );
|
||||
|
||||
foreach ( array_keys( $this->features ) as $feature_id ) {
|
||||
foreach ( array_keys( $this->get_feature_definitions() ) as $feature_id ) {
|
||||
if ( isset( $_GET[ $feature_id ] ) && is_numeric( $_GET[ $feature_id ] ) ) {
|
||||
$value = absint( $_GET[ $feature_id ] );
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class DatabaseUtil {
|
||||
|
||||
foreach ( $dbdelta_output as $table_name => $result ) {
|
||||
if ( "Created table $table_name" === $result ) {
|
||||
$created_tables[] = $table_name;
|
||||
$created_tables[] = str_replace( '(', '', $table_name );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,23 +233,62 @@ class DatabaseUtil {
|
||||
*/
|
||||
public function insert_on_duplicate_key_update( $table_name, $data, $format ) : int {
|
||||
global $wpdb;
|
||||
if ( empty( $data ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$columns = array_keys( $data );
|
||||
$columns = array_keys( $data );
|
||||
$value_format = array();
|
||||
$values = array();
|
||||
$index = 0;
|
||||
// Directly use NULL for placeholder if the value is NULL, since otherwise $wpdb->prepare will convert it to empty string.
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( is_null( $value ) ) {
|
||||
$value_format[] = 'NULL';
|
||||
} else {
|
||||
$values[] = $value;
|
||||
$value_format[] = $format[ $index ];
|
||||
}
|
||||
$index++;
|
||||
}
|
||||
$column_clause = '`' . implode( '`, `', $columns ) . '`';
|
||||
$value_placeholders = implode( ', ', array_values( $format ) );
|
||||
$value_format_clause = implode( ', ', $value_format );
|
||||
$on_duplicate_clause = $this->generate_on_duplicate_statement_clause( $columns );
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Values are escaped in $wpdb->prepare.
|
||||
$sql = $wpdb->prepare(
|
||||
"
|
||||
INSERT INTO $table_name ( $column_clause )
|
||||
VALUES ( $value_placeholders )
|
||||
VALUES ( $value_format_clause )
|
||||
$on_duplicate_clause
|
||||
",
|
||||
array_values( $data )
|
||||
$values
|
||||
);
|
||||
// phpcs:enable
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is prepared.
|
||||
return $wpdb->query( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max index length.
|
||||
*
|
||||
* @return int Max index length.
|
||||
*/
|
||||
public function get_max_index_length() : int {
|
||||
/**
|
||||
* Filters the maximum index length in the database.
|
||||
*
|
||||
* Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
|
||||
* As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which
|
||||
* used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
|
||||
*
|
||||
* Additionally, MyISAM engine also limits the index size to 1000 bytes. We add this filter so that interested folks on InnoDB engine can increase the size till allowed 3071 bytes.
|
||||
*
|
||||
* @param int $max_index_length Maximum index length. Default 191.
|
||||
*
|
||||
* @since 8.0.0
|
||||
*/
|
||||
$max_index_length = apply_filters( 'woocommerce_database_max_index_length', 191 );
|
||||
// Index length cannot be more than 768, which is 3078 bytes in utf8mb4 and max allowed by InnoDB engine.
|
||||
return min( absint( $max_index_length ), 767 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace Automattic\WooCommerce\Internal\Utilities;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Cache_Helper;
|
||||
|
||||
/**
|
||||
* Class with utility methods for dealing with webhooks.
|
||||
@@ -133,4 +134,23 @@ class WebhookUtil {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of webhooks that are configured to use the Legacy REST API to compose their payloads.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_legacy_webhooks_count(): int {
|
||||
global $wpdb;
|
||||
|
||||
$cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'legacy_count';
|
||||
$count = wp_cache_get( $cache_key, 'webhooks' );
|
||||
|
||||
if ( false === $count ) {
|
||||
$count = absint( $wpdb->get_var( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `api_version` < 1;" ) );
|
||||
wp_cache_add( $cache_key, $count, 'webhooks' );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user