From e35c7ac6dcbb8d79beb3c12bcbb07f8312db451e Mon Sep 17 00:00:00 2001 From: Josh Betz Date: Fri, 4 Mar 2022 15:06:43 -0600 Subject: [PATCH 001/206] Filter out product variation line_item meta There are cases where we want to display line item meta, similar to the checkout flow on the web. The web filters out variation meta because it's redundant. The product name already includes the relevant meta. ref: https://github.com/woocommerce/woocommerce/blob/8bc310008c4786f3c7a8ecfc99b470accc77c183/plugins/woocommerce/includes/class-wc-order-item.php#L282-L285 --- .../class-wc-rest-orders-v2-controller.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index 64cc4aa9df6..b80be00ef60 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -210,6 +210,27 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { // Expand meta_data to include user-friendly values. $formatted_meta_data = $item->get_formatted_meta_data( null, true ); + + // Filter out product variations + // TODO: Add a query arg to activate this. Default should be to keep variation meta for back-compat + if ( $product ) { + $order_item_name = $data['name']; + $data['meta_data'] = array_filter( $data['meta_data'], function( $meta ) use ( $product, $order_item_name ) { + $meta->key = rawurldecode( (string) $meta->key ); + $meta->value = rawurldecode( (string) $meta->value ); + $attribute_key = str_replace( 'attribute_', '', $meta->key ); + $display_key = wc_attribute_label( $attribute_key, $product ); + $display_value = wp_kses_post( $meta->value ); + + // Skip items with values already in the product details area of the product name. + if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + return false; + } + + return true; + } ); + } + $data['meta_data'] = array_map( array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), $data['meta_data'], From 0114d3b5d64879464db519e410ec842dd8c354e7 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Tue, 8 Mar 2022 16:54:45 +0100 Subject: [PATCH 002/206] Infrastructure for the sync process - Update settings UI - Start sync via scheduled actions when sync is enabled - Auto-switch authoritative table on sync finished if so configured - Disable auto-switch if sync is disabled - Show initial and current count of orders pending sync in settings UI --- .../admin/class-wc-admin-settings.php | 9 + .../Orders/CustomOrdersTableController.php | 217 ++++++++++++++++-- .../DataStores/Orders/DataSynchronizer.php | 127 +++++++++- 3 files changed, 331 insertions(+), 22 deletions(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php index 69506066749..48bbe0e6e2a 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php @@ -7,6 +7,7 @@ */ use Automattic\Jetpack\Constants; +use Automattic\WooCommerce\Utilities\ArrayUtil; if ( ! defined( 'ABSPATH' ) ) { exit; @@ -273,6 +274,12 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : } break; + case 'info': + echo ''; + echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) ); + echo ''; + break; + // Section Ends. case 'sectionend': if ( ! empty( $value['id'] ) ) { @@ -417,6 +424,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : // Radio inputs. case 'radio': $option_value = $value['value']; + $disabled_values = ArrayUtil::get_value_or_default($value, 'disabled', array()); ?> @@ -435,6 +443,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : name="" value="" type="radio" + style="" class="" diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index aaaa777505e..338a6e17608 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -92,6 +92,38 @@ class CustomOrdersTableController { 999, 2 ); + + add_filter( + 'updated_option', + function( $option, $old_value, $value ) { + $this->process_updated_option( $option, $old_value, $value ); + }, + 999, + 3 + ); + + add_filter( + 'pre_update_option', + function( $value, $option, $old_value ) { + return $this->process_pre_update_option( $option, $old_value, $value ); + }, + 999, + 3 + ); + + add_filter( + DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, + function() { + $this->process_sync_finished(); + } + ); + + add_action( + 'woocommerce_update_options_advanced_custom_data_stores', + function() { + $this->process_options_updated(); + } + ); } /** @@ -213,6 +245,7 @@ class CustomOrdersTableController { } $this->data_synchronizer->create_database_tables(); + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); } /** @@ -258,40 +291,182 @@ class CustomOrdersTableController { return $settings; } - $title_item = array( - 'title' => __( 'Custom orders tables', 'woocommerce' ), - 'type' => 'title', - 'desc' => sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), - '', - '' - ), - ); - if ( $this->data_synchronizer->check_orders_table_exists() ) { - $settings[] = $title_item; + $settings[] = array( + 'title' => __( 'Custom orders tables', 'woocommerce' ), + 'type' => 'title', + 'id' => 'cot-title', + 'desc' => sprintf( + /* translators: %1$s = tag, %2$s = tag. */ + __( '%1$sWARNING:%2$s This feature is currently under development and may cause database instability. For contributors only.', 'woocommerce' ), + '', + '' + ), + ); + + $sync_status = $this->data_synchronizer->get_sync_status(); + $sync_is_pending = 0 !== $sync_status['current_pending_count']; $settings[] = array( - 'title' => __( 'Enable tables usage', 'woocommerce' ), - 'desc' => __( 'Use the custom orders tables as the main orders data store.', 'woocommerce' ), + 'title' => __( 'Data store for orders', 'woocommerce' ), 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'default' => 'no', - 'type' => 'checkbox', + 'type' => 'radio', + 'options' => array( + 'yes' => __( 'Use the WooCommerce orders tables', 'woocommerce' ), + 'no' => __( 'Use the WordPress posts table', 'woocommerce' ), + ), 'checkboxgroup' => 'start', + 'disabled' => $sync_is_pending ? array( 'yes', 'no' ) : array(), ); + + if ( $sync_is_pending ) { + $initial_pending_count = $sync_status['initial_pending_count']; + if ( $initial_pending_count ) { + $text = sprintf( + /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ + __( 'There are %1$s orders (out of a total of %2$s) pending sync!', 'woocommerce' ), + $sync_status['current_pending_count'], + $initial_pending_count + ); + } else { + $text = sprintf( + /* translators: %1$s=current number of orders pending sync */ + __( 'There are %1$s orders pending sync!', 'woocommerce' ), + $sync_status['current_pending_count'] + ); + } + + if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) { + $text .= __( '
Syncrhonization for these orders is currently in progress.', 'woocommerce' ); + } + + $text .= __( "
The authoritative table can't be changed until sync completes.", 'woocommerce' ); + + $settings[] = array( + 'type' => 'info', + 'id' => 'cot-out-of-sync-warning', + 'css' => 'color: #C00000', + 'text' => $text, + ); + } + + $settings[] = array( + 'desc' => __( 'Keep the posts table and the orders tables synchronized', 'woocommerce' ), + 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, + 'type' => 'checkbox', + ); + + if ( $sync_is_pending ) { + if ( $this->data_synchronizer->data_sync_is_enabled() ) { + $message = $this->custom_orders_table_usage_is_enabled() ? + __( 'Switch to using the posts table as the authoritative data store for orders when sync finishes', 'woocommerce' ) : + __( 'Switch to using the orders table as the authoritative data store for orders when sync finishes', 'woocommerce' ); + $settings[] = array( + 'desc' => $message, + 'id' => DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, + 'type' => 'checkbox', + ); + } + } } else { - $title_item['desc'] = sprintf( - /* translators: %1$s = tag, %2$s = tag. */ - __( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), - '', - '' + $settings[] = array( + 'title' => __( 'Custom orders tables', 'woocommerce' ), + 'type' => 'title', + 'desc' => sprintf( + /* translators: %1$s = tag, %2$s = tag. */ + __( 'Create the tables first by going to %1$sWooCommerce > Status > Tools%2$s and running %1$sCreate the custom orders tables%2$s.', 'woocommerce' ), + '', + '' + ), ); - $settings[] = $title_item; } $settings[] = array( 'type' => 'sectionend' ); return $settings; } + + /** + * Handler for the individual setting updated hook. + * + * @param string $option Setting name. + * @param mixed $old_value Old value of the setting. + * @param mixed $value New value of the setting. + */ + private function process_updated_option( $option, $old_value, $value ) { + if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) { + $this->data_synchronizer->cleanup_synchronization_state(); + } + } + + /** + * Handler for the setting pre-update hook. + * We use it to verify that authoritative orders table switch doesn't happen while sync is pending. + * + * @param string $option Setting name. + * @param mixed $old_value Old value of the setting. + * @param mixed $value New value of the setting. + * + * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending. + */ + private function process_pre_update_option( $option, $old_value, $value ) { + if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) { + return $value; + } + + $sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count(); + if ( $sync_is_pending ) { + throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" ); + } + + return $value; + } + + /** + * Handler for the synchronization finished hook. + * Here we switch the authoritative table if needed. + */ + private function process_sync_finished() { + if ( $this->auto_flip_authoritative_table_enabled() ) { + return; + } + + update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' ); + + if ( $this->custom_orders_table_usage_is_enabled() ) { + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'no' ); + } else { + update_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, 'yes' ); + } + } + + /** + * Is the automatic authoritative table switch setting set? + * + * @return bool + */ + private function auto_flip_authoritative_table_enabled(): bool { + return 'yes' === get_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ); + } + + /** + * Handler for the all settings updated hook. + */ + private function process_options_updated() { + $data_sync_is_enabled = $this->data_synchronizer->data_sync_is_enabled(); + + // Disabling the sync implies disabling the automatic authoritative table switch too. + if ( ! $data_sync_is_enabled && $this->auto_flip_authoritative_table_enabled() ) { + update_option( DataSynchronizer::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION, 'no' ); + } + + // Enabling the sync implies starting 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. + if ( $data_sync_is_enabled && ! $this->data_synchronizer->pending_data_sync_is_in_progress() ) { + $this->data_synchronizer->start_synchronizing_pending_orders(); + } + } } diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 19894cfcb18..579a419e43b 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -17,6 +17,13 @@ defined( 'ABSPATH' ) || exit; */ class DataSynchronizer { + const ORDERS_DATA_SYNC_ENABLED_OPTION = 'woocommerce_custom_orders_table_data_sync_enabled'; + const INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_initial_orders_pending_sync_count'; + const AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION = 'woocommerce_auto_flip_authoritative_table_roles'; + const PENDING_SYNC_IS_IN_PROGRESS_OPTION = 'woocommerce_custom_orders_table_pending_sync_in_progress'; + const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback'; + const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; + /** * The data store object to use. * @@ -31,7 +38,17 @@ class DataSynchronizer { */ private $database_util; - // TODO: Add a constructor to handle hooks as appropriate. + /** + * Class constructor. + */ + public function __construct() { + add_action( + self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK, + function() { + $this->do_pending_orders_synchronization(); + } + ); + } /** * Class initialization, invoked by the DI container. @@ -74,5 +91,113 @@ class DataSynchronizer { } } + /** + * Is the data sync between old and new tables currently enabled? + * + * @return bool + */ + public function data_sync_is_enabled(): bool { + return 'yes' === get_option( self::ORDERS_DATA_SYNC_ENABLED_OPTION ); + } + /** + * Is a sync process currently in progress? + * + * @return bool + */ + public function pending_data_sync_is_in_progress(): bool { + return 'yes' === get_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION ); + } + + /** + * Get the current sync process status. + * The information is meaningful only if pending_data_sync_is_in_progress return true. + * + * @return array + */ + public function get_sync_status() { + return array( + 'initial_pending_count' => (int) get_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, 0 ), + 'current_pending_count' => $this->get_current_orders_pending_sync_count(), + 'auto_flip' => 'yes' === get_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ), + 'sync_in_progress' => $this->pending_data_sync_is_in_progress(), + ); + } + + /** + * Calculate how many orders need to be synchronized currently. + */ + public function get_current_orders_pending_sync_count(): int { + // TODO: get this value by querying the database. + return get_option( 'woocommerce_fake_orders_pending_sync_count', 0 ); + } + + /** + * Start an orders synchronization process. + * This will setup the appropriate status information and schedule the first synchronization batch. + */ + public function start_synchronizing_pending_orders() { + $initial_pending_count = $this->get_current_orders_pending_sync_count(); + if ( 0 === $initial_pending_count ) { + return; + } + + update_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION, $initial_pending_count ); + + $queue = WC()->get_instance_of( \WC_Queue::class ); + $queue->cancel_all( self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK ); + + update_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION, 'yes' ); + $this->schedule_pending_orders_synchronization(); + } + + /** + * Schedule the next orders synchronization batch. + */ + private function schedule_pending_orders_synchronization() { + $queue = WC()->get_instance_of( \WC_Queue::class ); + $queue->schedule_single( + WC()->call_function( 'time' ) + 1, + self::ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK, + array(), + 'woocommerce-db-updates' + ); + } + + /** + * Run one orders synchronization batch. + */ + private function do_pending_orders_synchronization() { + if ( ! $this->pending_data_sync_is_in_progress() ) { + return; + } + + // TODO: Syncrhonize a batch of orders. + $fake_count = (int) get_option( 'woocommerce_fake_orders_pending_sync_count', 0 ); + if ( $fake_count > 0 ) { + update_option( 'woocommerce_fake_orders_pending_sync_count', $fake_count - 1 ); + } + + if ( 0 === $this->get_current_orders_pending_sync_count() ) { + $this->cleanup_synchronization_state(); + + /** + * Hook to signal that the orders tables synchronization process has finised (nothing left to synchronize). + */ + do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION ); + } else { + $this->schedule_pending_orders_synchronization(); + } + } + + /** + * Cleanup all the synchronization status information, + * because the process has been disabled by the user via settings, + * or because there's nothing left to syncrhonize. + */ + public function cleanup_synchronization_state() { + delete_option( self::INITIAL_ORDERS_PENDING_SYNC_COUNT_OPTION ); + delete_option( self::PENDING_SYNC_IS_IN_PROGRESS_OPTION ); + delete_option( self::AUTO_FLIP_AUTHORITATIVE_TABLE_ROLES_OPTION ); + } } From c49280b91ad97c94c414d84be52bd205a4ea42f4 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Wed, 9 Mar 2022 12:09:19 +0100 Subject: [PATCH 003/206] Implement DataSynchronizer::get_current_orders_pending_sync_count Use real SQL to get the count of unsynced orders. --- .../DataStores/Orders/DataSynchronizer.php | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 579a419e43b..07d5160922e 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -24,6 +24,9 @@ class DataSynchronizer { const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback'; const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + const FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_fake_orders_pending_sync_count'; + /** * The data store object to use. * @@ -126,10 +129,53 @@ class DataSynchronizer { /** * Calculate how many orders need to be synchronized currently. + * + * If an option whose name is given by self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION exists, + * then the value of that option is returned. This is temporary, to ease testing the feature + * while it is in development. + * + * Otherwise a database query is performed to get how many orders match one of the following: + * + * - Existing in the authoritative table but not in the backup table. + * - Existing in both tables, but they have a different update date. */ public function get_current_orders_pending_sync_count(): int { - // TODO: get this value by querying the database. - return get_option( 'woocommerce_fake_orders_pending_sync_count', 0 ); + global $wpdb; + + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + $count = get_option( self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION ); + if ( false !== $count ) { + return (int) $count; + } + + $orders_table = $wpdb->prefix . 'wc_orders'; + + if ( 'yes' === get_option( CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) ) { + $missing_orders_count_sql = "SELECT COUNT(1) FROM $orders_table WHERE post_id IS NULL"; + } else { + $missing_orders_count_sql = " +SELECT COUNT(1) FROM ( + SELECT posts.ID,orders.post_id FROM $wpdb->posts posts + LEFT JOIN $orders_table orders ON posts.ID = orders.post_id + WHERE posts.post_type='shop_order' AND orders.post_id IS NULL + AND posts.post_status != 'auto-draft' +) x"; + + } + + $sql = " +SELECT( + ($missing_orders_count_sql) + + + (SELECT COUNT(1) FROM ( + SELECT orders.post_id FROM $orders_table orders + JOIN $wpdb->posts posts on posts.ID = orders.post_id + WHERE orders.date_updated_gmt != posts.post_modified_gmt + ) x) +) count"; + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return (int) $wpdb->get_var( $wpdb->prepare( $sql ) ); } /** @@ -172,10 +218,13 @@ class DataSynchronizer { return; } - // TODO: Syncrhonize a batch of orders. - $fake_count = (int) get_option( 'woocommerce_fake_orders_pending_sync_count', 0 ); - if ( $fake_count > 0 ) { - update_option( 'woocommerce_fake_orders_pending_sync_count', $fake_count - 1 ); + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. + $fake_count = get_option( self::FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION ); + if ( false !== $fake_count ) { + update_option( 'woocommerce_fake_orders_pending_sync_count', (int) $fake_count - 1 ); + // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse + } else { + // TODO: Synchronize a batch of orders. } if ( 0 === $this->get_current_orders_pending_sync_count() ) { From 65b716346d039424b4a5f0ac82ffd9721b9e7611 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Thu, 10 Mar 2022 11:41:58 +0100 Subject: [PATCH 004/206] Add the DataSynchronizer::get_ids_of_orders_pending_sync method --- .../DataStores/Orders/DataSynchronizer.php | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 07d5160922e..802b69aeab2 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -24,6 +24,11 @@ class DataSynchronizer { const ORDERS_SYNC_SCHEDULED_ACTION_CALLBACK = 'woocommerce_run_orders_sync_callback'; const PENDING_SYNCHRONIZATION_FINISHED_ACTION = 'woocommerce_orders_sync_finished'; + // Allowed values for $type in get_ids_of_orders_pending_sync method. + const ID_TYPE_MISSING_IN_ORDERS_TABLE = 0; + const ID_TYPE_MISSING_IN_POSTS_TABLE = 1; + const ID_TYPE_DIFFERENT_UPDATE_DATE = 2; + // TODO: Remove the usage of the fake pending orders count once development of the feature is complete. const FAKE_ORDERS_PENDING_SYNC_COUNT_OPTION = 'woocommerce_fake_orders_pending_sync_count'; @@ -160,7 +165,6 @@ SELECT COUNT(1) FROM ( WHERE posts.post_type='shop_order' AND orders.post_id IS NULL AND posts.post_status != 'auto-draft' ) x"; - } $sql = " @@ -178,6 +182,54 @@ SELECT( return (int) $wpdb->get_var( $wpdb->prepare( $sql ) ); } + /** + * Get a list of ids of orders than are out of sync. + * + * Valid values for $type are: + * + * ID_TYPE_MISSING_IN_ORDERS_TABLE: orders that exist in posts table but not in orders table. Returns post ids. + * ID_TYPE_MISSING_IN_POSTS_TABLE: orders that exist in orders table but not in posts table. Returns ids from orders table. + * ID_TYPE_DIFFERENT_UPDATE_DATE: orders that exist in both tables but have different last update dates. Returns ids from orders table. + * + * @param int $type One of ID_TYPE_MISSING_IN_ORDERS_TABLE, ID_TYPE_MISSING_IN_POSTS_TABLE, ID_TYPE_DIFFERENT_UPDATE_DATE. + * @param int $limit Maximum number of ids to return. + * @return array An array of order or post ids. + * @throws \Exception Invalid parameter. + */ + private function get_ids_of_orders_pending_sync( int $type, int $limit ) { + global $wpdb; + + if ( $limit < 1 ) { + throw new \Exception( '$limit must be at least 1' ); + } + + $orders_table = $wpdb->prefix . 'wc_orders'; + + switch ( $type ) { + case self::ID_TYPE_MISSING_IN_ORDERS_TABLE: + $sql = " +SELECT posts.ID FROM $wpdb->posts posts +LEFT JOIN $orders_table orders ON posts.ID = orders.post_id +WHERE posts.post_type='shop_order' AND orders.post_id IS NULL +AND posts.post_status != 'auto-draft'"; + break; + case self::ID_TYPE_MISSING_IN_POSTS_TABLE: + $sql = "SELECT id FROM $orders_table WHERE post_id IS NULL"; + break; + case self::ID_TYPE_DIFFERENT_UPDATE_DATE: + $sql = " +SELECT orders.id FROM $orders_table orders +JOIN $wpdb->posts posts on posts.ID = orders.post_id +WHERE orders.date_updated_gmt != posts.post_modified_gmt"; + break; + default: + throw new \Exception( 'Invalid $type, must be one of the ID_TYPE_... constants.' ); + } + + // phpcs:ignore WordPress.DB + return array_map( 'intval', $wpdb->get_col( $sql . " LIMIT $limit" ) ); + } + /** * Start an orders synchronization process. * This will setup the appropriate status information and schedule the first synchronization batch. @@ -224,7 +276,7 @@ SELECT( update_option( 'woocommerce_fake_orders_pending_sync_count', (int) $fake_count - 1 ); // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse } else { - // TODO: Synchronize a batch of orders. + // TODO: Use get_ids_of_orders_pending_sync to get a batch of order ids and syncrhonize them. } if ( 0 === $this->get_current_orders_pending_sync_count() ) { From 746d6a99b1f9e964332ea82d4ba152a283cb19aa Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Fri, 11 Mar 2022 16:56:31 +0100 Subject: [PATCH 005/206] Improve the "Orders out of sync" warning message in COT settings --- .../DataStores/Orders/CustomOrdersTableController.php | 6 +++--- .../src/Internal/DataStores/Orders/DataSynchronizer.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index 338a6e17608..5465fda33f7 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -338,11 +338,11 @@ class CustomOrdersTableController { } if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) { - $text .= __( '
Syncrhonization for these orders is currently in progress.', 'woocommerce' ); + $text .= __( "
Synchronization for these orders is currently in progress.
The authoritative table can't be changed until sync completes.", 'woocommerce' ); + } else { + $text .= __( "
The authoritative table can't be changed until these orders are synchronized.", 'woocommerce' ); } - $text .= __( "
The authoritative table can't be changed until sync completes.", 'woocommerce' ); - $settings[] = array( 'type' => 'info', 'id' => 'cot-out-of-sync-warning', diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 802b69aeab2..4b91d114c8b 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -283,7 +283,7 @@ WHERE orders.date_updated_gmt != posts.post_modified_gmt"; $this->cleanup_synchronization_state(); /** - * Hook to signal that the orders tables synchronization process has finised (nothing left to synchronize). + * Hook to signal that the orders tables synchronization process has finished (nothing left to synchronize). */ do_action( self::PENDING_SYNCHRONIZATION_FINISHED_ACTION ); } else { From 71cc725bb5e770f49c0f9afd54ef9d019ce7ac0e Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Wed, 16 Mar 2022 11:00:41 +0100 Subject: [PATCH 006/206] Some minor improvements in the custom orders table implmentation. --- .../admin/class-wc-admin-settings.php | 4 ++-- .../Orders/CustomOrdersTableController.php | 22 +++++++++---------- .../DataStores/Orders/DataSynchronizer.php | 10 ++++----- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php index 48bbe0e6e2a..a6a2499c7c1 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php @@ -275,7 +275,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : break; case 'info': - echo ''; + echo ''; echo wp_kses_post( wpautop( wptexturize( $value['text'] ) ) ); echo ''; break; @@ -443,7 +443,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) : name="" value="" type="radio" - + style="" class="" diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php index 5465fda33f7..baf04735c71 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php @@ -322,19 +322,19 @@ class CustomOrdersTableController { if ( $sync_is_pending ) { $initial_pending_count = $sync_status['initial_pending_count']; + $current_pending_count = $sync_status['current_pending_count']; if ( $initial_pending_count ) { - $text = sprintf( - /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ - __( 'There are %1$s orders (out of a total of %2$s) pending sync!', 'woocommerce' ), - $sync_status['current_pending_count'], - $initial_pending_count - ); + $text = + sprintf( + /* translators: %1$s=current number of orders pending sync, %2$s=initial number of orders pending sync */ + _n( 'There\'s %1$s order (out of a total of %2$s) pending sync!', 'There are %1$s orders (out of a total of %2$s) pending sync!', $current_pending_count, 'woocommerce' ), + $current_pending_count, + $initial_pending_count + ); } else { - $text = sprintf( - /* translators: %1$s=current number of orders pending sync */ - __( 'There are %1$s orders pending sync!', 'woocommerce' ), - $sync_status['current_pending_count'] - ); + $text = + /* translators: %s=initial number of orders pending sync */ + sprintf( _n( 'There\'s %s order pending sync!', 'There are %s orders pending sync!', $current_pending_count, 'woocommerce' ), $current_pending_count, 'woocommerce' ); } if ( $this->data_synchronizer->pending_data_sync_is_in_progress() ) { diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php index 4b91d114c8b..09bc4bf589c 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php @@ -159,12 +159,10 @@ class DataSynchronizer { $missing_orders_count_sql = "SELECT COUNT(1) FROM $orders_table WHERE post_id IS NULL"; } else { $missing_orders_count_sql = " -SELECT COUNT(1) FROM ( - SELECT posts.ID,orders.post_id FROM $wpdb->posts posts - LEFT JOIN $orders_table orders ON posts.ID = orders.post_id - WHERE posts.post_type='shop_order' AND orders.post_id IS NULL - AND posts.post_status != 'auto-draft' -) x"; +SELECT COUNT( posts.ID ) FROM $wpdb->posts posts +LEFT JOIN $orders_table orders ON posts.ID = orders.post_id +WHERE posts.post_type='shop_order' AND orders.post_id IS NULL +AND posts.post_status != 'auto-draft'"; } $sql = " From bb9feb7d68af2c7065523252cdacf2cd08f2f085 Mon Sep 17 00:00:00 2001 From: Josh Betz Date: Thu, 17 Mar 2022 14:20:17 -0500 Subject: [PATCH 007/206] Add `order_item_display_meta` query parameter By default we will show all meta for order items to maintain backwards compatibility. When this new parameter is set, we will filter out variation meta just as core does on the web. --- .../class-wc-rest-orders-v2-controller.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index b80be00ef60..bc6333dd33e 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -211,24 +211,26 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { // Expand meta_data to include user-friendly values. $formatted_meta_data = $item->get_formatted_meta_data( null, true ); - // Filter out product variations - // TODO: Add a query arg to activate this. Default should be to keep variation meta for back-compat - if ( $product ) { - $order_item_name = $data['name']; - $data['meta_data'] = array_filter( $data['meta_data'], function( $meta ) use ( $product, $order_item_name ) { - $meta->key = rawurldecode( (string) $meta->key ); - $meta->value = rawurldecode( (string) $meta->value ); - $attribute_key = str_replace( 'attribute_', '', $meta->key ); - $display_key = wc_attribute_label( $attribute_key, $product ); - $display_value = wp_kses_post( $meta->value ); + // Filter out product variations. + if ( $product && 'true' === $this->request['order_item_display_meta'] ) { + $order_item_name = $data['name']; + $data['meta_data'] = array_filter( + $data['meta_data'], + function( $meta ) use ( $product, $order_item_name ) { + $meta->key = rawurldecode( (string) $meta->key ); + $meta->value = rawurldecode( (string) $meta->value ); + $attribute_key = str_replace( 'attribute_', '', $meta->key ); + $display_key = wc_attribute_label( $attribute_key, $product ); + $display_value = wp_kses_post( $meta->value ); - // Skip items with values already in the product details area of the product name. - if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { - return false; + // Skip items with values already in the product details area of the product name. + if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { + return false; + } + + return true; } - - return true; - } ); + ); } $data['meta_data'] = array_map( From 31bcda40b23ac2ee1d8c295f180f7312228f2de5 Mon Sep 17 00:00:00 2001 From: Josh Betz Date: Thu, 17 Mar 2022 14:49:41 -0500 Subject: [PATCH 008/206] Check $product is set This variable is set conditionally, so we need to explicitly check if it is set rather than just checking if it's truthy. --- .../Controllers/Version2/class-wc-rest-orders-v2-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index e3374f2b6f9..ebbaa118c0e 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -212,7 +212,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { $formatted_meta_data = $item->get_all_formatted_meta_data( null ); // Filter out product variations. - if ( $product && 'true' === $this->request['order_item_display_meta'] ) { + if ( isset( $product ) && 'true' === $this->request['order_item_display_meta'] ) { $order_item_name = $data['name']; $data['meta_data'] = array_filter( $data['meta_data'], From 17b18ac68f850bbcb573b0d72e4ec3b9c5c0502e Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 22 Mar 2022 14:26:41 +0200 Subject: [PATCH 009/206] adding woocommerce_download_parse_file_path filter --- plugins/woocommerce/includes/class-wc-download-handler.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index 97f0961fed5..be4cd66fb21 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -276,9 +276,10 @@ class WC_Download_Handler { // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { + $file_path = (is_ssl() ? 'https:' : 'http:') . $file_path; return array( 'remote_file' => true, - 'file_path' => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path, + 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, true ), ); } @@ -299,7 +300,7 @@ class WC_Download_Handler { return array( 'remote_file' => $remote_file, - 'file_path' => $file_path, + 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ) ); } From 867f26da8b3c01198188597fb349356fafdfe972 Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 22 Mar 2022 14:42:08 +0200 Subject: [PATCH 010/206] woocommerce_download_parse_remote_file_path filter --- plugins/woocommerce/includes/class-wc-download-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index be4cd66fb21..2783c38ed65 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -279,7 +279,7 @@ class WC_Download_Handler { $file_path = (is_ssl() ? 'https:' : 'http:') . $file_path; return array( 'remote_file' => true, - 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, true ), + 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path, true ), ); } From 4c97d8e7573c979194f008db44ca8ebb0000d27e Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 22 Mar 2022 14:45:42 +0200 Subject: [PATCH 011/206] Update class-wc-download-handler.php --- plugins/woocommerce/includes/class-wc-download-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index 2783c38ed65..87d2a2214af 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -279,7 +279,7 @@ class WC_Download_Handler { $file_path = (is_ssl() ? 'https:' : 'http:') . $file_path; return array( 'remote_file' => true, - 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path, true ), + 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path ), ); } From 9d9b7464d70014bed7e56c283adbe515a368cb4e Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 22 Mar 2022 14:46:14 +0200 Subject: [PATCH 012/206] Update class-wc-download-handler.php --- plugins/woocommerce/includes/class-wc-download-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index 87d2a2214af..789bc19ef97 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -276,7 +276,7 @@ class WC_Download_Handler { // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { - $file_path = (is_ssl() ? 'https:' : 'http:') . $file_path; + $file_path = ( is_ssl() ? 'https:' : 'http:' ) . $file_path; return array( 'remote_file' => true, 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path ), From 939d429389c5a4d07627149d8099be72cb7e813b Mon Sep 17 00:00:00 2001 From: Josh Betz Date: Wed, 23 Mar 2022 11:09:50 -0500 Subject: [PATCH 013/206] Simplify product variation filtering Fixes an issue where we were conditionally mutating the meta key and value. This was originally copy/pasted from somewhere else. In this context, we don't need all of these values, so the simpler approach is good. Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- .../Version2/class-wc-rest-orders-v2-controller.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index ebbaa118c0e..421a55e0c11 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -217,11 +217,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { $data['meta_data'] = array_filter( $data['meta_data'], function( $meta ) use ( $product, $order_item_name ) { - $meta->key = rawurldecode( (string) $meta->key ); - $meta->value = rawurldecode( (string) $meta->value ); - $attribute_key = str_replace( 'attribute_', '', $meta->key ); - $display_key = wc_attribute_label( $attribute_key, $product ); - $display_value = wp_kses_post( $meta->value ); + $display_value = wp_kses_post( rawurldecode( (string) $meta->value ) ); // Skip items with values already in the product details area of the product name. if ( $product && $product->is_type( 'variation' ) && wc_is_attribute_in_product_name( $display_value, $order_item_name ) ) { From 43e36a945dd2fbc688e1cd1f53754938feec26bb Mon Sep 17 00:00:00 2001 From: Kamil Date: Wed, 30 Mar 2022 22:04:56 +0200 Subject: [PATCH 014/206] change `wp_safe_redirect` location from user dashboard to `edit-account` endpoint --- plugins/woocommerce/includes/class-wc-form-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-form-handler.php b/plugins/woocommerce/includes/class-wc-form-handler.php index b182f6a6de0..3ec4872d871 100644 --- a/plugins/woocommerce/includes/class-wc-form-handler.php +++ b/plugins/woocommerce/includes/class-wc-form-handler.php @@ -347,7 +347,7 @@ class WC_Form_Handler { do_action( 'woocommerce_save_account_details', $user->ID ); - wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + wp_safe_redirect( wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ) ); exit; } } From b903b84edb96fca1740d6a9bbd545a8c5c6a4f98 Mon Sep 17 00:00:00 2001 From: AlexTech01 <72891399+AlexTech01@users.noreply.github.com> Date: Sun, 3 Apr 2022 08:54:45 -0500 Subject: [PATCH 015/206] Change is_file_valid_csv to wc_is_file_valid_csv This pull request replaces the class-specific is_file_valid_csv() function with the global wc_is_file_valid_csv() function as requested by the todo that was previously above the is_file_valid_csv() function on the previous line 89. --- .../class-wc-product-csv-importer-controller.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index 848b36ad77b..97acf002e73 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -86,7 +86,6 @@ class WC_Product_CSV_Importer_Controller { /** * Check whether a file is a valid CSV file. * - * @todo Replace this method with wc_is_file_valid_csv() function. * @param string $file File path. * @param bool $check_path Whether to also check the file is located in a valid location (Default: true). * @return bool @@ -98,17 +97,7 @@ class WC_Product_CSV_Importer_Controller { * @param bool $check_import_file_path If the import file path should be checked. * @param string $file Path of the file to be checked. */ - if ( $check_path && apply_filters( 'woocommerce_product_csv_importer_check_import_file_path', true, $file ) && false !== stripos( $file, '://' ) ) { - return false; - } - - $valid_filetypes = self::get_valid_csv_filetypes(); - $filetype = wp_check_filetype( $file, $valid_filetypes ); - if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { - return true; - } - - return false; + return wc_is_file_valid_csv($file, $check_path); } /** From 885516f30213ad17b27890148e2b3be71f44bc9e Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 11:54:34 -0300 Subject: [PATCH 016/206] Slight improvements for loading of the task list --- .../client/tasks/fills/purchase.js | 43 +++++++++++-------- .../woocommerce-admin/client/tasks/tasks.tsx | 42 ++++-------------- .../client/two-column-tasks/index.js | 22 +--------- .../Features/OnboardingTasks/TaskList.php | 10 +++++ 4 files changed, 45 insertions(+), 72 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/purchase.js b/plugins/woocommerce-admin/client/tasks/fills/purchase.js index 31a894c9158..72ae6efcd48 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/purchase.js +++ b/plugins/woocommerce-admin/client/tasks/fills/purchase.js @@ -15,7 +15,7 @@ import { ONBOARDING_STORE_NAME, PLUGINS_STORE_NAME } from '@woocommerce/data'; import CartModal from '../../dashboard/components/cart-modal'; import { getCategorizedOnboardingProducts } from '../../dashboard/utils'; -const PurchaseTaskItem = () => { +const PurchaseTaskItem = ( { defaultTaskItem } ) => { const [ cartModalOpen, setCartModalOpen ] = useState( false ); const { installedPlugins, productTypes, profileItems } = useSelect( @@ -47,25 +47,32 @@ const PurchaseTaskItem = () => { installedPlugins ); const { remainingProducts } = groupedProducts; + const DefaultTaskItem = defaultTaskItem; + return ( + <> + { + if ( remainingProducts.length ) { + toggleCartModal(); + } + } } + /> + { cartModalOpen && ( + toggleCartModal() } + onClickPurchaseLater={ () => toggleCartModal() } + /> + ) } + + ); +}; + +const PurchaseTaskItemFill = () => { return ( - { ( { defaultTaskItem: DefaultTaskItem } ) => ( - <> - { - if ( remainingProducts.length ) { - toggleCartModal(); - } - } } - /> - { cartModalOpen && ( - toggleCartModal() } - onClickPurchaseLater={ () => toggleCartModal() } - /> - ) } - + { ( { defaultTaskItem } ) => ( + ) } ); @@ -73,5 +80,5 @@ const PurchaseTaskItem = () => { registerPlugin( 'woocommerce-admin-task-purchase', { scope: 'woocommerce-tasks', - render: PurchaseTaskItem, + render: PurchaseTaskItemFill, } ); diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 8e0e61be921..118597fcfa5 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -18,26 +18,15 @@ import { Task } from './task'; import { TasksPlaceholder } from './placeholder'; import './tasks.scss'; import { TaskListProps } from './task-list'; +import { TaskList } from './task-list'; +import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; import '../two-column-tasks/style.scss'; export type TasksProps = { query: { task?: string }; }; -const TaskList = lazy( - () => import( /* webpackChunkName: "task-list" */ './task-list' ) -); - -const TwoColumnTaskList = lazy( - () => - import( - /* webpackChunkName: "two-column-task-list" */ '../two-column-tasks/task-list' - ) -); - -function getTaskListComponent( - taskListId: string -): React.LazyExoticComponent< React.FC< TaskListProps > > { +function getTaskListComponent( taskListId: string ): React.FC< TaskListProps > { switch ( taskListId ) { case 'setup_experiment_1': return TwoColumnTaskList; @@ -56,9 +45,9 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { const { isResolving, taskLists } = useSelect( ( select ) => { return { - isResolving: select( ONBOARDING_STORE_NAME ).isResolving( - 'getTaskLists' - ), + isResolving: ! select( + ONBOARDING_STORE_NAME + ).hasFinishedResolution( 'getTaskLists' ), taskLists: select( ONBOARDING_STORE_NAME ).getTaskLists(), }; } ); @@ -131,17 +120,7 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { : ! id.endsWith( 'two_column' ) ) .map( ( taskList ) => { - const { - id, - eventPrefix, - isComplete, - isHidden, - isVisible, - isToggleable, - title, - tasks, - displayProgressHeader, - } = taskList; + const { id, isHidden, isVisible, isToggleable } = taskList; if ( ! isVisible ) { return null; @@ -153,18 +132,13 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { { isToggleable && ( diff --git a/plugins/woocommerce-admin/client/two-column-tasks/index.js b/plugins/woocommerce-admin/client/two-column-tasks/index.js index 11288fe0d30..4a3b315235c 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/index.js +++ b/plugins/woocommerce-admin/client/two-column-tasks/index.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; +import { ONBOARDING_STORE_NAME } from '@woocommerce/data'; /** * Internal dependencies @@ -14,25 +14,7 @@ import TaskList from './task-list'; import TaskListPlaceholder from './placeholder'; import { Task } from '../tasks/task'; -const taskDashboardSelect = ( select ) => { - const { getOption, hasFinishedResolution } = select( OPTIONS_STORE_NAME ); - - return { - keepCompletedTaskList: getOption( - 'woocommerce_task_list_keep_completed' - ), - isResolving: ! hasFinishedResolution( 'getOption', [ - 'woocommerce_task_list_keep_completed', - ] ), - }; -}; - const TaskDashboard = ( { query, twoColumns } ) => { - const { - keepCompletedTaskList, - isResolving: isResolvingOptions, - } = useSelect( taskDashboardSelect ); - const { task } = query; const { isResolving, taskLists } = useSelect( ( select ) => { @@ -104,7 +86,7 @@ const TaskDashboard = ( { query, twoColumns } ) => { id={ taskList.id } eventName="tasklist" twoColumns={ twoColumns } - keepCompletedTaskList={ keepCompletedTaskList } + keepCompletedTaskList={ taskList.keepCompletedTaskList } dismissedTasks={ dismissedTasks || [] } isComplete={ isTaskListComplete } query={ query } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index ddfa282d70e..0cbc9b73465 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -329,6 +329,15 @@ class TaskList { return $this->get_list_id() . '_tasklist_' . $event_name; } + /** + * Returns option to keep completed task list. + * + * @return string + */ + public function get_keep_completed_task_list() { + return get_option( 'woocommerce_task_list_keep_completed', 'no' ); + } + /** * Remove reminder bar four weeks after store creation. */ @@ -364,6 +373,7 @@ class TaskList { ), 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, + 'keepCompletedTaskList' => $this->get_keep_completed_task_list(), ); } } From b68a835d3a1a1e594625aee3e9253864f5a5f8cf Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 13:07:25 -0300 Subject: [PATCH 017/206] Add displayed task lists to admin settings data --- .../Admin/Features/OnboardingTasks/TaskLists.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 436adcff3ff..8363250cd36 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -70,6 +70,7 @@ class TaskLists { self::init_default_lists(); add_action( 'admin_init', array( __CLASS__, 'set_active_task' ), 5 ); add_action( 'init', array( __CLASS__, 'init_tasks' ) ); + add_filter( 'woocommerce_admin_shared_settings', array( __CLASS__, 'task_list_preloaded_settings' ), 20 ); } /** @@ -321,7 +322,7 @@ class TaskLists { return array_filter( self::get_lists(), function ( $task_list ) { - return ! $task_list->is_hidden(); + return $task_list->is_visible(); } ); } @@ -373,4 +374,17 @@ class TaskLists { return null; } + + /** + * Add visible list ids to component settings. + * + * @param array $settings Component settings. + * + * @return array + */ + public static function task_list_preloaded_settings( $settings ) { + $settings['visibleTaskListIds'] = array_keys( self::get_visible() ); + + return $settings; + } } From 4d6aab73df4954e41d284277475d0a1dc97cc3e2 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 15:38:27 -0300 Subject: [PATCH 018/206] Add placeholder for task list experiment one --- .../client/homescreen/layout.js | 3 ++- .../woocommerce-admin/client/tasks/tasks.tsx | 26 ++++++++++++++++--- .../{placeholder.js => placeholder.tsx} | 10 ++++++- 3 files changed, 33 insertions(+), 6 deletions(-) rename plugins/woocommerce-admin/client/two-column-tasks/{placeholder.js => placeholder.tsx} (87%) diff --git a/plugins/woocommerce-admin/client/homescreen/layout.js b/plugins/woocommerce-admin/client/homescreen/layout.js index 68521a58958..34fd8c4cc32 100644 --- a/plugins/woocommerce-admin/client/homescreen/layout.js +++ b/plugins/woocommerce-admin/client/homescreen/layout.js @@ -42,6 +42,7 @@ import { WelcomeModal } from './welcome-modal'; import { useHeadercardExperimentHook } from './hooks/use-headercard-experiment-hook'; import './style.scss'; import '../dashboard/style.scss'; +import { getAdminSetting } from '~/utils/admin-settings'; const Tasks = lazy( () => import( /* webpackChunkName: "tasks" */ '../tasks' ) @@ -337,7 +338,7 @@ export default compose( shouldShowWelcomeModal, shouldShowWelcomeFromCalypsoModal, isTaskListHidden: getTaskList( 'setup' )?.isHidden, - hasTaskList: !! taskLists.find( ( list ) => list.isVisible ), + hasTaskList: getAdminSetting( 'visibleTaskListIds', [] ).length > 0, taskListComplete: getTaskList( 'setup' )?.isComplete, installTimestamp, installTimestampHasResolved, diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 118597fcfa5..5370e426e3d 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { MenuGroup, MenuItem } from '@wordpress/components'; import { check } from '@wordpress/icons'; -import { Fragment, useEffect, lazy, Suspense } from '@wordpress/element'; +import { Fragment, useEffect, Suspense } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; import { useExperiment } from '@woocommerce/explat'; @@ -15,12 +15,14 @@ import { recordEvent } from '@woocommerce/tracks'; */ import { DisplayOption } from '~/activity-panel/display-options'; import { Task } from './task'; -import { TasksPlaceholder } from './placeholder'; +import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; import './tasks.scss'; import { TaskListProps } from './task-list'; import { TaskList } from './task-list'; import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; +import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; +import { getAdminSetting } from '~/utils/admin-settings'; export type TasksProps = { query: { task?: string }; @@ -35,6 +37,17 @@ function getTaskListComponent( taskListId: string ): React.FC< TaskListProps > { } } +function getTaskListPlaceholderComponent( + taskListId: string +): React.FC< TasksPlaceholderProps > { + switch ( taskListId ) { + case 'setup_experiment_1': + return TwoColumnTaskListPlaceholder; + default: + return TasksPlaceholder; + } +} + export const Tasks: React.FC< TasksProps > = ( { query } ) => { const { task } = query; const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME ); @@ -97,8 +110,13 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { return null; } + const taskListIds = getAdminSetting( 'visibleTaskListIds', [] ); + const TaskListPlaceholderComponent = getTaskListPlaceholderComponent( + taskListIds[ 0 ] + ); + if ( isResolving ) { - return ; + return ; } if ( currentTask ) { @@ -110,7 +128,7 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { } if ( isLoadingExperiment ) { - return ; + return ; } return taskLists diff --git a/plugins/woocommerce-admin/client/two-column-tasks/placeholder.js b/plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx similarity index 87% rename from plugins/woocommerce-admin/client/two-column-tasks/placeholder.js rename to plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx index 70358aee2ca..2cbea8720db 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/placeholder.js +++ b/plugins/woocommerce-admin/client/two-column-tasks/placeholder.tsx @@ -8,7 +8,15 @@ import classnames from 'classnames'; */ import './style.scss'; -const TaskListPlaceholder = ( props ) => { +type TasksPlaceholderProps = { + numTasks?: number; + twoColumns?: boolean; + query: { + task?: string; + }; +}; + +const TaskListPlaceholder: React.FC< TasksPlaceholderProps > = ( props ) => { const { numTasks = 5, twoColumns = false } = props; return ( From 217bec894297b9e15188288f97866a9649df8744 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 15:58:50 -0300 Subject: [PATCH 019/206] Fix lint errors --- plugins/woocommerce-admin/client/homescreen/layout.js | 3 +-- plugins/woocommerce-admin/client/tasks/tasks.tsx | 3 +-- plugins/woocommerce-admin/client/two-column-tasks/index.js | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/homescreen/layout.js b/plugins/woocommerce-admin/client/homescreen/layout.js index 34fd8c4cc32..282572d6b7b 100644 --- a/plugins/woocommerce-admin/client/homescreen/layout.js +++ b/plugins/woocommerce-admin/client/homescreen/layout.js @@ -286,8 +286,7 @@ export default compose( const { getOption, hasFinishedResolution } = select( OPTIONS_STORE_NAME ); - const { getTaskList, getTaskLists } = select( ONBOARDING_STORE_NAME ); - const taskLists = getTaskLists(); + const { getTaskList } = select( ONBOARDING_STORE_NAME ); const welcomeFromCalypsoModalDismissed = getOption( WELCOME_FROM_CALYPSO_MODAL_DISMISSED_OPTION_NAME ) !== diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 5370e426e3d..500661cb650 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -17,8 +17,7 @@ import { DisplayOption } from '~/activity-panel/display-options'; import { Task } from './task'; import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; import './tasks.scss'; -import { TaskListProps } from './task-list'; -import { TaskList } from './task-list'; +import { TaskListProps, TaskList } from './task-list'; import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/index.js b/plugins/woocommerce-admin/client/two-column-tasks/index.js index 4a3b315235c..d09c973f864 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/index.js +++ b/plugins/woocommerce-admin/client/two-column-tasks/index.js @@ -54,7 +54,7 @@ const TaskDashboard = ( { query, twoColumns } ) => { return null; } - if ( isResolving || isResolvingOptions || ! taskLists[ 0 ] ) { + if ( isResolving || ! taskLists[ 0 ] ) { return ; } From e4803ef40fb1e9afec02a91e3b0b267d8c4472f1 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 16:43:15 -0300 Subject: [PATCH 020/206] Fix tests --- .../woocommerce-admin/client/tasks/tasks.tsx | 21 ++++++++----------- .../client/tasks/test/tasks.test.tsx | 11 ++++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 500661cb650..6d450e63a43 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { MenuGroup, MenuItem } from '@wordpress/components'; import { check } from '@wordpress/icons'; -import { Fragment, useEffect, Suspense } from '@wordpress/element'; +import { Fragment, useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data'; import { useExperiment } from '@woocommerce/explat'; @@ -147,17 +147,14 @@ export const Tasks: React.FC< TasksProps > = ( { query } ) => { return ( - - - + { isToggleable && ( { jest.mock( '@woocommerce/explat' ); jest.mock( '@woocommerce/tracks' ); -jest.mock( '../task-list', () => ( { id } ) =>
task-list:{ id }
); -jest.mock( '../../two-column-tasks/task-list', () => ( { id } ) => ( -
two-column-list:{ id }
-) ); +jest.mock( '../task-list', () => ( { + TaskList: ( { id } ) =>
task-list:{ id }
, +} ) ); + +jest.mock( '../../two-column-tasks/task-list', () => ( { + TaskList: ( { id } ) =>
two-column-list:{ id }
, +} ) ); jest.mock( '../task', () => ( { Task: ( { query } ) =>
task:{ query.task }
, From fd4a4a5604de63d835fb2bd71cc2cdf388042ab8 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 31 Mar 2022 12:23:26 +0800 Subject: [PATCH 021/206] Add is_offline flag and split payment sections --- packages/js/data/src/plugins/types.ts | 1 + .../fills/PaymentGatewaySuggestions/index.js | 24 +++++++++++++++---- .../PaymentGatewaySuggestions/test/index.js | 2 ++ .../DefaultPaymentGateways.php | 2 ++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/js/data/src/plugins/types.ts b/packages/js/data/src/plugins/types.ts index 0988f81972a..54a0299ba39 100644 --- a/packages/js/data/src/plugins/types.ts +++ b/packages/js/data/src/plugins/types.ts @@ -39,6 +39,7 @@ export type Plugin = { recommendation_priority?: number; is_visible?: boolean; is_local_partner?: boolean; + is_offline?: boolean; }; type PaypalOnboardingState = 'unknown' | 'start' | 'progressive' | 'onboarded'; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index a908df8304f..a74092fb4a5 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -179,7 +179,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { return gateway; }, [ isResolving, query, paymentGateways ] ); - const [ wcPayGateway, enabledGateways, additionalGateways ] = useMemo( + const [ wcPayGateway, enabledGateways, offlineGateways, additionalGateways ] = useMemo( () => Array.from( paymentGateways.values() ) .sort( ( a, b ) => { @@ -196,7 +196,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { } ) .reduce( ( all, gateway ) => { - const [ wcPay, enabled, additional ] = all; + const [ wcPay, enabled, offline, additional ] = all; // WCPay is handled separately when not installed and configured if ( @@ -207,13 +207,15 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { wcPay.push( gateway ); } else if ( gateway.enabled ) { enabled.push( gateway ); + } else if ( gateway.is_offline ) { + offline.push( gateway ); } else { additional.push( gateway ); } return all; }, - [ [], [], [] ] + [ [], [], [], [] ] ), [ paymentGateways ] ); @@ -250,14 +252,26 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { { !! additionalGateways.length && ( ) } + + { !! offlineGateways.length && ( + + ) } ); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index eae438fe47b..386dd514510 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -49,6 +49,7 @@ const paymentGatewaySuggestions = [ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/cod.svg', is_visible: true, + is_offline: true, }, { id: 'bacs', @@ -57,6 +58,7 @@ const paymentGatewaySuggestions = [ image: 'http://localhost:8888/wp-content/plugins/woocommerce-admin/images/onboarding/bacs.svg', is_visible: true, + is_offline: true, }, { id: 'woocommerce_payments:non-us', diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php index 1b1a76e74e2..64ef1e33512 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php @@ -198,6 +198,7 @@ class DefaultPaymentGateways { 'is_visible' => array( self::get_rules_for_cbd( false ), ), + 'is_offline' => true, ), array( 'id' => 'bacs', @@ -207,6 +208,7 @@ class DefaultPaymentGateways { 'is_visible' => array( self::get_rules_for_cbd( false ), ), + 'is_offline' => true, ), array( 'id' => 'woocommerce_payments', From fb89df5f2c1594f2810986a9587da308e334a354 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 31 Mar 2022 12:26:22 +0800 Subject: [PATCH 022/206] Rename set up button to get started --- .../fills/PaymentGatewaySuggestions/components/Action.js | 2 +- .../PaymentGatewaySuggestions/components/List/test/list.js | 4 ++-- .../PaymentGatewaySuggestions/components/Setup/Configure.js | 2 +- .../components/Setup/test/configure.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js index e16ff3644b1..90c0486ecbc 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js @@ -24,7 +24,7 @@ export const Action = ( { markConfigured, onSetUp = () => {}, onSetupCallback, - setupButtonText = __( 'Set up', 'woocommerce' ), + setupButtonText = __( 'Get started', 'woocommerce-admin' ), } ) => { const [ isBusy, setIsBusy ] = useState( false ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js index 8e7d4caca25..6bd6e4e07e0 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/test/list.js @@ -49,7 +49,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { expect( queryByRole( 'button' ) ).toHaveTextContent( 'Enable' ); } ); - it( 'should display the "Set up" button when setup is required', () => { + it( 'should display the "Get started" button when setup is required', () => { const props = { ...defaultProps, paymentGateways: [ @@ -63,7 +63,7 @@ describe( 'PaymentGatewaySuggestions > List', () => { const { queryByRole } = render( ); - expect( queryByRole( 'button' ) ).toHaveTextContent( 'Set up' ); + expect( queryByRole( 'button' ) ).toHaveTextContent( 'Get started' ); } ); it( 'should display the SetupRequired component when appropriate', () => { diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js index 528d62f63a3..5b5e45b5f92 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js @@ -156,7 +156,7 @@ export const Configure = ( { markConfigured, paymentGateway } ) => {

) } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js index daacf3cc803..81d63b0930e 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/test/configure.js @@ -81,7 +81,7 @@ describe( 'Configure', () => { const { container } = render( ); const button = container.querySelector( 'a' ); - expect( button.textContent ).toBe( 'Set up' ); + expect( button.textContent ).toBe( 'Get started' ); expect( button.href ).toBe( mockGateway.settingsUrl ); } ); } ); From a3bd1e5109d7fc95a57883ce3334c7eb7bbb0ad2 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 31 Mar 2022 13:57:38 +0800 Subject: [PATCH 023/206] Add see more button --- .../components/List/List.js | 8 ++++++- .../fills/PaymentGatewaySuggestions/index.js | 24 +++++++++++++++++-- .../payment-gateway-suggestions.scss | 9 +++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js index 9375f94938a..924ae456415 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Card, CardHeader } from '@wordpress/components'; +import { Card, CardHeader, CardFooter } from '@wordpress/components'; /** * Internal dependencies @@ -15,6 +15,7 @@ export const List = ( { markConfigured, recommendation, paymentGateways, + footerLink, } ) => { return ( @@ -30,6 +31,11 @@ export const List = ( { /> ); } ) } + { footerLink ? ( + { footerLink } + ) : ( + '' + ) } ); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index a74092fb4a5..1d2f5160e71 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -13,6 +13,8 @@ import { useMemo, useCallback, useEffect } from '@wordpress/element'; import { registerPlugin } from '@wordpress/plugins'; import { WooOnboardingTask } from '@woocommerce/onboarding'; import { getNewPath } from '@woocommerce/navigation'; +import { Button } from '@wordpress/components'; +import ExternalIcon from 'gridicons/dist/external'; /** * Internal dependencies @@ -24,6 +26,9 @@ import { getPluginSlug } from '~/utils'; import './plugins/Bacs'; import './payment-gateway-suggestions.scss'; +const SEE_MORE_LINK = + 'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations'; + const comparePaymentGatewaysByPriority = ( a, b ) => a.recommendation_priority - b.recommendation_priority; @@ -179,7 +184,12 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { return gateway; }, [ isResolving, query, paymentGateways ] ); - const [ wcPayGateway, enabledGateways, offlineGateways, additionalGateways ] = useMemo( + const [ + wcPayGateway, + enabledGateways, + offlineGateways, + additionalGateways, + ] = useMemo( () => Array.from( paymentGateways.values() ) .sort( ( a, b ) => { @@ -258,7 +268,17 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { recommendation={ recommendation } paymentGateways={ additionalGateways } markConfigured={ markConfigured } - /> + footerLink={ + + } + >
) } { !! offlineGateways.length && ( diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss index 0716dc3d3fa..8e408b15e20 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss @@ -22,6 +22,15 @@ margin: 0; } + + .components-card__footer { + a.components-button { + .gridicon { + margin-left: 4px; + } + } + } + .woocommerce-task-payment__recommended-pill { border: 1px solid $studio-gray-5; border-radius: 28px; From 0f554436da200fce975928e2ebfed714ddb80d1b Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 4 Apr 2022 18:12:20 +0800 Subject: [PATCH 024/206] Broke wcpay task to different sections, added toggle component, added other payment methods toggle --- .../components/List/List.js | 2 +- .../components/Toggle/Toggle.js | 41 +++++++ .../components/Toggle/Toggle.scss | 9 ++ .../components/Toggle/index.js | 1 + .../fills/PaymentGatewaySuggestions/index.js | 100 ++++++++++-------- 5 files changed, 108 insertions(+), 45 deletions(-) create mode 100644 plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js create mode 100644 plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss create mode 100644 plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js index 924ae456415..d9442b284f7 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js @@ -19,7 +19,7 @@ export const List = ( { } ) => { return ( - { heading } + { heading ? { heading } : null } { paymentGateways.map( ( paymentGateway ) => { const { id } = paymentGateway; return ( diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js new file mode 100644 index 00000000000..d87e6fa6ff9 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import ChevronUpIcon from 'gridicons/dist/chevron-up'; +import ChevronDownIcon from 'gridicons/dist/chevron-down'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './Toggle.scss'; + +export const Toggle = ( { children, heading } ) => { + const [ isOpen, setIsOpen ] = useState( false ); + const onToggle = () => { + setIsOpen( ! isOpen ); + }; + + return ( +
+ + { isOpen ? children : null } +
+ ); +}; + +// export default Toggle; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss new file mode 100644 index 00000000000..b1605309872 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.scss @@ -0,0 +1,9 @@ +.woocommerce-task-payments { + .toggle-button { + margin: $gap-small 0; + + .gridicon { + margin-left: 4px; + } + } +} diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js new file mode 100644 index 00000000000..87fdc434811 --- /dev/null +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/index.js @@ -0,0 +1 @@ +export { Toggle } from './Toggle'; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index 1d2f5160e71..99d9f2effea 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -21,6 +21,7 @@ import ExternalIcon from 'gridicons/dist/external'; */ import { List, Placeholder as ListPlaceholder } from './components/List'; import { Setup, Placeholder as SetupPlaceholder } from './components/Setup'; +import { Toggle } from './components/Toggle/Toggle'; import { WCPaySuggestion } from './components/WCPay'; import { getPluginSlug } from '~/utils'; import './plugins/Bacs'; @@ -230,6 +231,8 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { [ paymentGateways ] ); + const isEligibleWCPay = !! wcPayGateway.length; + if ( query.id && ! currentGateway ) { return ; } @@ -243,54 +246,63 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { ); } + const enabledSection = !! enabledGateways.length && ( + + ); + + const additionalSection = !! additionalGateways.length && ( + + { __( 'See more', 'woocommerce' ) } + + + } + > + ); + + const offlineSection = !! offlineGateways.length && ( + + ); + return (
{ ! paymentGateways.size && } - { !! wcPayGateway.length && ( - - ) } - - { !! enabledGateways.length && ( - - ) } - - { !! additionalGateways.length && ( - - { __( 'See more', 'woocommerce-admin' ) } - - - } - > - ) } - - { !! offlineGateways.length && ( - + { isEligibleWCPay ? ( + <> + + + { enabledSection } + { additionalSection } + { offlineSection } + + + ) : ( + <> + { enabledSection } + { additionalSection } + { offlineSection } + ) }
); From 5ff23bf10331accfefb98a9910eb065ec3c8ab65 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 4 Apr 2022 18:38:07 +0800 Subject: [PATCH 025/206] Add tracks and small refactor to toggle --- .../components/Toggle/Toggle.js | 17 +++++++++-------- .../fills/PaymentGatewaySuggestions/index.js | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js index d87e6fa6ff9..d096360b166 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js @@ -11,29 +11,30 @@ import { useState } from '@wordpress/element'; */ import './Toggle.scss'; -export const Toggle = ( { children, heading } ) => { - const [ isOpen, setIsOpen ] = useState( false ); - const onToggle = () => { - setIsOpen( ! isOpen ); +export const Toggle = ( { children, heading, onToggle } ) => { + const [ isShow, setIsShow ] = useState( false ); + const onClick = () => { + onToggle( isShow ); + setIsShow( ! isShow ); }; return (
- { isOpen ? children : null } + { isShow ? children : null }
); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index 99d9f2effea..99d4df681a6 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -161,6 +161,16 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { [ paymentGateways ] ); + const trackSeeMore = () => { + recordEvent( 'tasklist_payment_see_more', {} ); + }; + + const trackToggle = ( isShow ) => { + recordEvent( 'tasklist_payment_show_toggle', { + toggle: isShow ? 'hide' : 'show', + } ); + }; + const recommendation = useMemo( () => Array.from( paymentGateways.values() ) @@ -265,7 +275,12 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { paymentGateways={ additionalGateways } markConfigured={ markConfigured } footerLink={ - @@ -291,6 +306,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { { enabledSection } { additionalSection } From c596222069b4fa9e23da15aa4d516ba968d2777a Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 5 Apr 2022 11:01:03 +0800 Subject: [PATCH 026/206] Refactor tests --- .../PaymentGatewaySuggestions/test/index.js | 102 ++++++++++++++++-- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index 386dd514510..7df91e7a82a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -85,8 +85,12 @@ const paymentGatewaySuggestions = [ }, ]; +const paymentGatewaySuggestionsWithoutWCPay = paymentGatewaySuggestions.filter( + ( p ) => p.title !== 'WooCommerce Payments' +); + describe( 'PaymentGatewaySuggestions', () => { - test( 'should render payment gateway lists', () => { + test( 'should render only WCPay if its suggested', () => { const onComplete = jest.fn(); const query = {}; useSelect.mockImplementation( () => ( { @@ -111,6 +115,38 @@ describe( 'PaymentGatewaySuggestions', () => { ( e ) => e.textContent ); + expect( paymentTitles ).toEqual( [] ); + + expect( + container.getElementsByTagName( 'title' )[ 0 ].textContent + ).toBe( 'WooCommerce Payments' ); + } ); + + test( 'should render all payment gateways if no WCPay', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, + installedPaymentGateways: [], + } ) ); + + const { container } = render( + + ); + + const paymentTitleElements = container.querySelectorAll( + '.woocommerce-task-payment__title' + ); + + const paymentTitles = Array.from( paymentTitleElements ).map( + ( e ) => e.textContent + ); + expect( paymentTitles ).toEqual( [ 'Stripe', 'PayPal Payments', @@ -118,10 +154,6 @@ describe( 'PaymentGatewaySuggestions', () => { 'Cash on delivery', 'Direct bank transfer', ] ); - - expect( - container.getElementsByTagName( 'title' )[ 0 ].textContent - ).toBe( 'WooCommerce Payments' ); } ); test( 'should the payment gateway offline options at the bottom', () => { @@ -130,7 +162,7 @@ describe( 'PaymentGatewaySuggestions', () => { useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [], } ) ); @@ -156,7 +188,7 @@ describe( 'PaymentGatewaySuggestions', () => { useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [ { id: 'ppcp-gateway', @@ -186,7 +218,7 @@ describe( 'PaymentGatewaySuggestions', () => { useSelect.mockImplementation( () => ( { isResolving: false, getPaymentGateway: jest.fn(), - paymentGatewaySuggestions, + paymentGatewaySuggestions: paymentGatewaySuggestionsWithoutWCPay, installedPaymentGateways: [ { id: 'ppcp-gateway', @@ -213,4 +245,58 @@ describe( 'PaymentGatewaySuggestions', () => { selected: 'ppcp_gateway', } ); } ); + + test( 'should record event correctly when other payment methods is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions, + installedPaymentGateways: [], + } ) ); + + render( + + ); + + fireEvent.click( screen.getByText( 'Other payment methods' ) ); + + // By default it's hidden, so when toggle it shows. + expect( recordEvent ).toHaveBeenCalledWith( + 'tasklist_payment_show_toggle', + { + toggle: 'show', + } + ); + } ); + + test( 'should record event correctly when see more is clicked', () => { + const onComplete = jest.fn(); + const query = {}; + useSelect.mockImplementation( () => ( { + isResolving: false, + getPaymentGateway: jest.fn(), + paymentGatewaySuggestions, + installedPaymentGateways: [], + } ) ); + + render( + + ); + + fireEvent.click( screen.getByText( 'Other payment methods' ) ); + fireEvent.click( screen.getByText( 'See more' ) ); + + expect( recordEvent ).toHaveBeenCalledWith( + 'tasklist_payment_see_more', + {} + ); + } ); } ); From 1980fac6fd06f03a3074a3306fabe90014b1ef10 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 12:25:18 +0800 Subject: [PATCH 027/206] Revamp WCPay suggestion in payments task --- .../src/components/WCPayCard/WCPayCard.js | 139 +++++++++---- .../src/components/WCPayCard/WCPayCard.scss | 51 ++++- .../onboarding/src/images/wcpay-benefit-1.js | 134 +++++++++++++ .../onboarding/src/images/wcpay-benefit-2.js | 102 ++++++++++ .../onboarding/src/images/wcpay-benefit-3.js | 188 ++++++++++++++++++ .../onboarding/src/images/wcpay-hero-image.js | 140 +++++++++++++ .../components/WCPay/Suggestion.js | 50 ++--- .../payment-gateway-suggestions.scss | 1 - 8 files changed, 735 insertions(+), 70 deletions(-) create mode 100644 packages/js/onboarding/src/images/wcpay-benefit-1.js create mode 100644 packages/js/onboarding/src/images/wcpay-benefit-2.js create mode 100644 packages/js/onboarding/src/images/wcpay-benefit-3.js create mode 100644 packages/js/onboarding/src/images/wcpay-hero-image.js diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js index 88a79a7ad1c..6f4fde45af7 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js @@ -1,63 +1,126 @@ /** * External dependencies */ -import { Card, CardBody, CardHeader, CardFooter } from '@wordpress/components'; +import { Card, CardBody, CardFooter } from '@wordpress/components'; import { Text } from '@woocommerce/experimental'; import { createElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; +import interpolateComponents from '@automattic/interpolate-components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { WCPayAcceptedMethods } from '../WCPayAcceptedMethods'; -import WCPayLogo from '../../images/wcpay-logo'; - -export const WCPayCardHeader = ( { - logoWidth = 196, - logoHeight = 41, - children, -} ) => ( - - - { children } - -); +import WCPayHeroImage from '../../images/wcpay-hero-image'; +import WCPayBenefit1 from '../../images/wcpay-benefit-1'; +import WCPayBenefit2 from '../../images/wcpay-benefit-2'; +import WCPayBenefit3 from '../../images/wcpay-benefit-3'; export const WCPayCardBody = ( { - description, heading, + children, onLinkClick = () => {}, } ) => ( - { heading && { heading } } - - - { description } -
- - { __( 'Learn more', 'woocommerce' ) } - -
- - +
+
+ { heading && ( + + { heading } + + ) } + + { interpolateComponents( { + mixedString: __( + 'By using WooCommerce Payments you agree to be bound by our {{tosLink /}} and acknowledge that you have read our {{privacyLink /}}', + 'woocommerce' + ), + components: { + tosLink: ( + + { __( 'Terms of Service', 'woocommerce' ) } + + ), + privacyLink: ( + + { __( 'Privacy Policy', 'woocommerce' ) } + + ), + }, + } ) } +
+
+
{ children }
+
+
+ +
+
); -export const WCPayCardFooter = ( { children } ) => ( - { children } +export const WCPayCardFooter = () => ( + + + ); export const WCPayCard = ( { children } ) => { - return { children }; + return ( + + { children } + + ); +}; + +export const WCPayBenefitCard = () => { + return ( + + +
+
+ + { __( + 'Offer your customers their preferred way to pay including debit and credit card payments, Apple Pay, Sofort, SEPA, iDeal and many more.', + 'woocommerce' + ) } +
+
+ + { __( + 'Sell to international markets and accept more than 135 currencies with local payment methods.', + 'woocommerce' + ) } +
+
+ + { __( + 'Earn and manage recurring revenue and get automatic deposits into your nominated bank account.', + 'woocommerce' + ) } +
+
+
+
+ ); }; diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index c21e835ff68..21b1407c54c 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -1,6 +1,44 @@ .woocommerce-task-payments .woocommerce-task-payment-wcpay { + + .vstack, .hstack { + display: flex; + &.content-center { + justify-content: center; + } + + &.content-around { + justify-content: space-around; + } + + &.flex-1 { + flex: 1; + } + } + + .vstack { + flex-direction: column; + } + + .hstack { + flex-direction: row; + } + + .wcpay-hero-image { + margin-right: -32px; + margin-bottom: -28px; + } + + .woocommerce-task-payment-wcpay__heading { + letter-spacing: 0.6px; + max-width: 250px; + margin-right: $gap-small; + margin-bottom: $gap; + } + .woocommerce-task-payment-wcpay__description { - font-size: 16px; + font-size: 13px; + max-width: 325px; + margin-right: $gap-small; margin-bottom: $gap-large; } @@ -40,4 +78,15 @@ color: #40464d; } } + + .woocommerce-task-payment-wcpay__benefit { + svg { + margin: 0 auto; + } + max-width: 170px; + text-align: center; + font-size: 15px; + line-height: 24px; + letter-spacing: normal; + } } diff --git a/packages/js/onboarding/src/images/wcpay-benefit-1.js b/packages/js/onboarding/src/images/wcpay-benefit-1.js new file mode 100644 index 00000000000..f2e7ee9497b --- /dev/null +++ b/packages/js/onboarding/src/images/wcpay-benefit-1.js @@ -0,0 +1,134 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default ( { width = 141, height = 148, ...props } ) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/js/onboarding/src/images/wcpay-benefit-2.js b/packages/js/onboarding/src/images/wcpay-benefit-2.js new file mode 100644 index 00000000000..3bcd83d6d99 --- /dev/null +++ b/packages/js/onboarding/src/images/wcpay-benefit-2.js @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default ( { width = 140, height = 148, ...props } ) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/js/onboarding/src/images/wcpay-benefit-3.js b/packages/js/onboarding/src/images/wcpay-benefit-3.js new file mode 100644 index 00000000000..9d29102f3e8 --- /dev/null +++ b/packages/js/onboarding/src/images/wcpay-benefit-3.js @@ -0,0 +1,188 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default ( { width = 157, height = 148, ...props } ) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/js/onboarding/src/images/wcpay-hero-image.js b/packages/js/onboarding/src/images/wcpay-hero-image.js new file mode 100644 index 00000000000..2d70f3d126d --- /dev/null +++ b/packages/js/onboarding/src/images/wcpay-hero-image.js @@ -0,0 +1,140 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default ( { width = 293, height = 275, ...props } ) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js index ee4ad87a861..786ee03c58b 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js @@ -3,15 +3,13 @@ */ import { __ } from '@wordpress/i18n'; import interpolateComponents from '@automattic/interpolate-components'; -import { Link, Pill } from '@woocommerce/components'; +import { Link } from '@woocommerce/components'; import { recordEvent } from '@woocommerce/tracks'; -import { Text } from '@woocommerce/experimental'; import { WCPayCard, - WCPayCardHeader, WCPayCardFooter, WCPayCardBody, - SetupRequired, + WCPayBenefitCard, } from '@woocommerce/onboarding'; import { useDispatch } from '@wordpress/data'; @@ -41,7 +39,6 @@ const TosPrompt = () => export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { const { - description, id, needsSetup, installed, @@ -62,27 +59,14 @@ export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { } return ( - - - { installed && needsSetup ? ( - - ) : ( - { __( 'Recommended', 'woocommerce' ) } - ) } - - - { - recordEvent( 'tasklist_payment_learn_more' ); - } } - /> - - - <> - - - + <> + + { + recordEvent( 'tasklist_payment_learn_more' ); + } } + > { isRecommended={ true } isInstalled={ isInstalled } hasPlugins={ true } - setupButtonText={ __( 'Get started', 'woocommerce' ) } + setupButtonText={ + installed && needsSetup + ? __( 'Finish setup', 'woocommerce' ) + : __( 'Install', 'woocommerce' ) + } onSetupCallback={ onSetupCallback } /> - - - + + + + + ); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss index 8e408b15e20..8298e321dbe 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/payment-gateway-suggestions.scss @@ -22,7 +22,6 @@ margin: 0; } - .components-card__footer { a.components-button { .gridicon { From 1f8988675fc48fc3d5f9076996efd57d2b36657d Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 13:43:08 +0800 Subject: [PATCH 028/206] Some adjustment to card footer --- .../js/onboarding/src/components/WCPayAcceptedMethods.js | 4 ++-- .../js/onboarding/src/components/WCPayCard/WCPayCard.scss | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js index f104c24e4e9..7fc42131c2d 100644 --- a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js +++ b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js @@ -21,8 +21,8 @@ import UnionPay from '../images/cards/unionpay.js'; export const WCPayAcceptedMethods = () => ( <> - - { __( 'Accepted payment methods', 'woocommerce' ) } + + { __( 'Accepted payment methods include:', 'woocommerce' ) }
diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index 21b1407c54c..fd950b5b4f7 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -60,6 +60,14 @@ margin-top: $gap; margin-left: 0; } + + &, h3 { + color: #757575; + } + + svg { + height: 24px; + } } .components-card__body { From 3050cacc39d941544055350a0a50b58b7be99586 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 13:51:26 +0800 Subject: [PATCH 029/206] Minor adjustments --- packages/js/onboarding/src/images/wcpay-benefit-3.js | 2 +- .../client/tasks/fills/PaymentGatewaySuggestions/index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/js/onboarding/src/images/wcpay-benefit-3.js b/packages/js/onboarding/src/images/wcpay-benefit-3.js index 9d29102f3e8..80e16205927 100644 --- a/packages/js/onboarding/src/images/wcpay-benefit-3.js +++ b/packages/js/onboarding/src/images/wcpay-benefit-3.js @@ -7,7 +7,7 @@ export default ( { width = 157, height = 148, ...props } ) => ( { heading={ __( 'Other payment methods', 'woocommerce' ) } onToggle={ trackToggle } > - { enabledSection } { additionalSection } + { enabledSection } { offlineSection } ) : ( <> - { enabledSection } { additionalSection } + { enabledSection } { offlineSection } ) } From ffdf1e6c02381c0446a5878a720da88d0723f07e Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 16:25:08 +0800 Subject: [PATCH 030/206] More adjustments --- .../src/components/WCPayCard/WCPayCard.js | 4 ++-- .../onboarding/src/images/wcpay-benefit-1.js | 2 +- .../onboarding/src/images/wcpay-benefit-2.js | 2 +- .../onboarding/src/images/wcpay-benefit-3.js | 2 +- .../components/WCPay/Suggestion.js | 19 ------------------- 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js index 6f4fde45af7..458fd0669c9 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js @@ -49,7 +49,7 @@ export const WCPayCardBody = ( { target="_blank" type="external" rel="noreferrer" - href="https://woocommerce.com/payments/?utm_medium=product" + href="https://wordpress.com/tos/" onClick={ onLinkClick } > { __( 'Terms of Service', 'woocommerce' ) } @@ -60,7 +60,7 @@ export const WCPayCardBody = ( { target="_blank" type="external" rel="noreferrer" - href="https://woocommerce.com/payments/?utm_medium=product" + href="https://automattic.com/privacy/" onClick={ onLinkClick } > { __( 'Privacy Policy', 'woocommerce' ) } diff --git a/packages/js/onboarding/src/images/wcpay-benefit-1.js b/packages/js/onboarding/src/images/wcpay-benefit-1.js index f2e7ee9497b..e0dc078c0f7 100644 --- a/packages/js/onboarding/src/images/wcpay-benefit-1.js +++ b/packages/js/onboarding/src/images/wcpay-benefit-1.js @@ -7,7 +7,7 @@ export default ( { width = 141, height = 148, ...props } ) => ( ( ( - interpolateComponents( { - mixedString: __( - 'Upon clicking "Get started", you agree to the {{link}}Terms of service{{/link}}. Next we’ll ask you to share a few details about your business to create your account.', - 'woocommerce' - ), - components: { - link: ( - - ), - }, - } ); - export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { const { id, From 16c28e3599b24068dae85868ae465af14ce750be Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 16:41:27 +0800 Subject: [PATCH 031/206] Fix css --- .../src/components/WCPayCard/WCPayCard.js | 5 +- .../src/components/WCPayCard/WCPayCard.scss | 187 +++++++++--------- 2 files changed, 101 insertions(+), 91 deletions(-) diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js index 458fd0669c9..a865588b22f 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js @@ -95,7 +95,10 @@ export const WCPayCard = ( { children } ) => { export const WCPayBenefitCard = () => { return ( - +
diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index fd950b5b4f7..9d375486176 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -1,100 +1,107 @@ -.woocommerce-task-payments .woocommerce-task-payment-wcpay { +.woocommerce-task-payments { + .woocommerce-task-payment-wcpay { + .vstack, + .hstack { + display: flex; + &.content-center { + justify-content: center; + } - .vstack, .hstack { - display: flex; - &.content-center { - justify-content: center; + &.content-around { + justify-content: space-around; + } + + &.flex-1 { + flex: 1; + } } - &.content-around { - justify-content: space-around; + .vstack { + flex-direction: column; } - &.flex-1 { - flex: 1; + .hstack { + flex-direction: row; + } + + .wcpay-hero-image { + margin-right: -32px; + margin-bottom: -28px; + } + + .woocommerce-task-payment-wcpay__heading { + letter-spacing: 0.6px; + max-width: 250px; + margin-right: $gap-small; + margin-bottom: $gap; + } + + .woocommerce-task-payment-wcpay__description { + font-size: 13px; + max-width: 325px; + margin-right: $gap-small; + margin-bottom: $gap-large; + } + + .components-card__header { + margin-bottom: $gap-small; + justify-content: flex-start; + padding: 25px; + + .woocommerce-pill { + margin-left: $gap-small; + } + } + + .components-card__footer { + flex-direction: column; + align-items: flex-start; + + .components-button { + margin-top: $gap; + margin-left: 0; + } + + &, + h3 { + color: #757575; + } + + svg { + height: 24px; + } + } + + .components-card__body { + h2 { + margin: 0 0 20px 0; + } + } + + .woocommerce-task-payment-wcpay__accepted { + display: flex; + margin-top: $gap-small; + flex-wrap: wrap; + gap: $gap-small; + + h3 { + color: #40464d; + } + } + + .woocommerce-task-payment-wcpay__benefit { + svg { + margin: 0 auto; + } + max-width: 170px; + text-align: center; + font-size: 15px; + line-height: 24px; + letter-spacing: normal; } } - .vstack { - flex-direction: column; - } - - .hstack { - flex-direction: row; - } - - .wcpay-hero-image { - margin-right: -32px; - margin-bottom: -28px; - } - - .woocommerce-task-payment-wcpay__heading { - letter-spacing: 0.6px; - max-width: 250px; - margin-right: $gap-small; - margin-bottom: $gap; - } - - .woocommerce-task-payment-wcpay__description { - font-size: 13px; - max-width: 325px; - margin-right: $gap-small; - margin-bottom: $gap-large; - } - - .components-card__header { - margin-bottom: $gap-small; - justify-content: flex-start; - padding: 25px; - - .woocommerce-pill { - margin-left: $gap-small; - } - } - - .components-card__footer { - flex-direction: column; - align-items: flex-start; - - .components-button { - margin-top: $gap; - margin-left: 0; - } - - &, h3 { - color: #757575; - } - - svg { - height: 24px; - } - } - - .components-card__body { - h2 { - margin: 0 0 20px 0; - } - } - - .woocommerce-task-payment-wcpay__accepted { - display: flex; - margin-top: $gap-small; - flex-wrap: wrap; - gap: $gap-small; - - h3 { - color: #40464d; - } - } - - .woocommerce-task-payment-wcpay__benefit { - svg { - margin: 0 auto; - } - max-width: 170px; - text-align: center; - font-size: 15px; - line-height: 24px; - letter-spacing: normal; + .woocommerce-task-payment-wcpay__benefits-card { + margin-bottom: 0; } } From ae44908f33e983b37314423f66f19532441eca4d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 15:41:59 +0800 Subject: [PATCH 032/206] Move .husky to root path Add .husky --- .../woocommerce-admin/.husky/pre-push => .husky/post-merge | 2 +- .husky/post-push | 4 ++++ {plugins/woocommerce-admin/.husky => .husky}/pre-commit | 0 {plugins/woocommerce/bin => bin}/post-merge.sh | 0 {plugins/woocommerce/bin => bin}/pre-push.sh | 0 5 files changed, 5 insertions(+), 1 deletion(-) rename plugins/woocommerce-admin/.husky/pre-push => .husky/post-merge (61%) create mode 100755 .husky/post-push rename {plugins/woocommerce-admin/.husky => .husky}/pre-commit (100%) rename {plugins/woocommerce/bin => bin}/post-merge.sh (100%) rename {plugins/woocommerce/bin => bin}/pre-push.sh (100%) diff --git a/plugins/woocommerce-admin/.husky/pre-push b/.husky/post-merge similarity index 61% rename from plugins/woocommerce-admin/.husky/pre-push rename to .husky/post-merge index db541032602..0b5aa72c9d6 100755 --- a/plugins/woocommerce-admin/.husky/pre-push +++ b/.husky/post-merge @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -node bin/pre-push-hook.js +./bin/post-merge.sh diff --git a/.husky/post-push b/.husky/post-push new file mode 100755 index 00000000000..a8877f5c054 --- /dev/null +++ b/.husky/post-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +./bin/post-push.sh diff --git a/plugins/woocommerce-admin/.husky/pre-commit b/.husky/pre-commit similarity index 100% rename from plugins/woocommerce-admin/.husky/pre-commit rename to .husky/pre-commit diff --git a/plugins/woocommerce/bin/post-merge.sh b/bin/post-merge.sh similarity index 100% rename from plugins/woocommerce/bin/post-merge.sh rename to bin/post-merge.sh diff --git a/plugins/woocommerce/bin/pre-push.sh b/bin/pre-push.sh similarity index 100% rename from plugins/woocommerce/bin/pre-push.sh rename to bin/pre-push.sh From 7c4c98c91d4025aacaac85d7ea06c4b833dad07e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 15:44:51 +0800 Subject: [PATCH 033/206] Fix bin/post-merge.sh & bin/pre-push.sh --- .husky/{post-push => pre-push} | 2 +- bin/post-merge.sh | 4 +++- bin/pre-push.sh | 39 ++++++++++++++++------------------ 3 files changed, 22 insertions(+), 23 deletions(-) rename .husky/{post-push => pre-push} (68%) diff --git a/.husky/post-push b/.husky/pre-push similarity index 68% rename from .husky/post-push rename to .husky/pre-push index a8877f5c054..e52fd7c5635 100755 --- a/.husky/post-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -./bin/post-push.sh +./bin/pre-push.sh diff --git a/bin/post-merge.sh b/bin/post-merge.sh index b7d9ac1bab4..ea9143d568d 100755 --- a/bin/post-merge.sh +++ b/bin/post-merge.sh @@ -9,5 +9,7 @@ runOnChange() { fi } -runOnChange "package-lock.json" "pnpm install" +runOnChange "pnpm-lock.yaml" "pnpm install" runOnChange "composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer install" +runOnChange "plugins/woocommerce/composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer --working-dir=plugins/woocommerce install" +runOnChange "plugins/woocommerce-beta-tester/composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer --working-dir=plugins/woocommerce-beta-tester install" diff --git a/bin/pre-push.sh b/bin/pre-push.sh index 8dc3a6e0f9b..9b2346b136f 100755 --- a/bin/pre-push.sh +++ b/bin/pre-push.sh @@ -1,27 +1,24 @@ #!/bin/sh PROTECTED_BRANCH="trunk" -REMOTE_REF=$(echo "$HUSKY_GIT_STDIN" | cut -d " " -f 3) - -if [ -n "$REMOTE_REF" ]; then - if [ "refs/heads/${PROTECTED_BRANCH}" = "$REMOTE_REF" ]; then - if [ "$TERM" = "dumb" ]; then - >&2 echo "Sorry, you are unable to push to trunk using a GUI client! Please use git CLI." - exit 1 - fi - - printf "%sYou're about to push to trunk, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)" - read -r PROCEED < /dev/tty - echo - - if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then - echo "$(tput setaf 2)Brace yourself! Pushing to the trunk branch...$(tput sgr0)" - echo - exit 0 - fi - - echo "$(tput setaf 2)Push to trunk cancelled!$(tput sgr0)" - echo +CURRENT_BRANCH=$(git branch --show-current) +if [ $PROTECTED_BRANCH = $CURRENT_BRANCH ]; then + if [ "$TERM" = "dumb" ]; then + >&2 echo "Sorry, you are unable to push to $PROTECTED_BRANCH using a GUI client! Please use git CLI." exit 1 fi + + printf "%sYou're about to push to $PROTECTED_BRANCH, is that what you intended? [y/N]: %s" "$(tput setaf 3)" "$(tput sgr0)" + read -r PROCEED < /dev/tty + echo + + if [ "$(echo "${PROCEED:-n}" | tr "[:upper:]" "[:lower:]")" = "y" ]; then + echo "$(tput setaf 2)Brace yourself! Pushing to the $PROTECTED_BRANCH branch...$(tput sgr0)" + echo + exit 0 + fi + + echo "$(tput setaf 2)Push to $PROTECTED_BRANCH cancelled!$(tput sgr0)" + echo + exit 1 fi From a4ece1b7b9c71684379229140d4419be5456d68f Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 15:47:27 +0800 Subject: [PATCH 034/206] Remove husky lint-staged from plugins/* --- plugins/woocommerce-admin/package.json | 2 -- plugins/woocommerce-beta-tester/package.json | 22 -------------- plugins/woocommerce/package.json | 30 +------------------- 3 files changed, 1 insertion(+), 53 deletions(-) diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 50d499eea0c..374bbccfffd 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -241,11 +241,9 @@ "grunt": "^1.4.1", "grunt-checktextdomain": "^1.0.1", "grunt-wp-i18n": "^1.0.3", - "husky": "^7.0.0", "jest": "^27.5.1", "jest-environment-jsdom": "~27.5.0", "jest-environment-node": "^27.5.1", - "lint-staged": "^12.3.5", "md5": "^2.3.0", "merge-config": "^2.0.0", "mini-css-extract-plugin": "^2.6.0", diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index b2bf1ba26da..2c4c3b89233 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -11,8 +11,6 @@ "homepage": "http://github.com/woocommerce/woocommerce-beta-tester", "devDependencies": { "eslint": "5.16.0", - "husky": "1.3.1", - "lint-staged": "8.1.5", "uglify-js": "^3.5.3" }, "assets": { @@ -32,26 +30,6 @@ "node": ">=10.15.0", "npm": ">=6.4.1" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "linters": { - "*.php": [ - "php -d display_errors=1 -l", - "composer run-script phpcs-pre-commit" - ], - "*.js": [ - "eslint --fix", - "git add" - ] - }, - "ignore": [ - "*.min.js" - ] - }, "woorelease": { "svn_reauth": "true", "wp_org_slug": "woocommerce-beta-tester" diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 1f20ec694d6..c6a827e7ef9 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -27,8 +27,7 @@ "test:e2e-dev": "pnpx wc-e2e test:e2e-dev", "test:unit": "./vendor/bin/phpunit -c ./phpunit.xml", "makepot": "composer run-script makepot", - "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install" + "packages:fix:textdomain": "node ./bin/package-update-textdomain.js" }, "devDependencies": { "@babel/cli": "7.12.8", @@ -60,10 +59,8 @@ "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.20.0", "github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master", - "husky": "4.3.0", "istanbul": "1.0.0-alpha.2", "jest": "^25.1.0", - "lint-staged": "9.5.0", "mocha": "7.2.0", "prettier": "npm:wp-prettier@2.0.5", "stylelint": "^13.8.0", @@ -76,31 +73,6 @@ "node": "^16.13.1", "pnpm": "^6.24.2" }, - "husky": { - "hooks": { - "post-merge": "./bin/post-merge.sh", - "pre-commit": "lint-staged", - "pre-push": "./bin/pre-push.sh" - } - }, - "lint-staged": { - "*.php": [ - "php -d display_errors=1 -l", - "composer run-script phpcs-pre-commit" - ], - "*.scss": [ - "stylelint --syntax=scss --fix", - "git add" - ], - "*.js": [ - "eslint --fix", - "git add" - ], - "*.ts": [ - "eslint --fix", - "git add" - ] - }, "browserslist": [ "> 0.1%", "ie 8", From 14a2f9a8d89e92a79bcdbd6f73d4f43937ea4531 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 15:58:25 +0800 Subject: [PATCH 035/206] Remove git:update-hooks from woocommerce/project.json --- plugins/woocommerce/project.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/woocommerce/project.json b/plugins/woocommerce/project.json index 9fbc742dca1..79c76fb2462 100644 --- a/plugins/woocommerce/project.json +++ b/plugins/woocommerce/project.json @@ -142,12 +142,6 @@ "script": "packages:fix:textdomain" } }, - "git-update-hooks": { - "executor": "@nrwl/workspace:run-script", - "options": { - "script": "git:update-hooks" - } - }, "make-collection": { "executor": "@nrwl/workspace:run-script", "options": { From f2dcc1f705b16d43b34a3f5d9b7dce302a2d576c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 16:32:38 +0800 Subject: [PATCH 036/206] Add git:update-hooks to postinstall --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f7f9789ea7..4b93a796730 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "url": "https://github.com/woocommerce/woocommerce/issues" }, "scripts": { - "preinstall": "npx only-allow pnpm" + "preinstall": "npx only-allow pnpm", + "postinstall": "pnpm git:update-hooks", + "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && husky install" }, "devDependencies": { "@automattic/nx-composer": "^0.1.0", From 7e146cf35382ce8a4f3ddbe9827145baecf37cb9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 5 Apr 2022 11:57:07 +0800 Subject: [PATCH 037/206] Remove post-merge.sh --- bin/post-merge.sh | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100755 bin/post-merge.sh diff --git a/bin/post-merge.sh b/bin/post-merge.sh deleted file mode 100755 index ea9143d568d..00000000000 --- a/bin/post-merge.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -changedFiles="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)" - -runOnChange() { - if echo "$changedFiles" | grep -q "$1" - then - eval "$2" - fi -} - -runOnChange "pnpm-lock.yaml" "pnpm install" -runOnChange "composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer install" -runOnChange "plugins/woocommerce/composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer --working-dir=plugins/woocommerce install" -runOnChange "plugins/woocommerce-beta-tester/composer.lock" "SKIP_UPDATE_TEXTDOMAINS=true composer --working-dir=plugins/woocommerce-beta-tester install" From 01edee8d90f494f90e5c18f607adc54eaf09b20f Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 30 Mar 2022 16:39:32 +0800 Subject: [PATCH 038/206] Remove lint-staged.config.js --- .../woocommerce-admin/lint-staged.config.js | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 plugins/woocommerce-admin/lint-staged.config.js diff --git a/plugins/woocommerce-admin/lint-staged.config.js b/plugins/woocommerce-admin/lint-staged.config.js deleted file mode 100644 index 0dc6e3b021a..00000000000 --- a/plugins/woocommerce-admin/lint-staged.config.js +++ /dev/null @@ -1,47 +0,0 @@ -module.exports = { - '*.scss': [ 'pnpm run lint:css-fix' ], - 'client/**/*.(t|j)s?(x)': [ - 'wp-scripts format-js', - 'wp-scripts lint-js', - 'pnpm run test-staged', - ], - 'packages/**/*.(t|j)s?(x)': ( packageFiles ) => { - const globalScripts = [ - `wp-scripts format-js ${ packageFiles.join( ' ' ) }`, - `wp-scripts lint-js ${ packageFiles.join( ' ' ) }`, - ]; - - const filesByPackage = packageFiles.reduce( - ( packages, packageFile ) => { - const packageNameMatch = packageFile.match( - /\/packages\/([a-z0-9\-]+)\// - ); - - if ( ! packageNameMatch ) { - return packages; - } - - const packageName = packageNameMatch[ 1 ]; - - if ( Array.isArray( packages[ packageName ] ) ) { - packages[ packageName ].push( packageFile ); - } else { - packages[ packageName ] = [ packageFile ]; - } - - return packages; - }, - {} - ); - - const workspaceScripts = Object.keys( filesByPackage ).map( - ( packageName ) => - `pnpm --filter @woocommerce/${ packageName } run test-staged -- ${ filesByPackage[ - packageName - ].join( ' ' ) }` - ); - - return globalScripts.concat( workspaceScripts ); - }, - '*.php': [ 'php -d display_errors=1 -l', 'composer run-script phpcs' ], -}; From d7df8ff8ab68f4c0d7e1fb804246990f955782f8 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 5 Apr 2022 11:30:52 +0800 Subject: [PATCH 039/206] Set up husky Update pnpm-lock.yaml --- .husky/post-merge | 3 +- .husky/pre-commit | 2 +- package.json | 1 + pnpm-lock.yaml | 617 +--------------------------------------------- 4 files changed, 16 insertions(+), 607 deletions(-) diff --git a/.husky/post-merge b/.husky/post-merge index 0b5aa72c9d6..48bc4affda4 100755 --- a/.husky/post-merge +++ b/.husky/post-merge @@ -1,4 +1,5 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -./bin/post-merge.sh +pnpm install +pnpm nx affected --target="composer-install" --base=ORIG_HEAD --head=HEAD diff --git a/.husky/pre-commit b/.husky/pre-commit index 5e592735698..aa52ea8822c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -pnpm exec lint-staged +pnpm nx affected:lint --uncommitted diff --git a/package.json b/package.json index 4b93a796730..277da8b5ba9 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@wordpress/prettier-config": "^1.1.1", "chalk": "^4.1.2", "glob": "^7.2.0", + "husky": "^7.0.4", "jest": "^27.3.1", "mkdirp": "^1.0.4", "node-stream-zip": "^1.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3554369126f..0e2932d41c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,7 @@ importers: '@wordpress/prettier-config': ^1.1.1 chalk: ^4.1.2 glob: ^7.2.0 + husky: ^7.0.4 jest: ^27.3.1 lodash: ^4.17.21 mkdirp: ^1.0.4 @@ -48,6 +49,7 @@ importers: '@wordpress/prettier-config': 1.1.1 chalk: 4.1.2 glob: 7.2.0 + husky: 7.0.4 jest: 27.3.1 mkdirp: 1.0.4 node-stream-zip: 1.15.0 @@ -889,9 +891,9 @@ importers: qs: 6.10.3 devDependencies: '@babel/core': 7.17.8 + '@babel/runtime': 7.17.7 '@wordpress/eslint-plugin': 11.0.1_7c040a9b494a33cf8bf9079642892fb1 eslint: 8.12.0 - '@babel/runtime': 7.17.7 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -928,9 +930,9 @@ importers: packages/js/number: specifiers: '@babel/core': ^7.17.5 + '@babel/runtime': ^7.17.2 '@wordpress/eslint-plugin': ^11.0.0 eslint: ^8.12.0 - '@babel/runtime': ^7.17.2 jest: ^27.5.1 jest-cli: ^27.5.1 locutus: ^2.0.16 @@ -941,9 +943,9 @@ importers: locutus: 2.0.16 devDependencies: '@babel/core': 7.17.8 + '@babel/runtime': 7.17.7 '@wordpress/eslint-plugin': 11.0.1_7c040a9b494a33cf8bf9079642892fb1 eslint: 8.12.0 - '@babel/runtime': 7.17.7 jest: 27.5.1 jest-cli: 27.5.1 rimraf: 3.0.2 @@ -1098,10 +1100,8 @@ importers: eslint-config-wpcalypso: 5.0.0 eslint-plugin-jest: 23.20.0 github-contributors-list: https://github.com/woocommerce/github-contributors-list/tarball/master - husky: 4.3.0 istanbul: 1.0.0-alpha.2 jest: ^25.1.0 - lint-staged: 9.5.0 mocha: 7.2.0 prettier: npm:wp-prettier@2.0.5 stylelint: ^13.8.0 @@ -1139,10 +1139,8 @@ importers: eslint-config-wpcalypso: 5.0.0_eslint@6.8.0 eslint-plugin-jest: 23.20.0_eslint@6.8.0+typescript@3.9.7 github-contributors-list: '@github.com/woocommerce/github-contributors-list/tarball/master' - husky: 4.3.0 istanbul: 1.0.0-alpha.2 jest: 25.5.4 - lint-staged: 9.5.0 mocha: 7.2.0 prettier: /wp-prettier/2.0.5 stylelint: 13.13.1 @@ -1292,11 +1290,9 @@ importers: grunt-checktextdomain: ^1.0.1 grunt-wp-i18n: ^1.0.3 history: ^4.10.1 - husky: ^7.0.0 jest: ^27.5.1 jest-environment-jsdom: ~27.5.0 jest-environment-node: ^27.5.1 - lint-staged: ^12.3.5 lodash: ^4.17.21 md5: ^2.3.0 memize: ^1.1.0 @@ -1492,11 +1488,9 @@ importers: grunt: 1.4.1 grunt-checktextdomain: 1.0.1_grunt@1.4.1 grunt-wp-i18n: 1.0.3 - husky: 7.0.4 jest: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 - lint-staged: 12.3.7 md5: 2.3.0 merge-config: 2.0.0 mini-css-extract-plugin: 2.6.0_webpack@5.70.0 @@ -1533,13 +1527,9 @@ importers: plugins/woocommerce-beta-tester: specifiers: eslint: 5.16.0 - husky: 1.3.1 - lint-staged: 8.1.5 uglify-js: ^3.5.3 devDependencies: eslint: 5.16.0 - husky: 1.3.1 - lint-staged: 8.1.5 uglify-js: 3.14.5 plugins/woocommerce/legacy: @@ -3812,7 +3802,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.0 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 dev: true /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.16.12: @@ -3821,7 +3811,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-nullish-coalescing-operator/7.8.3_@babel+core@7.17.8: resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} @@ -3829,7 +3819,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-numeric-separator/7.10.4_@babel+core@7.12.9: resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} @@ -3944,7 +3934,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.0 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 dev: true /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.16.12: @@ -3953,7 +3943,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-optional-chaining/7.8.3_@babel+core@7.17.8: resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} @@ -3961,7 +3951,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.16.7 + '@babel/helper-plugin-utils': 7.14.5 /@babel/plugin-syntax-private-property-in-object/7.14.5_@babel+core@7.16.0: resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} @@ -6198,12 +6188,6 @@ packages: core-js-pure: 3.19.1 regenerator-runtime: 0.13.9 - /@babel/runtime/7.0.0: - resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} - dependencies: - regenerator-runtime: 0.12.1 - dev: true - /@babel/runtime/7.15.4: resolution: {integrity: sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==} engines: {node: '>=6.9.0'} @@ -8808,22 +8792,6 @@ packages: lodash.merge: 4.6.2 postcss: 5.2.18 - /@samverschueren/stream-to-observable/0.3.1_rxjs@6.6.7: - resolution: {integrity: sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==} - engines: {node: '>=6'} - peerDependencies: - rxjs: '*' - zen-observable: '*' - peerDependenciesMeta: - rxjs: - optional: true - zen-observable: - optional: true - dependencies: - any-observable: 0.3.0 - rxjs: 6.6.7 - dev: true - /@sindresorhus/is/0.14.0: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} @@ -15669,11 +15637,6 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - /ansi-styles/6.1.0: - resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} - engines: {node: '>=12'} - dev: true - /ansi-to-html/0.6.15: resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==} engines: {node: '>=8.0.0'} @@ -15690,11 +15653,6 @@ packages: engines: {node: '>=12.13'} dev: false - /any-observable/0.3.0: - resolution: {integrity: sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==} - engines: {node: '>=6'} - dev: true - /anymatch/2.0.0: resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} dependencies: @@ -17979,14 +17937,6 @@ packages: colors: 1.4.0 dev: true - /cli-truncate/0.2.1: - resolution: {integrity: sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=} - engines: {node: '>=0.10.0'} - dependencies: - slice-ansi: 0.0.4 - string-width: 1.0.2 - dev: true - /cli-truncate/2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} engines: {node: '>=8'} @@ -17995,14 +17945,6 @@ packages: string-width: 4.2.3 dev: true - /cli-truncate/3.1.0: - resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - slice-ansi: 5.0.0 - string-width: 5.1.2 - dev: true - /cli-width/2.2.1: resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} dev: true @@ -18318,10 +18260,6 @@ packages: resolution: {integrity: sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=} dev: true - /compare-versions/3.6.0: - resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} - dev: true - /component-emitter/1.3.0: resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} @@ -19306,10 +19244,6 @@ packages: whatwg-url: 8.7.0 dev: true - /date-fns/1.30.1: - resolution: {integrity: sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==} - dev: true - /date-fns/2.28.0: resolution: {integrity: sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==} engines: {node: '>=0.11'} @@ -19388,19 +19322,6 @@ packages: ms: 2.1.2 supports-color: 8.1.1 - /debug/4.3.3_supports-color@9.2.1: - resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - supports-color: 9.2.1 - dev: true - /debuglog/1.0.1: resolution: {integrity: sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=} dev: true @@ -19544,18 +19465,6 @@ packages: is-descriptor: 1.0.2 isobject: 3.0.1 - /del/3.0.0: - resolution: {integrity: sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=} - engines: {node: '>=4'} - dependencies: - globby: 6.1.0 - is-path-cwd: 1.0.0 - is-path-in-cwd: 1.0.1 - p-map: 1.2.0 - pify: 3.0.0 - rimraf: 2.7.1 - dev: true - /del/4.1.1: resolution: {integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==} engines: {node: '>=6'} @@ -19569,20 +19478,6 @@ packages: rimraf: 2.7.1 dev: true - /del/5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - dependencies: - globby: 10.0.1 - graceful-fs: 4.2.9 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: true - /del/6.0.0: resolution: {integrity: sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==} engines: {node: '>=10'} @@ -20015,10 +19910,6 @@ packages: stream-shift: 1.0.1 dev: true - /eastasianwidth/0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - /ecc-jsbn/0.1.2: resolution: {integrity: sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=} dependencies: @@ -20062,11 +19953,6 @@ packages: /electron-to-chromium/1.4.88: resolution: {integrity: sha512-oA7mzccefkvTNi9u7DXmT0LqvhnOiN2BhSrKerta7HeUC1cLoIwtbf2wL+Ah2ozh5KQd3/1njrGrwDBXx6d14Q==} - /elegant-spinner/1.0.1: - resolution: {integrity: sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=} - engines: {node: '>=0.10.0'} - dev: true - /element-resize-detector/1.2.4: resolution: {integrity: sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==} dependencies: @@ -21989,21 +21875,6 @@ packages: signal-exit: 3.0.7 strip-eof: 1.0.0 - /execa/2.1.0: - resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} - engines: {node: ^8.12.0 || >=9.7.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 3.1.0 - onetime: 5.1.2 - p-finally: 2.0.1 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - /execa/3.4.0: resolution: {integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==} engines: {node: ^8.12.0 || >=9.7.0} @@ -22576,13 +22447,6 @@ packages: path-exists: 4.0.0 dev: true - /find-versions/3.2.0: - resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} - engines: {node: '>=6'} - dependencies: - semver-regex: 2.0.0 - dev: true - /find-yarn-workspace-root/2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} dependencies: @@ -22717,11 +22581,6 @@ packages: readable-stream: 2.3.7 dev: true - /fn-name/2.0.1: - resolution: {integrity: sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=} - engines: {node: '>=0.10.0'} - dev: true - /follow-redirects/1.14.5: resolution: {integrity: sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==} engines: {node: '>=4.0'} @@ -23129,17 +22988,6 @@ packages: engines: {node: '>=6'} dev: true - /g-status/2.0.2: - resolution: {integrity: sha512-kQoE9qH+T1AHKgSSD0Hkv98bobE90ILQcXAF4wvGgsr7uFqNvwmh8j+Lq3l0RVt3E3HjSbv2B9biEGcEtpHLCA==} - engines: {node: '>=6'} - dependencies: - arrify: 1.0.1 - matcher: 1.1.1 - simple-git: 1.132.0 - transitivePeerDependencies: - - supports-color - dev: true - /gauge/3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -23206,10 +23054,6 @@ packages: has: 1.0.3 has-symbols: 1.0.2 - /get-own-enumerable-property-symbols/3.0.2: - resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} - dev: true - /get-package-type/0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -24616,42 +24460,6 @@ packages: ms: 2.1.3 dev: true - /husky/1.3.1: - resolution: {integrity: sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==} - engines: {node: '>=6'} - hasBin: true - requiresBuild: true - dependencies: - cosmiconfig: 5.2.1 - execa: 1.0.0 - find-up: 3.0.0 - get-stdin: 6.0.0 - is-ci: 2.0.0 - pkg-dir: 3.0.0 - please-upgrade-node: 3.2.0 - read-pkg: 4.0.1 - run-node: 1.0.0 - slash: 2.0.0 - dev: true - - /husky/4.3.0: - resolution: {integrity: sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA==} - engines: {node: '>=10'} - hasBin: true - requiresBuild: true - dependencies: - chalk: 4.1.2 - ci-info: 2.0.0 - compare-versions: 3.6.0 - cosmiconfig: 7.0.1 - find-versions: 3.2.0 - opencollective-postinstall: 2.0.3 - pkg-dir: 4.2.0 - please-upgrade-node: 3.2.0 - slash: 3.0.0 - which-pm-runs: 1.0.0 - dev: true - /husky/7.0.4: resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==} engines: {node: '>=12'} @@ -24839,11 +24647,6 @@ packages: resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} engines: {node: '>=0.8.19'} - /indent-string/3.2.0: - resolution: {integrity: sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=} - engines: {node: '>=4'} - dev: true - /indent-string/4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -25285,11 +25088,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - /is-fullwidth-code-point/4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - dev: true - /is-function/1.0.2: resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} dev: true @@ -25404,30 +25202,11 @@ packages: resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} dev: true - /is-observable/1.1.0: - resolution: {integrity: sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==} - engines: {node: '>=4'} - dependencies: - symbol-observable: 1.2.0 - dev: true - - /is-path-cwd/1.0.0: - resolution: {integrity: sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=} - engines: {node: '>=0.10.0'} - dev: true - /is-path-cwd/2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} engines: {node: '>=6'} dev: true - /is-path-in-cwd/1.0.1: - resolution: {integrity: sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==} - engines: {node: '>=0.10.0'} - dependencies: - is-path-inside: 1.0.1 - dev: true - /is-path-in-cwd/2.1.0: resolution: {integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==} engines: {node: '>=6'} @@ -25488,10 +25267,6 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true - /is-promise/2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - dev: true - /is-promise/4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} dev: false @@ -25514,11 +25289,6 @@ packages: call-bind: 1.0.2 has-tostringtag: 1.0.0 - /is-regexp/1.0.0: - resolution: {integrity: sha1-/S2INUXEa6xaYz57mgnof6LLUGk=} - engines: {node: '>=0.10.0'} - dev: true - /is-regexp/2.1.0: resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==} engines: {node: '>=6'} @@ -28455,159 +28225,11 @@ packages: uc.micro: 1.0.6 dev: true - /lint-staged/12.3.7: - resolution: {integrity: sha512-/S4D726e2GIsDVWIk1XGvheCaDm1SJRQp8efamZFWJxQMVEbOwSysp7xb49Oo73KYCdy97mIWinhlxcoNqIfIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - dependencies: - cli-truncate: 3.1.0 - colorette: 2.0.16 - commander: 8.3.0 - debug: 4.3.3_supports-color@9.2.1 - execa: 5.1.1 - lilconfig: 2.0.4 - listr2: 4.0.5 - micromatch: 4.0.4 - normalize-path: 3.0.0 - object-inspect: 1.12.0 - pidtree: 0.5.0 - string-argv: 0.3.1 - supports-color: 9.2.1 - yaml: 1.10.2 - transitivePeerDependencies: - - enquirer - dev: true - - /lint-staged/8.1.5: - resolution: {integrity: sha512-e5ZavfnSLcBJE1BTzRTqw6ly8OkqVyO3GL2M6teSmTBYQ/2BuueD5GIt2RPsP31u/vjKdexUyDCxSyK75q4BDA==} - hasBin: true - dependencies: - chalk: 2.4.2 - commander: 2.20.3 - cosmiconfig: 5.2.1 - debug: 3.2.7 - dedent: 0.7.0 - del: 3.0.0 - execa: 1.0.0 - find-parent-dir: 0.3.1 - g-status: 2.0.2 - is-glob: 4.0.3 - is-windows: 1.0.2 - listr: 0.14.3 - listr-update-renderer: 0.5.0_listr@0.14.3 - lodash: 4.17.21 - log-symbols: 2.2.0 - micromatch: 3.1.10 - npm-which: 3.0.1 - p-map: 1.2.0 - path-is-inside: 1.0.2 - pify: 3.0.0 - please-upgrade-node: 3.2.0 - staged-git-files: 1.1.2 - string-argv: 0.0.2 - stringify-object: 3.3.0 - yup: 0.26.10 - transitivePeerDependencies: - - supports-color - - zen-observable - dev: true - - /lint-staged/9.5.0: - resolution: {integrity: sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA==} - hasBin: true - dependencies: - chalk: 2.4.2 - commander: 2.20.3 - cosmiconfig: 5.2.1 - debug: 4.3.3 - dedent: 0.7.0 - del: 5.1.0 - execa: 2.1.0 - listr: 0.14.3 - log-symbols: 3.0.0 - micromatch: 4.0.4 - normalize-path: 3.0.0 - please-upgrade-node: 3.2.0 - string-argv: 0.3.1 - stringify-object: 3.3.0 - transitivePeerDependencies: - - supports-color - - zen-observable - dev: true - /liquid-json/0.3.1: resolution: {integrity: sha1-kVWhgTbYprJhXl8W+aJEira1Duo=} engines: {node: '>=4'} dev: false - /listr-silent-renderer/1.1.1: - resolution: {integrity: sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=} - engines: {node: '>=4'} - dev: true - - /listr-update-renderer/0.5.0_listr@0.14.3: - resolution: {integrity: sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==} - engines: {node: '>=6'} - peerDependencies: - listr: ^0.14.2 - dependencies: - chalk: 1.1.3 - cli-truncate: 0.2.1 - elegant-spinner: 1.0.1 - figures: 1.7.0 - indent-string: 3.2.0 - listr: 0.14.3 - log-symbols: 1.0.2 - log-update: 2.3.0 - strip-ansi: 3.0.1 - dev: true - - /listr-verbose-renderer/0.5.0: - resolution: {integrity: sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==} - engines: {node: '>=4'} - dependencies: - chalk: 2.4.2 - cli-cursor: 2.1.0 - date-fns: 1.30.1 - figures: 2.0.0 - dev: true - - /listr/0.14.3: - resolution: {integrity: sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==} - engines: {node: '>=6'} - dependencies: - '@samverschueren/stream-to-observable': 0.3.1_rxjs@6.6.7 - is-observable: 1.1.0 - is-promise: 2.2.2 - is-stream: 1.1.0 - listr-silent-renderer: 1.1.1 - listr-update-renderer: 0.5.0_listr@0.14.3 - listr-verbose-renderer: 0.5.0 - p-map: 2.1.0 - rxjs: 6.6.7 - transitivePeerDependencies: - - zen-observable - dev: true - - /listr2/4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.16 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.3.0 - rxjs: 7.5.5 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true - /livereload-js/2.4.0: resolution: {integrity: sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==} dev: true @@ -28884,20 +28506,6 @@ packages: /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - /log-symbols/1.0.2: - resolution: {integrity: sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=} - engines: {node: '>=0.10.0'} - dependencies: - chalk: 1.1.3 - dev: true - - /log-symbols/2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - dependencies: - chalk: 2.4.2 - dev: true - /log-symbols/3.0.0: resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} engines: {node: '>=8'} @@ -28913,25 +28521,6 @@ packages: is-unicode-supported: 0.1.0 dev: true - /log-update/2.3.0: - resolution: {integrity: sha1-iDKP19HOeTiykoN0bwsbwSayRwg=} - engines: {node: '>=4'} - dependencies: - ansi-escapes: 3.2.0 - cli-cursor: 2.1.0 - wrap-ansi: 3.0.1 - dev: true - - /log-update/4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - dev: true - /lolex/5.1.2: resolution: {integrity: sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==} dependencies: @@ -29207,13 +28796,6 @@ packages: hasBin: true dev: true - /matcher/1.1.1: - resolution: {integrity: sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==} - engines: {node: '>=4'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - /mathml-tag-names/2.1.3: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} dev: true @@ -30358,14 +29940,6 @@ packages: npm-normalize-package-bin: 1.0.1 dev: true - /npm-path/2.0.4: - resolution: {integrity: sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==} - engines: {node: '>=0.8'} - hasBin: true - dependencies: - which: 1.3.1 - dev: true - /npm-pick-manifest/6.1.1: resolution: {integrity: sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==} dependencies: @@ -30411,29 +29985,12 @@ packages: dependencies: path-key: 2.0.1 - /npm-run-path/3.1.0: - resolution: {integrity: sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - /npm-run-path/4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} dependencies: path-key: 3.1.1 - /npm-which/3.0.1: - resolution: {integrity: sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=} - engines: {node: '>=4.2.0'} - hasBin: true - dependencies: - commander: 2.20.3 - npm-path: 2.0.4 - which: 1.3.1 - dev: true - /npmlog/5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} dependencies: @@ -30914,11 +30471,6 @@ packages: p-limit: 3.1.0 dev: true - /p-map/1.2.0: - resolution: {integrity: sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==} - engines: {node: '>=4'} - dev: true - /p-map/2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -31340,12 +30892,6 @@ packages: hasBin: true dev: true - /pidtree/0.5.0: - resolution: {integrity: sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==} - engines: {node: '>=0.10'} - hasBin: true - dev: true - /pify/2.3.0: resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} engines: {node: '>=0.10.0'} @@ -31405,12 +30951,6 @@ packages: find-up: 5.0.0 dev: true - /please-upgrade-node/3.2.0: - resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} - dependencies: - semver-compare: 1.0.0 - dev: true - /plur/4.0.0: resolution: {integrity: sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==} engines: {node: '>=10'} @@ -32929,10 +32469,6 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 - /property-expr/1.5.1: - resolution: {integrity: sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==} - dev: true - /property-information/5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} dependencies: @@ -34143,15 +33679,6 @@ packages: normalize-package-data: 2.5.0 path-type: 3.0.0 - /read-pkg/4.0.1: - resolution: {integrity: sha1-ljYlN48+HE1IyFhytabsfV0JMjc=} - engines: {node: '>=6'} - dependencies: - normalize-package-data: 2.5.0 - parse-json: 4.0.0 - pify: 3.0.0 - dev: true - /read-pkg/5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} @@ -34391,10 +33918,6 @@ packages: resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} dev: true - /regenerator-runtime/0.12.1: - resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} - dev: true - /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} @@ -34871,10 +34394,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - /rfdc/1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - dev: true - /rgb-regex/1.0.1: resolution: {integrity: sha1-wODWiC3w4jviVKR16O3UGRX+rrE=} @@ -35017,12 +34536,6 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - /run-node/1.0.0: - resolution: {integrity: sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==} - engines: {node: '>=4'} - hasBin: true - dev: true - /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -35061,12 +34574,6 @@ packages: tslib: 2.3.1 dev: true - /rxjs/7.5.5: - resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} - dependencies: - tslib: 2.3.1 - dev: true - /safe-buffer/5.1.1: resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==} dev: true @@ -35322,10 +34829,6 @@ packages: node-forge: 0.10.0 dev: true - /semver-compare/1.0.0: - resolution: {integrity: sha1-De4hahyUGrN+nvsXiPavxf9VN/w=} - dev: true - /semver-diff/2.1.0: resolution: {integrity: sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=} engines: {node: '>=0.10.0'} @@ -35340,11 +34843,6 @@ packages: semver: 6.3.0 dev: true - /semver-regex/2.0.0: - resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} - engines: {node: '>=6'} - dev: true - /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -35572,14 +35070,6 @@ packages: /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - /simple-git/1.132.0: - resolution: {integrity: sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg==} - dependencies: - debug: 4.3.3 - transitivePeerDependencies: - - supports-color - dev: true - /simple-html-tokenizer/0.5.11: resolution: {integrity: sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==} dev: false @@ -35605,11 +35095,6 @@ packages: engines: {node: '>=12'} dev: true - /slice-ansi/0.0.4: - resolution: {integrity: sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=} - engines: {node: '>=0.10.0'} - dev: true - /slice-ansi/2.1.0: resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} engines: {node: '>=6'} @@ -35636,14 +35121,6 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true - /slice-ansi/5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.1.0 - is-fullwidth-code-point: 4.0.0 - dev: true - /smart-buffer/4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -35924,11 +35401,6 @@ packages: resolution: {integrity: sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==} dev: true - /staged-git-files/1.1.2: - resolution: {integrity: sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA==} - hasBin: true - dev: true - /state-toggle/1.0.3: resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} dev: true @@ -35986,16 +35458,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /string-argv/0.0.2: - resolution: {integrity: sha1-2sMECGkMIfPDYwo/86BYd73L1zY=} - engines: {node: '>=0.6.19'} - dev: true - - /string-argv/0.3.1: - resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} - engines: {node: '>=0.6.19'} - dev: true - /string-hash/1.1.3: resolution: {integrity: sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=} dev: true @@ -36060,15 +35522,6 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width/5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.0.1 - dev: true - /string.prototype.matchall/4.0.6: resolution: {integrity: sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==} dependencies: @@ -36133,15 +35586,6 @@ packages: dependencies: safe-buffer: 5.2.1 - /stringify-object/3.3.0: - resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} - engines: {node: '>=4'} - dependencies: - get-own-enumerable-property-symbols: 3.0.2 - is-obj: 1.0.1 - is-regexp: 1.0.0 - dev: true - /strip-ansi/2.0.1: resolution: {integrity: sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=} engines: {node: '>=0.10.0'} @@ -36731,11 +36175,6 @@ packages: dependencies: has-flag: 4.0.0 - /supports-color/9.2.1: - resolution: {integrity: sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==} - engines: {node: '>=12'} - dev: true - /supports-hyperlinks/2.2.0: resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} engines: {node: '>=8'} @@ -36792,11 +36231,6 @@ packages: upper-case: 1.1.3 dev: true - /symbol-observable/1.2.0: - resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} - engines: {node: '>=0.10.0'} - dev: true - /symbol-tree/3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -37084,7 +36518,7 @@ packages: serialize-javascript: 6.0.0 source-map: 0.6.1 terser: 5.10.0_acorn@8.7.0 - webpack: 5.70.0 + webpack: 5.70.0_webpack-cli@3.3.12 transitivePeerDependencies: - acorn dev: true @@ -37425,10 +36859,6 @@ packages: engines: {node: '>=0.6'} dev: true - /toposort/2.0.2: - resolution: {integrity: sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=} - dev: true - /tough-cookie/2.5.0: resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} engines: {node: '>=0.8'} @@ -39384,10 +38814,6 @@ packages: /which-module/2.0.0: resolution: {integrity: sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=} - /which-pm-runs/1.0.0: - resolution: {integrity: sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=} - dev: true - /which-pm/2.0.0: resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} engines: {node: '>=8.15'} @@ -39502,14 +38928,6 @@ packages: strip-ansi: 3.0.1 dev: true - /wrap-ansi/3.0.1: - resolution: {integrity: sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=} - engines: {node: '>=4'} - dependencies: - string-width: 2.1.1 - strip-ansi: 4.0.0 - dev: true - /wrap-ansi/5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -39933,17 +39351,6 @@ packages: wrap-ansi: 2.1.0 dev: true - /yup/0.26.10: - resolution: {integrity: sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw==} - dependencies: - '@babel/runtime': 7.0.0 - fn-name: 2.0.1 - lodash: 4.17.21 - property-expr: 1.5.1 - synchronous-promise: 2.0.15 - toposort: 2.0.2 - dev: true - /zwitch/1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} dev: true From 44b4008849ab1434d92c0fe51752962c0f67cc45 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 6 Apr 2022 16:57:09 +0800 Subject: [PATCH 040/206] Install lint-staged --- package.json | 1 + pnpm-lock.yaml | 152 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 277da8b5ba9..2ff6a3d8460 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "glob": "^7.2.0", "husky": "^7.0.4", "jest": "^27.3.1", + "lint-staged": "^12.3.7", "mkdirp": "^1.0.4", "node-stream-zip": "^1.15.0", "prettier": "npm:wp-prettier@^2.2.1-beta-1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2932d41c3..bb0942ed732 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ importers: glob: ^7.2.0 husky: ^7.0.4 jest: ^27.3.1 + lint-staged: ^12.3.7 lodash: ^4.17.21 mkdirp: ^1.0.4 node-stream-zip: ^1.15.0 @@ -51,6 +52,7 @@ importers: glob: 7.2.0 husky: 7.0.4 jest: 27.3.1 + lint-staged: 12.3.7 mkdirp: 1.0.4 node-stream-zip: 1.15.0 prettier: /wp-prettier/2.2.1-beta-1 @@ -1765,7 +1767,7 @@ packages: '@babel/traverse': 7.16.3 '@babel/types': 7.16.0 convert-source-map: 1.8.0 - debug: 4.3.2 + debug: 4.3.3 gensync: 1.0.0-beta.2 json5: 2.2.0 semver: 6.3.0 @@ -2206,7 +2208,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.2 + debug: 4.3.3 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -2224,7 +2226,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.2 + debug: 4.3.3 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -2242,7 +2244,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.2 + debug: 4.3.3 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -6234,7 +6236,7 @@ packages: '@babel/helper-split-export-declaration': 7.16.0 '@babel/parser': 7.16.4 '@babel/types': 7.16.0 - debug: 4.3.2 + debug: 4.3.3 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -6676,7 +6678,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.2 + debug: 4.3.3 espree: 9.0.0 globals: 13.12.0 ignore: 4.0.6 @@ -6756,7 +6758,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.2 + debug: 4.3.3 minimatch: 3.0.4 transitivePeerDependencies: - supports-color @@ -13273,7 +13275,7 @@ packages: dependencies: '@typescript-eslint/types': 5.4.0 '@typescript-eslint/visitor-keys': 5.4.0 - debug: 4.3.2 + debug: 4.3.3 globby: 11.0.4 is-glob: 4.0.3 semver: 7.3.5 @@ -15637,6 +15639,11 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + /ansi-styles/6.1.0: + resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} + engines: {node: '>=12'} + dev: true + /ansi-to-html/0.6.15: resolution: {integrity: sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==} engines: {node: '>=8.0.0'} @@ -17945,6 +17952,14 @@ packages: string-width: 4.2.3 dev: true + /cli-truncate/3.1.0: + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + slice-ansi: 5.0.0 + string-width: 5.1.2 + dev: true + /cli-width/2.2.1: resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} dev: true @@ -19322,6 +19337,19 @@ packages: ms: 2.1.2 supports-color: 8.1.1 + /debug/4.3.3_supports-color@9.2.2: + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 9.2.2 + dev: true + /debuglog/1.0.1: resolution: {integrity: sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=} dev: true @@ -19910,6 +19938,10 @@ packages: stream-shift: 1.0.1 dev: true + /eastasianwidth/0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecc-jsbn/0.1.2: resolution: {integrity: sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=} dependencies: @@ -24719,7 +24751,7 @@ packages: react-devtools-core: 4.22.0 react-reconciler: 0.26.2_react@17.0.2 scheduler: 0.20.2 - signal-exit: 3.0.5 + signal-exit: 3.0.7 slice-ansi: 3.0.0 stack-utils: 2.0.5 string-width: 4.2.3 @@ -25088,6 +25120,11 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + /is-fullwidth-code-point/4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + dev: true + /is-function/1.0.2: resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} dev: true @@ -25603,7 +25640,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.2 + debug: 4.3.3 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -28225,11 +28262,53 @@ packages: uc.micro: 1.0.6 dev: true + /lint-staged/12.3.7: + resolution: {integrity: sha512-/S4D726e2GIsDVWIk1XGvheCaDm1SJRQp8efamZFWJxQMVEbOwSysp7xb49Oo73KYCdy97mIWinhlxcoNqIfIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + dependencies: + cli-truncate: 3.1.0 + colorette: 2.0.16 + commander: 8.3.0 + debug: 4.3.3_supports-color@9.2.2 + execa: 5.1.1 + lilconfig: 2.0.4 + listr2: 4.0.5 + micromatch: 4.0.4 + normalize-path: 3.0.0 + object-inspect: 1.12.0 + pidtree: 0.5.0 + string-argv: 0.3.1 + supports-color: 9.2.2 + yaml: 1.10.2 + transitivePeerDependencies: + - enquirer + dev: true + /liquid-json/0.3.1: resolution: {integrity: sha1-kVWhgTbYprJhXl8W+aJEira1Duo=} engines: {node: '>=4'} dev: false + /listr2/4.0.5: + resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} + engines: {node: '>=12'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.16 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.0 + rxjs: 7.5.5 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + /livereload-js/2.4.0: resolution: {integrity: sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==} dev: true @@ -28521,6 +28600,16 @@ packages: is-unicode-supported: 0.1.0 dev: true + /log-update/4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + dev: true + /lolex/5.1.2: resolution: {integrity: sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==} dependencies: @@ -30892,6 +30981,12 @@ packages: hasBin: true dev: true + /pidtree/0.5.0: + resolution: {integrity: sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + /pify/2.3.0: resolution: {integrity: sha1-7RQaasBDqEnqWISY59yosVMw6Qw=} engines: {node: '>=0.10.0'} @@ -34394,6 +34489,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rfdc/1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + /rgb-regex/1.0.1: resolution: {integrity: sha1-wODWiC3w4jviVKR16O3UGRX+rrE=} @@ -34574,6 +34673,12 @@ packages: tslib: 2.3.1 dev: true + /rxjs/7.5.5: + resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==} + dependencies: + tslib: 2.3.1 + dev: true + /safe-buffer/5.1.1: resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==} dev: true @@ -35121,6 +35226,14 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /slice-ansi/5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.1.0 + is-fullwidth-code-point: 4.0.0 + dev: true + /smart-buffer/4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -35458,6 +35571,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /string-argv/0.3.1: + resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} + engines: {node: '>=0.6.19'} + dev: true + /string-hash/1.1.3: resolution: {integrity: sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=} dev: true @@ -35522,6 +35640,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width/5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: true + /string.prototype.matchall/4.0.6: resolution: {integrity: sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==} dependencies: @@ -36175,6 +36302,11 @@ packages: dependencies: has-flag: 4.0.0 + /supports-color/9.2.2: + resolution: {integrity: sha512-XC6g/Kgux+rJXmwokjm9ECpD6k/smUoS5LKlUCcsYr4IY3rW0XyAympon2RmxGrlnZURMpg5T18gWDP9CsHXFA==} + engines: {node: '>=12'} + dev: true + /supports-hyperlinks/2.2.0: resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} engines: {node: '>=8'} From 5bfcaa7f3f091db5c64b692b2b94b046a0c2760a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 6 Apr 2022 16:57:29 +0800 Subject: [PATCH 041/206] Use lint-staged in pre-commit hook --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index aa52ea8822c..5e592735698 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -pnpm nx affected:lint --uncommitted +pnpm exec lint-staged From 2b1f54b09d24cb9ba283f5381b6acf0d442fe114 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 6 Apr 2022 16:58:17 +0800 Subject: [PATCH 042/206] Set up lint-staged configuration for each package --- packages/js/admin-e2e-tests/package.json | 5 +++++ packages/js/api-core-tests/package.json | 5 +++++ packages/js/api/package.json | 5 +++++ packages/js/components/package.json | 6 ++++++ packages/js/csv-export/package.json | 6 ++++++ packages/js/currency/package.json | 6 ++++++ packages/js/customer-effort-score/package.json | 6 ++++++ packages/js/data/package.json | 6 ++++++ packages/js/date/package.json | 6 ++++++ .../js/dependency-extraction-webpack-plugin/package.json | 5 +++++ packages/js/e2e-core-tests/package.json | 5 +++++ packages/js/e2e-environment/package.json | 5 +++++ packages/js/e2e-utils/package.json | 5 +++++ packages/js/experimental/package.json | 6 ++++++ packages/js/explat/package.json | 6 ++++++ packages/js/js-tests/package.json | 5 +++++ packages/js/navigation/package.json | 6 ++++++ packages/js/notices/package.json | 5 +++++ packages/js/number/package.json | 6 ++++++ packages/js/onboarding/package.json | 5 +++++ packages/js/style-build/package.json | 5 +++++ packages/js/tracks/package.json | 5 +++++ plugins/woocommerce-admin/package.json | 9 +++++++++ plugins/woocommerce-beta-tester/package.json | 9 +++++++++ plugins/woocommerce/package.json | 9 +++++++++ 25 files changed, 147 insertions(+) diff --git a/packages/js/admin-e2e-tests/package.json b/packages/js/admin-e2e-tests/package.json index be6a8efe6d2..200ee2170f9 100644 --- a/packages/js/admin-e2e-tests/package.json +++ b/packages/js/admin-e2e-tests/package.json @@ -56,5 +56,10 @@ "clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*", "lint": "eslint src", "prepack": "pnpm run clean && pnpm run build" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/api-core-tests/package.json b/packages/js/api-core-tests/package.json index 4c4bf1fa0d3..f83420e77d3 100644 --- a/packages/js/api-core-tests/package.json +++ b/packages/js/api-core-tests/package.json @@ -33,5 +33,10 @@ }, "bin": { "wc-api-tests": "bin/wc-api-tests.sh" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/api/package.json b/packages/js/api/package.json index af4790ba749..913125e6bcd 100644 --- a/packages/js/api/package.json +++ b/packages/js/api/package.json @@ -52,5 +52,10 @@ }, "publishConfig": { "access": "public" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/components/package.json b/packages/js/components/package.json index b9ec712baa5..abdca8364fc 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -123,5 +123,11 @@ "test:nobuild": "jest --config ./jest.config.json", "test:update-snapshots": "pnpm run test:nobuild -- --updateSnapshot", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/csv-export/package.json b/packages/js/csv-export/package.json index 50722c87690..3db27db855d 100644 --- a/packages/js/csv-export/package.json +++ b/packages/js/csv-export/package.json @@ -46,5 +46,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/currency/package.json b/packages/js/currency/package.json index 80661eca8bd..bcaadf03b03 100644 --- a/packages/js/currency/package.json +++ b/packages/js/currency/package.json @@ -49,5 +49,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/customer-effort-score/package.json b/packages/js/customer-effort-score/package.json index c1bd32fd97c..b21e073edfe 100644 --- a/packages/js/customer-effort-score/package.json +++ b/packages/js/customer-effort-score/package.json @@ -70,5 +70,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/data/package.json b/packages/js/data/package.json index 1308e6827d0..1096444c615 100644 --- a/packages/js/data/package.json +++ b/packages/js/data/package.json @@ -70,5 +70,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/date/package.json b/packages/js/date/package.json index eb6240d2f7c..9fa0935dfad 100644 --- a/packages/js/date/package.json +++ b/packages/js/date/package.json @@ -52,5 +52,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/dependency-extraction-webpack-plugin/package.json b/packages/js/dependency-extraction-webpack-plugin/package.json index eccc6705f2b..e280009130c 100644 --- a/packages/js/dependency-extraction-webpack-plugin/package.json +++ b/packages/js/dependency-extraction-webpack-plugin/package.json @@ -34,5 +34,10 @@ "typescript": "^4.6.2", "webpack": "^5.70.0", "webpack-cli": "^3.3.12" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-core-tests/package.json b/packages/js/e2e-core-tests/package.json index bbc55e8eadc..475cb1aa8fe 100644 --- a/packages/js/e2e-core-tests/package.json +++ b/packages/js/e2e-core-tests/package.json @@ -48,5 +48,10 @@ "clean": "rm -rf ./build ./build-module", "compile": "node ./../bin/build.js", "build": "./bin/build.sh && pnpm run clean && pnpm run compile" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-environment/package.json b/packages/js/e2e-environment/package.json index 07de1318f83..f30f3a535c2 100644 --- a/packages/js/e2e-environment/package.json +++ b/packages/js/e2e-environment/package.json @@ -77,5 +77,10 @@ }, "bin": { "wc-e2e": "bin/wc-e2e.sh" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/e2e-utils/package.json b/packages/js/e2e-utils/package.json index 867a4bab905..f83dd1c01cb 100644 --- a/packages/js/e2e-utils/package.json +++ b/packages/js/e2e-utils/package.json @@ -45,5 +45,10 @@ "build": "pnpm run clean && pnpm run compile", "prepare": "pnpm run build", "lint": "eslint src" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index 8a7530f6085..c75114da92e 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -83,5 +83,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 533172d328f..a2145aef476 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -54,5 +54,11 @@ "test": "pnpm run build && pnpm run test:nobuild", "test:nobuild": "jest --config ./jest.config.json", "test-staged": "jest --bail --config ./jest.config.json --findRelatedTests" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/js-tests/package.json b/packages/js/js-tests/package.json index c6de210f262..d8e7895f022 100644 --- a/packages/js/js-tests/package.json +++ b/packages/js/js-tests/package.json @@ -40,5 +40,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index c4b578c2cc6..75f25e62743 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -57,5 +57,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index ca89ed63893..b316ab1899a 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -51,5 +51,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/number/package.json b/packages/js/number/package.json index d7daf0f01d0..92843dff466 100644 --- a/packages/js/number/package.json +++ b/packages/js/number/package.json @@ -45,5 +45,11 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix", + "pnpm test-staged" + ] } } diff --git a/packages/js/onboarding/package.json b/packages/js/onboarding/package.json index 8ee2642b336..5119a30f58e 100644 --- a/packages/js/onboarding/package.json +++ b/packages/js/onboarding/package.json @@ -59,5 +59,10 @@ "start": "concurrently \"tsc --build --watch\" \"webpack --watch\"", "prepack": "pnpm run clean && pnpm run build", "lint": "eslint src" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/style-build/package.json b/packages/js/style-build/package.json index 96926105d6c..aca30b6c530 100644 --- a/packages/js/style-build/package.json +++ b/packages/js/style-build/package.json @@ -42,5 +42,10 @@ "ts-jest": "^27.1.3", "typescript": "^4.6.2", "webpack": "^5.70.0" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/packages/js/tracks/package.json b/packages/js/tracks/package.json index 314b4c9abe5..cf5eca20e78 100644 --- a/packages/js/tracks/package.json +++ b/packages/js/tracks/package.json @@ -42,5 +42,10 @@ "rimraf": "^3.0.2", "ts-jest": "^27.1.3", "typescript": "^4.6.2" + }, + "lint-staged": { + "*.(t|j)s?(x)": [ + "eslint --fix" + ] } } diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 374bbccfffd..b2bb9d530ba 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -48,6 +48,7 @@ "lint:css": "stylelint '**/*.scss'", "lint:css-fix": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", "lint:js": "wp-scripts lint-js ./client --ext=js,ts,tsx", + "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", "lint:js-fix": "pnpm run lint:js -- --fix --ext=js,ts,tsx", "lint:php": "./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$)", @@ -280,6 +281,14 @@ "peerDependencies": { "@wordpress/data": "^6.3.0" }, + "lint-staged": { + "*.scss": [ "pnpm lint:css-fix" ], + "client/**/*.(t|j)s?(x)": [ + "pnpm reformat-files", + "pnpm wp-scripts lint-js", + "pnpm test-staged" + ] + }, "engines": { "node": "^16.13.1", "pnpm": "^6.24.2" diff --git a/plugins/woocommerce-beta-tester/package.json b/plugins/woocommerce-beta-tester/package.json index 2c4c3b89233..c9044a0682f 100644 --- a/plugins/woocommerce-beta-tester/package.json +++ b/plugins/woocommerce-beta-tester/package.json @@ -33,5 +33,14 @@ "woorelease": { "svn_reauth": "true", "wp_org_slug": "woocommerce-beta-tester" + }, + "lint-staged": { + "*.php": [ + "php -d display_errors=1 -l", + "composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit" + ], + "!(*min).js": [ + "eslint --fix" + ] } } diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index c6a827e7ef9..6ae8e8e0ace 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -69,6 +69,15 @@ "webpack-cli": "3.3.12", "wp-textdomain": "1.0.1" }, + "lint-staged": { + "*.php": [ + "php -d display_errors=1 -l", + "composer --working-dir=./plugins/woocommerce run-script phpcs-pre-commit" + ], + "!(*min).js": [ + "eslint --fix" + ] + }, "engines": { "node": "^16.13.1", "pnpm": "^6.24.2" From f50379d8a2c5a493c6b3df1d5e09de6a8aa269d3 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 19:15:08 +0800 Subject: [PATCH 043/206] Update all onboarding card images --- .../src/components/WCPayAcceptedMethods.js | 15 +- .../src/components/WCPayCard/WCPayCard.scss | 17 +- .../js/onboarding/src/images/cards/amex.js | 23 +- .../onboarding/src/images/cards/applepay.js | 21 +- packages/js/onboarding/src/images/cards/cb.js | 33 ++- .../js/onboarding/src/images/cards/diners.js | 251 ++---------------- .../onboarding/src/images/cards/discover.js | 148 ++++++++--- .../js/onboarding/src/images/cards/giropay.js | 49 ++++ .../onboarding/src/images/cards/googlepay.js | 47 ++-- .../js/onboarding/src/images/cards/jcb.js | 72 +++-- .../onboarding/src/images/cards/mastercard.js | 35 ++- .../js/onboarding/src/images/cards/sofort.js | 35 +++ .../onboarding/src/images/cards/unionpay.js | 103 +------ .../js/onboarding/src/images/cards/visa.js | 35 ++- 14 files changed, 413 insertions(+), 471 deletions(-) create mode 100644 packages/js/onboarding/src/images/cards/giropay.js create mode 100644 packages/js/onboarding/src/images/cards/sofort.js diff --git a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js index 7fc42131c2d..a79a1031c23 100644 --- a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js +++ b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js @@ -10,14 +10,16 @@ import { __ } from '@wordpress/i18n'; */ import Visa from '../images/cards/visa.js'; import MasterCard from '../images/cards/mastercard.js'; -import Maestro from '../images/cards/maestro.js'; import Amex from '../images/cards/amex.js'; import ApplePay from '../images/cards/applepay.js'; +import GooglePay from '../images/cards/googlepay.js'; import CB from '../images/cards/cb.js'; import DinersClub from '../images/cards/diners.js'; import Discover from '../images/cards/discover.js'; import JCB from '../images/cards/jcb.js'; import UnionPay from '../images/cards/unionpay.js'; +import GiroPay from '../images/cards/giropay.js'; +import Sofort from '../images/cards/sofort.js'; export const WCPayAcceptedMethods = () => ( <> @@ -28,14 +30,19 @@ export const WCPayAcceptedMethods = () => (
- - + + + + - + +
+ & more. +
); diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index 9d375486176..57a2ee5305a 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -79,14 +79,17 @@ } .woocommerce-task-payment-wcpay__accepted { - display: flex; - margin-top: $gap-small; - flex-wrap: wrap; - gap: $gap-small; - h3 { color: #40464d; } + + display: flex; + flex-wrap: wrap; + margin-top: $gap-small; + margin-left: 0; + gap: 8px; + justify-content: flex-start; + align-items: flex-end; } .woocommerce-task-payment-wcpay__benefit { @@ -99,6 +102,10 @@ line-height: 24px; letter-spacing: normal; } + + .woocommerce-task-payment-wcpay__accepted__and-more { + white-space: nowrap; + } } .woocommerce-task-payment-wcpay__benefits-card { diff --git a/packages/js/onboarding/src/images/cards/amex.js b/packages/js/onboarding/src/images/cards/amex.js index daa085352db..49facfe9d6e 100644 --- a/packages/js/onboarding/src/images/cards/amex.js +++ b/packages/js/onboarding/src/images/cards/amex.js @@ -5,26 +5,25 @@ import { createElement } from '@wordpress/element'; export default () => ( - + ); diff --git a/packages/js/onboarding/src/images/cards/applepay.js b/packages/js/onboarding/src/images/cards/applepay.js index 74febe62299..edb77a8a8e8 100644 --- a/packages/js/onboarding/src/images/cards/applepay.js +++ b/packages/js/onboarding/src/images/cards/applepay.js @@ -5,26 +5,25 @@ import { createElement } from '@wordpress/element'; export default () => ( - + ); diff --git a/packages/js/onboarding/src/images/cards/cb.js b/packages/js/onboarding/src/images/cards/cb.js index da48c9a5b13..b3dcb6cadcd 100644 --- a/packages/js/onboarding/src/images/cards/cb.js +++ b/packages/js/onboarding/src/images/cards/cb.js @@ -5,34 +5,33 @@ import { createElement } from '@wordpress/element'; export default () => ( - + diff --git a/packages/js/onboarding/src/images/cards/diners.js b/packages/js/onboarding/src/images/cards/diners.js index 039bad8b238..5630431f0eb 100644 --- a/packages/js/onboarding/src/images/cards/diners.js +++ b/packages/js/onboarding/src/images/cards/diners.js @@ -5,254 +5,37 @@ import { createElement } from '@wordpress/element'; export default () => ( - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); diff --git a/packages/js/onboarding/src/images/cards/discover.js b/packages/js/onboarding/src/images/cards/discover.js index 51479f917aa..a9e764ba46c 100644 --- a/packages/js/onboarding/src/images/cards/discover.js +++ b/packages/js/onboarding/src/images/cards/discover.js @@ -5,46 +5,134 @@ import { createElement } from '@wordpress/element'; export default () => ( - + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + ); diff --git a/packages/js/onboarding/src/images/cards/giropay.js b/packages/js/onboarding/src/images/cards/giropay.js new file mode 100644 index 00000000000..6979d1911c5 --- /dev/null +++ b/packages/js/onboarding/src/images/cards/giropay.js @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default () => ( + + + + + + + + + +); diff --git a/packages/js/onboarding/src/images/cards/googlepay.js b/packages/js/onboarding/src/images/cards/googlepay.js index 329ec0e3641..d7e00b3b269 100644 --- a/packages/js/onboarding/src/images/cards/googlepay.js +++ b/packages/js/onboarding/src/images/cards/googlepay.js @@ -5,48 +5,61 @@ import { createElement } from '@wordpress/element'; export default () => ( - + ); diff --git a/packages/js/onboarding/src/images/cards/jcb.js b/packages/js/onboarding/src/images/cards/jcb.js index ee332408bba..36c6569289e 100644 --- a/packages/js/onboarding/src/images/cards/jcb.js +++ b/packages/js/onboarding/src/images/cards/jcb.js @@ -5,38 +5,78 @@ import { createElement } from '@wordpress/element'; export default () => ( - + + + + + + + + + + + + + + + + ); diff --git a/packages/js/onboarding/src/images/cards/mastercard.js b/packages/js/onboarding/src/images/cards/mastercard.js index 8fbde439e41..d6755de398d 100644 --- a/packages/js/onboarding/src/images/cards/mastercard.js +++ b/packages/js/onboarding/src/images/cards/mastercard.js @@ -5,38 +5,37 @@ import { createElement } from '@wordpress/element'; export default () => ( - ); diff --git a/packages/js/onboarding/src/images/cards/sofort.js b/packages/js/onboarding/src/images/cards/sofort.js new file mode 100644 index 00000000000..9d651368c38 --- /dev/null +++ b/packages/js/onboarding/src/images/cards/sofort.js @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { createElement } from '@wordpress/element'; + +export default () => ( + + + + + + + +); diff --git a/packages/js/onboarding/src/images/cards/unionpay.js b/packages/js/onboarding/src/images/cards/unionpay.js index 0be12014c7b..457b182fbbb 100644 --- a/packages/js/onboarding/src/images/cards/unionpay.js +++ b/packages/js/onboarding/src/images/cards/unionpay.js @@ -5,110 +5,35 @@ import { createElement } from '@wordpress/element'; export default () => ( - - - - - - - - - - ); diff --git a/packages/js/onboarding/src/images/cards/visa.js b/packages/js/onboarding/src/images/cards/visa.js index cd81d8a8fa3..dbae529ccd4 100644 --- a/packages/js/onboarding/src/images/cards/visa.js +++ b/packages/js/onboarding/src/images/cards/visa.js @@ -5,40 +5,39 @@ import { createElement } from '@wordpress/element'; export default () => ( - + ); From a13f15624a1099eb3cd07d94900e0dad1164b618 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Tue, 5 Apr 2022 11:57:18 -0300 Subject: [PATCH 044/206] Make sure we also track completion of non visible tasks and fix parent id in WooCommercePayments task --- .../src/Admin/Features/OnboardingTasks/TaskList.php | 7 +++++++ .../OnboardingTasks/Tasks/AdditionalPayments.php | 7 +++++-- .../OnboardingTasks/Tasks/WooCommercePayments.php | 10 +--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index ddfa282d70e..e2439ad0d2c 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -350,6 +350,13 @@ class TaskList { */ public function get_json() { $this->possibly_track_completion(); + // Track completion of non viewable tasks. + foreach ( $this->tasks as $task ) { + if ( ! $task->can_view() ) { + $task->possibly_track_completion(); + } + } + return array( 'id' => $this->get_list_id(), 'title' => $this->title, diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php index 6277a0acd55..e479c4292db 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/AdditionalPayments.php @@ -73,8 +73,11 @@ class AdditionalPayments extends Payments { $woocommerce_payments = new WooCommercePayments(); - if ( ! $woocommerce_payments->is_requested() || ( $woocommerce_payments->is_supported() && ! $woocommerce_payments->is_connected() ) ) { - // Hide task if WC Pay is installed via OBW, in supported country, but not connected. + if ( ! $woocommerce_payments->is_requested() || ! $woocommerce_payments->is_supported() || ! $woocommerce_payments->is_connected() ) { + // Hide task if WC Pay is not installed via OBW, or is not connected, or the store is located in a country that is not supported by WC Pay. + return false; + } + if ( $this->get_parent_id() === 'extended_two_column' && WooCommercePayments::is_connected() ) { return false; } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php index 55b30784880..af95dfcc174 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php @@ -19,15 +19,6 @@ class WooCommercePayments extends Task { return 'woocommerce-payments'; } - /** - * Parent ID. - * - * @return string - */ - public function get_parent_id() { - return 'setup'; - } - /** * Title. * @@ -130,6 +121,7 @@ class WooCommercePayments extends Task { * @return bool */ public static function is_connected() { + return true; if ( class_exists( '\WC_Payments' ) ) { $wc_payments_gateway = \WC_Payments::get_gateway(); return method_exists( $wc_payments_gateway, 'is_connected' ) From a7ec02447196467b54daa1a70f5e00ebc602929d Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Tue, 5 Apr 2022 12:09:49 -0300 Subject: [PATCH 045/206] Remove test condition --- .../Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php index af95dfcc174..fa36d7df0dd 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/WooCommercePayments.php @@ -121,7 +121,6 @@ class WooCommercePayments extends Task { * @return bool */ public static function is_connected() { - return true; if ( class_exists( '\WC_Payments' ) ) { $wc_payments_gateway = \WC_Payments::get_gateway(); return method_exists( $wc_payments_gateway, 'is_connected' ) From 828e34a9745e5f84e195f76ed08e099048de4130 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 10:08:22 -0300 Subject: [PATCH 046/206] Address PR feedback and fix PHP unit tests --- .../Admin/Features/OnboardingTasks/TaskList.php | 14 +++++--------- .../features/onboarding-tasks/task.php | 2 +- .../features/onboarding-tasks/test-task.php | 9 --------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index e2439ad0d2c..f451ea48caf 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -350,10 +350,11 @@ class TaskList { */ public function get_json() { $this->possibly_track_completion(); - // Track completion of non viewable tasks. + $tasks_json = array(); foreach ( $this->tasks as $task ) { - if ( ! $task->can_view() ) { - $task->possibly_track_completion(); + $json = $task->get_json(); + if ( $json['canView'] ) { + $tasks_json[] = $json; } } @@ -363,12 +364,7 @@ class TaskList { 'isHidden' => $this->is_hidden(), 'isVisible' => $this->is_visible(), 'isComplete' => $this->is_complete(), - 'tasks' => array_map( - function( $task ) { - return $task->get_json(); - }, - $this->get_viewable_tasks() - ), + 'tasks' => $tasks_json, 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, ); diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index 88eaeffee34..b9b27a0c1ae 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -394,7 +394,7 @@ class WC_Admin_Tests_OnboardingTasks_Task extends WC_Unit_Test_Case { */ public function test_get_list_id() { $task = new TestTask( - new TaskList( array( 'id' => 'setup' ) ), + new TaskList( array( 'id' => 'extended' ) ), array( 'id' => 'wc-unit-test-task', ) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php index 2f3b81ba966..262a9503305 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/test-task.php @@ -96,15 +96,6 @@ class TestTask extends Task { return $this->content; } - /** - * Parent ID. - * - * @return string - */ - public function get_parent_id() { - return 'extended'; - } - /** * Level. * From b660f58c1c1b34b9932f0c4135567246b96bac5e Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 10:10:20 -0300 Subject: [PATCH 047/206] Add changelog --- plugins/woocommerce/changelog/fix-32140_wcpay_completion_task | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-32140_wcpay_completion_task diff --git a/plugins/woocommerce/changelog/fix-32140_wcpay_completion_task b/plugins/woocommerce/changelog/fix-32140_wcpay_completion_task new file mode 100644 index 00000000000..d8b06ce4cb0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32140_wcpay_completion_task @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix issue where some tasks where not being tracked as completed, when tracking is enabled. #32493 From f82b5d78f1669e2ec50f0567c67bbdc7a88db55c Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Wed, 6 Apr 2022 21:31:50 +0800 Subject: [PATCH 048/206] Fix css and test --- .../src/components/WCPayCard/WCPayCard.js | 27 +++++++++---------- .../src/components/WCPayCard/WCPayCard.scss | 18 ++++++++++--- .../PaymentGatewaySuggestions/test/index.js | 6 +++-- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js index a865588b22f..ca4f5cf634b 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js @@ -17,22 +17,19 @@ import WCPayBenefit1 from '../../images/wcpay-benefit-1'; import WCPayBenefit2 from '../../images/wcpay-benefit-2'; import WCPayBenefit3 from '../../images/wcpay-benefit-3'; -export const WCPayCardBody = ( { - heading, - children, - onLinkClick = () => {}, -} ) => ( +export const WCPayCardBody = ( { children, onLinkClick = () => {} } ) => (
- { heading && ( - - { heading } - - ) } + + { __( + 'Accept payments and manage your business.', + 'woocommerce' + ) } +
{ children }
-
- +
+
diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index 57a2ee5305a..65d81ee1a88 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -3,6 +3,7 @@ .vstack, .hstack { display: flex; + &.content-center { justify-content: center; } @@ -24,9 +25,19 @@ flex-direction: row; } - .wcpay-hero-image { - margin-right: -32px; - margin-bottom: -28px; + .woocommerce-task-payment-wcpay__hero-image-container { + display: flex; + flex-direction: column; + justify-content: flex-end; + + svg { + margin-right: -32px; + margin-bottom: -24px; + } + + @media screen and (max-width: 600px) { + max-width: 200px; + } } .woocommerce-task-payment-wcpay__heading { @@ -96,6 +107,7 @@ svg { margin: 0 auto; } + max-width: 170px; text-align: center; font-size: 15px; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index 7df91e7a82a..3fdf6c29efd 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -118,8 +118,10 @@ describe( 'PaymentGatewaySuggestions', () => { expect( paymentTitles ).toEqual( [] ); expect( - container.getElementsByTagName( 'title' )[ 0 ].textContent - ).toBe( 'WooCommerce Payments' ); + container.getElementsByClassName( + 'woocommerce-task-payment-wcpay' + )[ 0 ].textContent + ).toContain( 'By using WooCommerce Payments' ); } ); test( 'should render all payment gateways if no WCPay', () => { From fcc7143207d6b33d389015dbf2073645ba3d20f2 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Wed, 6 Apr 2022 14:01:36 -0300 Subject: [PATCH 049/206] Add alreadyActivatedExtensions to persisted business extensions --- .../steps/business-details/flows/selective-bundle/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js index 10dfadf55f3..71ecdb0bf33 100644 --- a/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js +++ b/plugins/woocommerce-admin/client/profile-wizard/steps/business-details/flows/selective-bundle/index.js @@ -201,7 +201,10 @@ class BusinessDetails extends Component { const promises = [ this.persistProfileItems( { - business_extensions: businessExtensions, + business_extensions: [ + ...businessExtensions, + ...alreadyActivatedExtensions, + ], } ), ]; From fd1bca35404d486bd4ca421fca0243dd048f3255 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Wed, 6 Apr 2022 14:07:55 -0300 Subject: [PATCH 050/206] Add changelog --- plugins/woocommerce/changelog/fix-hidden_wcpay_task | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-hidden_wcpay_task diff --git a/plugins/woocommerce/changelog/fix-hidden_wcpay_task b/plugins/woocommerce/changelog/fix-hidden_wcpay_task new file mode 100644 index 00000000000..cc3627b2ba0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-hidden_wcpay_task @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +WCPayments task is not visible after installing the plugin #32506 From 30c7586f36b9d38153b38077502d524530ec4caa Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 16:12:56 -0300 Subject: [PATCH 051/206] Update comment --- .../woocommerce-admin/features/onboarding-tasks/task.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index b9b27a0c1ae..3cdae60afe9 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -1,6 +1,6 @@ Date: Wed, 6 Apr 2022 16:36:39 -0300 Subject: [PATCH 052/206] Move require_once below use statements --- .../woocommerce-admin/features/onboarding-tasks/task.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php index 3cdae60afe9..ba2ee818c71 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/features/onboarding-tasks/task.php @@ -5,11 +5,11 @@ * @package WooCommerce\Admin\Tests\OnboardingTasks */ -require_once __DIR__ . '/test-task.php'; - use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task; use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskList; +require_once __DIR__ . '/test-task.php'; + /** * class WC_Admin_Tests_OnboardingTasks_Task */ From ce1bc10ea3a604d1c1a730e2eb792dbd12d91efe Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 10:44:04 +0800 Subject: [PATCH 053/206] Add changelog --- plugins/woocommerce/changelog/fix-32399-husky | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-32399-husky diff --git a/plugins/woocommerce/changelog/fix-32399-husky b/plugins/woocommerce/changelog/fix-32399-husky new file mode 100644 index 00000000000..da836bbc7d1 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32399-husky @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Fix husky git hooks. From 4c4edc1c03ae1f8e7dc3d59945c00821ca2adfd8 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 7 Apr 2022 11:23:04 +0800 Subject: [PATCH 054/206] Fix css --- .../js/onboarding/src/components/WCPayAcceptedMethods.js | 8 ++++---- .../js/onboarding/src/components/WCPayCard/WCPayCard.scss | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js index a79a1031c23..cea52fdb3d9 100644 --- a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js +++ b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js @@ -22,12 +22,12 @@ import GiroPay from '../images/cards/giropay.js'; import Sofort from '../images/cards/sofort.js'; export const WCPayAcceptedMethods = () => ( - <> +
{ __( 'Accepted payment methods include:', 'woocommerce' ) } -
+
@@ -40,9 +40,9 @@ export const WCPayAcceptedMethods = () => ( -
+
& more.
- +
); diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index 65d81ee1a88..6e50f1f3edd 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -89,7 +89,7 @@ } } - .woocommerce-task-payment-wcpay__accepted { + .woocommerce-task-payment-wcpay__accepted-icons { h3 { color: #40464d; } @@ -115,7 +115,7 @@ letter-spacing: normal; } - .woocommerce-task-payment-wcpay__accepted__and-more { + .woocommerce-task-payment-wcpay__accepted-and-more { white-space: nowrap; } } From 70359c8d11d470e56a5e54c75fd72f2a92fec652 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 14:16:46 +0800 Subject: [PATCH 055/206] Update @wordpress/a11y from ^2.15.3 to ^3.5.0 --- packages/js/notices/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- pnpm-lock.yaml | 55 ++++++++++++++++++-------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/js/notices/package.json b/packages/js/notices/package.json index ca89ed63893..476b26a0789 100644 --- a/packages/js/notices/package.json +++ b/packages/js/notices/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@wordpress/a11y": "^2.15.3", + "@wordpress/a11y": "^3.5.0", "@wordpress/data": "^6.3.0", "@wordpress/notices": "^3.3.2" }, diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index a21d959a0dd..17ff33bf8e1 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -96,7 +96,7 @@ "@woocommerce/api": "^0.2.0", "@woocommerce/e2e-environment": "^0.3.0", "@woocommerce/e2e-utils": "^0.2.0", - "@wordpress/a11y": "^2.15.3", + "@wordpress/a11y": "^3.5.0", "@wordpress/api-fetch": "^6.0.1", "@wordpress/base-styles": "^3.6.0", "@wordpress/components": "^19.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87d9d2c2986..661111ef532 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -901,7 +901,7 @@ importers: packages/js/notices: specifiers: '@babel/core': ^7.17.5 - '@wordpress/a11y': ^2.15.3 + '@wordpress/a11y': ^3.5.0 '@wordpress/data': ^6.3.0 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/notices': ^3.3.2 @@ -912,7 +912,7 @@ importers: ts-jest: ^27.1.3 typescript: ^4.6.2 dependencies: - '@wordpress/a11y': 2.15.3 + '@wordpress/a11y': 3.5.0 '@wordpress/data': 6.4.1 '@wordpress/notices': 3.4.1 devDependencies: @@ -1226,7 +1226,7 @@ importers: '@woocommerce/onboarding': workspace:* '@woocommerce/style-build': workspace:* '@woocommerce/tracks': workspace:* - '@wordpress/a11y': ^2.15.3 + '@wordpress/a11y': ^3.5.0 '@wordpress/api-fetch': ^6.0.1 '@wordpress/babel-plugin-makepot': ^2.1.3 '@wordpress/babel-preset-default': ^6.5.1 @@ -1348,7 +1348,7 @@ importers: '@woocommerce/api': link:../../packages/js/api '@woocommerce/e2e-environment': link:../../packages/js/e2e-environment '@woocommerce/e2e-utils': link:../../packages/js/e2e-utils - '@wordpress/a11y': 2.15.3 + '@wordpress/a11y': 3.5.0 '@wordpress/api-fetch': 6.1.1 '@wordpress/base-styles': 3.6.0 '@wordpress/components': 19.6.1_978f344c876a57c1143ffe356b90df31 @@ -13779,14 +13779,6 @@ packages: react: 17.0.2 dev: true - /@wordpress/a11y/2.15.3: - resolution: {integrity: sha512-uoCznHY3/TaNWeXutLI6juC198ykaBwZ34P51PNHHQqi3WzVoBhFx6AnAR/9Uupl3tZcekefpkVHy7AJHMAPIA==} - dependencies: - '@babel/runtime': 7.17.7 - '@wordpress/dom-ready': 2.13.2 - '@wordpress/i18n': 3.20.0 - dev: false - /@wordpress/a11y/3.4.1: resolution: {integrity: sha512-SjeLO8x/Y/QAcKBrvyJiu8KVAPckRLNwuFfgX7zCGM8vBfg+Depj94Hp55ARLjq0oXHg7EWKxSdzNkvmTz8AIA==} engines: {node: '>=12'} @@ -13796,6 +13788,15 @@ packages: '@wordpress/i18n': 4.4.1 dev: false + /@wordpress/a11y/3.5.0: + resolution: {integrity: sha512-pJyDexol4yFfUNs6BAW1IKftdxZBsxvNRpzmYcwXiFA+1jSnMJFehp0nu47skdzxiHS6CKyLqBks17J+a/GqGA==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/dom-ready': 3.5.0 + '@wordpress/i18n': 4.5.0 + dev: false + /@wordpress/api-fetch/5.2.6: resolution: {integrity: sha512-AG8KdCHwtYJWR38AAU7nEI+UbumUSqSBthQj3rShLUVyFbYGkQdpwXJJG6vFj7FjIp41zljiyj3K1Fh3cqdaAw==} engines: {node: '>=12'} @@ -14366,14 +14367,15 @@ packages: '@wordpress/hooks': 3.4.1 dev: false - /@wordpress/dom-ready/2.13.2: - resolution: {integrity: sha512-COH7n2uZfBq4FtluSbl37N3nCEcdMXzV42ETCWKUcumiP1Zd3qnkfQKcsxTaHWY8aVt/358RvJ7ghWe3xAd+fg==} + /@wordpress/dom-ready/3.4.1: + resolution: {integrity: sha512-w6DVKKpNwX0XUp0Cuh1OyFyGXLabr47k/ecRHKmQkQh9LdjRew7QvxUHYDN1rejRvq5GqcDb7Gnkz4E6hWIo4Q==} + engines: {node: '>=12'} dependencies: '@babel/runtime': 7.17.7 dev: false - /@wordpress/dom-ready/3.4.1: - resolution: {integrity: sha512-w6DVKKpNwX0XUp0Cuh1OyFyGXLabr47k/ecRHKmQkQh9LdjRew7QvxUHYDN1rejRvq5GqcDb7Gnkz4E6hWIo4Q==} + /@wordpress/dom-ready/3.5.0: + resolution: {integrity: sha512-xhxZx3qH0UoWI3AMvZpB7NnKkHR5m5ifrBlinXM3+kSPQ8bIUkuOi2cFYdCnglPi0a+dd7OahWKFzXwDvgjO1w==} engines: {node: '>=12'} dependencies: '@babel/runtime': 7.17.7 @@ -14716,6 +14718,13 @@ packages: dependencies: '@babel/runtime': 7.17.7 + /@wordpress/hooks/3.5.0: + resolution: {integrity: sha512-6Ko+1rWLq75s2LeZah6e0sJC5lC2nL1M+DDLlP/EZ+YCGZlIKoCvkhVBuCdI2wgIHRPXU56OqLvw85rUjsfDJw==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + dev: false + /@wordpress/html-entities/3.4.1: resolution: {integrity: sha512-wSuwgONTefnhCB9B7mKS+e8islHuCkprfDc+FhqVAa6r5RbVBGvaHUJs8embgdtww7MwBRMnskNf/buQ8Jr02A==} engines: {node: '>=12'} @@ -14749,6 +14758,20 @@ packages: sprintf-js: 1.1.2 tannin: 1.2.0 + /@wordpress/i18n/4.5.0: + resolution: {integrity: sha512-BhOHsgnbWeUWT+23P/vksen0MiZ+OyhemZkKUHtQnelRKY4FnFEYvjp5q9v/O7DY3J0Hqc+Ss4wLNqajRQmMIw==} + engines: {node: '>=12'} + hasBin: true + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/hooks': 3.5.0 + gettext-parser: 1.4.0 + lodash: 4.17.21 + memize: 1.1.0 + sprintf-js: 1.1.2 + tannin: 1.2.0 + dev: false + /@wordpress/icons/6.3.0: resolution: {integrity: sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==} engines: {node: '>=12'} From c60db1215e576addf13e54e14d654f120f8e343a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 14:18:13 +0800 Subject: [PATCH 056/206] Update @wordpress/hooks from ^2.12.3 to ^3.5.0 --- packages/js/components/package.json | 2 +- packages/js/data/package.json | 2 +- packages/js/explat/package.json | 2 +- packages/js/navigation/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- pnpm-lock.yaml | 32 +++++++++++++------------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/js/components/package.json b/packages/js/components/package.json index b9ec712baa5..cff938a6107 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -39,7 +39,7 @@ "@wordpress/deprecated": "^3.3.1", "@wordpress/dom": "^3.3.2", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", "@wordpress/icons": "^6.3.0", diff --git a/packages/js/data/package.json b/packages/js/data/package.json index 1308e6827d0..f77d1322bae 100644 --- a/packages/js/data/package.json +++ b/packages/js/data/package.json @@ -30,7 +30,7 @@ "@wordpress/data-controls": "^2.3.2", "@wordpress/deprecated": "^3.3.1", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/i18n": "^4.3.1", "@wordpress/url": "^3.4.1", "dompurify": "^2.3.6", diff --git a/packages/js/explat/package.json b/packages/js/explat/package.json index 533172d328f..91de23b69c3 100644 --- a/packages/js/explat/package.json +++ b/packages/js/explat/package.json @@ -28,7 +28,7 @@ "@automattic/explat-client": "^0.0.3", "@automattic/explat-client-react-helpers": "^0.0.4", "@wordpress/api-fetch": "^6.0.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "cookie": "^0.4.2", "qs": "^6.10.3" }, diff --git a/packages/js/navigation/package.json b/packages/js/navigation/package.json index c4b578c2cc6..af94fb36e60 100644 --- a/packages/js/navigation/package.json +++ b/packages/js/navigation/package.json @@ -25,7 +25,7 @@ "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/notices": "^3.3.2", "@wordpress/url": "^3.4.1", "history": "^4.10.1", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 17ff33bf8e1..993949bf8c9 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -107,7 +107,7 @@ "@wordpress/dom": "^3.3.2", "@wordpress/dom-ready": "^3.3.1", "@wordpress/element": "^4.1.1", - "@wordpress/hooks": "^2.12.3", + "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", "@wordpress/icons": "^6.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 661111ef532..acb4d13c849 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,7 +177,7 @@ importers: '@wordpress/dom': ^3.3.2 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 '@wordpress/icons': ^6.3.0 @@ -230,7 +230,7 @@ importers: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 6.3.0 @@ -428,7 +428,7 @@ importers: '@wordpress/deprecated': ^3.3.1 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/i18n': ^4.3.1 '@wordpress/url': ^3.4.1 dompurify: ^2.3.6 @@ -451,7 +451,7 @@ importers: '@wordpress/data-controls': 2.4.1 '@wordpress/deprecated': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/url': 3.5.1 dompurify: 2.3.6 @@ -795,7 +795,7 @@ importers: '@types/qs': ^6.9.7 '@wordpress/api-fetch': ^6.0.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 cookie: ^0.4.2 eslint: ^8.12.0 jest: ^27.5.1 @@ -808,7 +808,7 @@ importers: '@automattic/explat-client': 0.0.3 '@automattic/explat-client-react-helpers': 0.0.4 '@wordpress/api-fetch': 6.1.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 cookie: 0.4.2 qs: 6.10.3 devDependencies: @@ -866,7 +866,7 @@ importers: '@wordpress/compose': ^5.1.2 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/notices': ^3.3.2 '@wordpress/url': ^3.4.1 eslint: ^8.12.0 @@ -882,7 +882,7 @@ importers: '@wordpress/components': 19.6.1_@babel+core@7.17.8 '@wordpress/compose': 5.2.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/notices': 3.4.1 '@wordpress/url': 3.5.1 history: 4.10.1 @@ -1242,7 +1242,7 @@ importers: '@wordpress/dom-ready': ^3.3.1 '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 - '@wordpress/hooks': ^2.12.3 + '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 '@wordpress/icons': ^6.3.0 @@ -1359,7 +1359,7 @@ importers: '@wordpress/dom': 3.4.1 '@wordpress/dom-ready': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 2.12.3 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 6.3.0 @@ -13983,7 +13983,7 @@ packages: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14012,7 +14012,7 @@ packages: '@wordpress/deprecated': 3.4.1 '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14062,7 +14062,7 @@ packages: '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 '@wordpress/escape-html': 2.4.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 8.0.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14115,7 +14115,7 @@ packages: '@wordpress/dom': 3.4.1 '@wordpress/element': 4.2.1 '@wordpress/escape-html': 2.4.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/i18n': 4.4.1 '@wordpress/icons': 8.0.1 '@wordpress/is-shallow-equal': 4.4.1 @@ -14364,7 +14364,7 @@ packages: engines: {node: '>=12'} dependencies: '@babel/runtime': 7.17.7 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 dev: false /@wordpress/dom-ready/3.4.1: @@ -14973,7 +14973,7 @@ packages: '@babel/runtime': 7.17.7 '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/element': 4.2.1 - '@wordpress/hooks': 3.4.1 + '@wordpress/hooks': 3.5.0 '@wordpress/icons': 8.0.1 lodash: 4.17.21 memize: 1.1.0 From 317dd7c9e92c397214f21c639e4f36e111b7a571 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 14:21:33 +0800 Subject: [PATCH 057/206] Update @wordpress/base-styles from ^3.6.0 to ^4.3.0 --- packages/js/style-build/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/style-build/package.json b/packages/js/style-build/package.json index 96926105d6c..2d7cbe02f56 100644 --- a/packages/js/style-build/package.json +++ b/packages/js/style-build/package.json @@ -19,7 +19,7 @@ "main": "index.js", "dependencies": { "@automattic/color-studio": "^2.5.0", - "@wordpress/base-styles": "^3.6.0", + "@wordpress/base-styles": "^4.3.0", "@wordpress/postcss-plugins-preset": "^1.6.0", "css-loader": "^3.6.0", "mini-css-extract-plugin": "^2.6.0", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 993949bf8c9..07c026f3e66 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -98,7 +98,7 @@ "@woocommerce/e2e-utils": "^0.2.0", "@wordpress/a11y": "^3.5.0", "@wordpress/api-fetch": "^6.0.1", - "@wordpress/base-styles": "^3.6.0", + "@wordpress/base-styles": "^4.3.0", "@wordpress/components": "^19.5.0", "@wordpress/compose": "^5.1.2", "@wordpress/core-data": "^4.1.2", From 344338704501af8315dc18434604b716187d89f1 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 14:24:19 +0800 Subject: [PATCH 058/206] Update @wordpress/icons from ^6.3.0 to ^8.1.0 --- packages/js/components/package.json | 2 +- packages/js/experimental/package.json | 2 +- plugins/woocommerce-admin/package.json | 2 +- pnpm-lock.yaml | 73 +++++++++++++++++++------- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/js/components/package.json b/packages/js/components/package.json index cff938a6107..5246dda39aa 100644 --- a/packages/js/components/package.json +++ b/packages/js/components/package.json @@ -42,7 +42,7 @@ "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "@wordpress/url": "^3.4.1", "@wordpress/viewport": "^4.1.2", diff --git a/packages/js/experimental/package.json b/packages/js/experimental/package.json index 8a7530f6085..225a29476f3 100644 --- a/packages/js/experimental/package.json +++ b/packages/js/experimental/package.json @@ -29,7 +29,7 @@ "@wordpress/components": "^19.5.0", "@wordpress/element": "^4.1.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "classnames": "^2.3.1", "dompurify": "^2.3.6", diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 07c026f3e66..0b26f2c91dd 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -110,7 +110,7 @@ "@wordpress/hooks": "^3.5.0", "@wordpress/html-entities": "^3.3.1", "@wordpress/i18n": "^4.3.1", - "@wordpress/icons": "^6.3.0", + "@wordpress/icons": "^8.1.0", "@wordpress/keycodes": "^3.3.1", "@wordpress/notices": "^3.3.2", "@wordpress/plugins": "^4.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acb4d13c849..e5b72f2030c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,7 +180,7 @@ importers: '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/keycodes': ^3.3.1 '@wordpress/scripts': ^12.6.1 '@wordpress/url': ^3.4.1 @@ -233,7 +233,7 @@ importers: '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 '@wordpress/url': 3.5.1 '@wordpress/viewport': 4.2.1 @@ -725,7 +725,7 @@ importers: '@wordpress/element': ^4.1.1 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/keycodes': ^3.3.1 classnames: ^2.3.1 concurrently: ^7.0.0 @@ -750,7 +750,7 @@ importers: '@wordpress/components': 19.6.1_@babel+core@7.17.8 '@wordpress/element': 4.2.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 classnames: 2.3.1 dompurify: 2.3.6 @@ -1007,7 +1007,7 @@ importers: specifiers: '@automattic/color-studio': ^2.5.0 '@babel/core': ^7.17.5 - '@wordpress/base-styles': ^3.6.0 + '@wordpress/base-styles': ^4.3.0 '@wordpress/eslint-plugin': ^11.0.0 '@wordpress/postcss-plugins-preset': ^1.6.0 css-loader: ^3.6.0 @@ -1025,7 +1025,7 @@ importers: webpack-rtl-plugin: ^2.0.0 dependencies: '@automattic/color-studio': 2.5.0 - '@wordpress/base-styles': 3.6.0 + '@wordpress/base-styles': 4.3.0 '@wordpress/postcss-plugins-preset': 1.6.0 css-loader: 3.6.0_webpack@5.70.0 mini-css-extract-plugin: 2.6.0_webpack@5.70.0 @@ -1230,7 +1230,7 @@ importers: '@wordpress/api-fetch': ^6.0.1 '@wordpress/babel-plugin-makepot': ^2.1.3 '@wordpress/babel-preset-default': ^6.5.1 - '@wordpress/base-styles': ^3.6.0 + '@wordpress/base-styles': ^4.3.0 '@wordpress/browserslist-config': ^4.1.1 '@wordpress/components': ^19.5.0 '@wordpress/compose': ^5.1.2 @@ -1245,7 +1245,7 @@ importers: '@wordpress/hooks': ^3.5.0 '@wordpress/html-entities': ^3.3.1 '@wordpress/i18n': ^4.3.1 - '@wordpress/icons': ^6.3.0 + '@wordpress/icons': ^8.1.0 '@wordpress/jest-preset-default': ^8.0.1 '@wordpress/keycodes': ^3.3.1 '@wordpress/notices': ^3.3.2 @@ -1350,7 +1350,7 @@ importers: '@woocommerce/e2e-utils': link:../../packages/js/e2e-utils '@wordpress/a11y': 3.5.0 '@wordpress/api-fetch': 6.1.1 - '@wordpress/base-styles': 3.6.0 + '@wordpress/base-styles': 4.3.0 '@wordpress/components': 19.6.1_978f344c876a57c1143ffe356b90df31 '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/core-data': 4.2.1_react@17.0.2 @@ -1362,7 +1362,7 @@ importers: '@wordpress/hooks': 3.5.0 '@wordpress/html-entities': 3.4.1 '@wordpress/i18n': 4.4.1 - '@wordpress/icons': 6.3.0 + '@wordpress/icons': 8.1.0 '@wordpress/keycodes': 3.4.1 '@wordpress/notices': 3.4.1_react@17.0.2 '@wordpress/plugins': 4.2.1_react@17.0.2 @@ -13954,6 +13954,10 @@ packages: /@wordpress/base-styles/3.6.0: resolution: {integrity: sha512-6/vXAmc9FSX7Y17UjKgUJoVU++Pv1U1G8uMx7iClRUaLetc7/jj2DD9PTyX/cdJjHr32e3yXuLVT9wfEbo6SEg==} + /@wordpress/base-styles/4.3.0: + resolution: {integrity: sha512-e9Z+txhEQ3zyAHkzzsuYg1ADFhKArz1eGU3ayqCNtCdakrgNjI6Q/sPODI26LlwTmjJPBIJ5wSCBrsDjMhdWqA==} + dev: false + /@wordpress/blob/3.4.1: resolution: {integrity: sha512-rGm7nXaxnsXStIu9v9IjbUOKtE9UzkvgYiJMX5SyVyzAGLOo2Aq759+JNRDLRR0RDkS6igH/G7qBXS6xSgLFgA==} engines: {node: '>=12'} @@ -14492,6 +14496,19 @@ packages: react: 17.0.2 react-dom: 17.0.2_react@17.0.2 + /@wordpress/element/4.3.0: + resolution: {integrity: sha512-QN5qsNN6kzbHgrCL9CG2877iOu01KMEwls1K3iKk43EQ8hr/D/Ms/h5TqfOgF6oIGUR/QUlbeZQJs4zdvEnFOg==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@types/react': 17.0.40 + '@types/react-dom': 17.0.13 + '@wordpress/escape-html': 2.5.0 + lodash: 4.17.21 + react: 17.0.2 + react-dom: 17.0.2_react@17.0.2 + dev: false + /@wordpress/escape-html/1.12.2: resolution: {integrity: sha512-FabgSwznhdaUwe6hr1CsGpgxQbzqEoGevv73WIL1B9GvlZ6csRWodgHfWh4P6fYqpzxFL4WYB8wPJ1PdO32XFA==} dependencies: @@ -14511,6 +14528,13 @@ packages: dependencies: '@babel/runtime': 7.17.7 + /@wordpress/escape-html/2.5.0: + resolution: {integrity: sha512-WV4jI6uBPZNxxOQdftiOsx1WgimkjxnwCfx6T+K7Ltfnm78Q5q2P5R98twGOqSVI/rPqtZubv9e7oMDbpp4H2w==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + dev: false + /@wordpress/eslint-plugin/11.0.1_2205da2c9bef219d53091cb9dbc5524c: resolution: {integrity: sha512-HDKwKjOmCaWdyJEtWKRAd0xK/NAXL/ykUP/I8l+zCvzvCXbS1UuixWN09RRzl09tv17JUtPiEqehDilkWRCBZg==} engines: {node: '>=12', npm: '>=6.9'} @@ -14772,15 +14796,6 @@ packages: tannin: 1.2.0 dev: false - /@wordpress/icons/6.3.0: - resolution: {integrity: sha512-Vliw7QsFuTsrA05GZov4i3PQiLQOGO97PR2keUeY53fVZdeoJKv/nfDqOZxZCIts5jR2Mfje6P6hc/KlurxsKg==} - engines: {node: '>=12'} - dependencies: - '@babel/runtime': 7.17.7 - '@wordpress/element': 4.2.1 - '@wordpress/primitives': 3.2.1 - dev: false - /@wordpress/icons/8.0.1: resolution: {integrity: sha512-+K0yWNSMR6d/Q/Zlixw6I7/s8UIeMFuJUET3LhprMGLO3K+t+o/8xqfgXAMf4GiVkc0YV+kXOuCsMMXFFwzi+A==} engines: {node: '>=12'} @@ -14790,6 +14805,15 @@ packages: '@wordpress/primitives': 3.2.1 dev: false + /@wordpress/icons/8.1.0: + resolution: {integrity: sha512-fNq0Mnzzf03uxIwKqQeU/G48wElyypwkhcBZWYQRpmwLZrOR231dxUeK9mzPOSGlYbgM+YKK+k3lzysGmrJU0A==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/element': 4.3.0 + '@wordpress/primitives': 3.3.0 + dev: false + /@wordpress/is-shallow-equal/4.4.1: resolution: {integrity: sha512-NlcqqrukKe4zT5fCs3O5FVYwqmHhtqM//KqWs7xfIaoz9B07oKZQNZqOrU72mgz7mgRliQumTQHzFM76RO0hZQ==} engines: {node: '>=12'} @@ -14974,7 +14998,7 @@ packages: '@wordpress/compose': 5.2.1_react@17.0.2 '@wordpress/element': 4.2.1 '@wordpress/hooks': 3.5.0 - '@wordpress/icons': 8.0.1 + '@wordpress/icons': 8.1.0 lodash: 4.17.21 memize: 1.1.0 react: 17.0.2 @@ -15027,6 +15051,15 @@ packages: classnames: 2.3.1 dev: false + /@wordpress/primitives/3.3.0: + resolution: {integrity: sha512-iwlFGSaI2RnQF0SxsWJ3KaM0LPdUosI5mb9879JXOh/vAFVObrQdyk5Fv+++vGUzDfxRnxAH68UpJi7nOzcRRA==} + engines: {node: '>=12'} + dependencies: + '@babel/runtime': 7.17.7 + '@wordpress/element': 4.3.0 + classnames: 2.3.1 + dev: false + /@wordpress/priority-queue/2.4.1: resolution: {integrity: sha512-5+pyUvQCQTTkoiccnO5G6AUDxzCKdAiDh3oLbl+qLz3j56iGuLoKWR6L5ySj+knaYIZb4g8expFsbvf2+RcVtw==} engines: {node: '>=12'} From a1a63e943382d03cdb7c379cb3f83f05fcbaf045 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 14:34:42 +0800 Subject: [PATCH 059/206] Update package changelogs --- packages/js/components/CHANGELOG.md | 3 +++ packages/js/data/CHANGELOG.md | 2 ++ packages/js/experimental/CHANGELOG.md | 2 ++ packages/js/explat/CHANGELOG.md | 2 ++ packages/js/navigation/CHANGELOG.md | 2 ++ packages/js/notices/CHANGELOG.md | 2 ++ 6 files changed, 13 insertions(+) diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index 798bde85839..1453039b5d5 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 +- Update dependency `@wordpress/icons` to ^8.1.0 + # 10.0.0 - Replace deprecated wp.compose.withState with wp.element.useState. #8338 - Add missing dependencies. #8349 diff --git a/packages/js/data/CHANGELOG.md b/packages/js/data/CHANGELOG.md index d7acf37a3dd..cb955c6b7e2 100644 --- a/packages/js/data/CHANGELOG.md +++ b/packages/js/data/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 + # 3.1.0 - Add "moment" to peerDependencies. #8349 diff --git a/packages/js/experimental/CHANGELOG.md b/packages/js/experimental/CHANGELOG.md index 1e9c72b068d..c43445f78c6 100644 --- a/packages/js/experimental/CHANGELOG.md +++ b/packages/js/experimental/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/icons` to ^8.1.0 + # 3.0.1 - Update all js packages with minor/patch version changes. #8392 diff --git a/packages/js/explat/CHANGELOG.md b/packages/js/explat/CHANGELOG.md index 9a709dc19f6..1c2faa25278 100644 --- a/packages/js/explat/CHANGELOG.md +++ b/packages/js/explat/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 + # 2.1.0 - Add missing dependencies. #8349 diff --git a/packages/js/navigation/CHANGELOG.md b/packages/js/navigation/CHANGELOG.md index b90c2da966b..81fd1afc1cd 100644 --- a/packages/js/navigation/CHANGELOG.md +++ b/packages/js/navigation/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update dependency `@wordpress/hooks` to ^3.5.0 + # 7.0.1 - Add missing dependencies. #8349 diff --git a/packages/js/notices/CHANGELOG.md b/packages/js/notices/CHANGELOG.md index c738abfb1d1..e0a22a3fc5c 100644 --- a/packages/js/notices/CHANGELOG.md +++ b/packages/js/notices/CHANGELOG.md @@ -2,6 +2,8 @@ # Unreleased +- Update dependency `@wordpress/a11y` to ^3.5.0 + # 4.0.1 - Update all js packages with minor/patch version changes. #8392 From c4033b14de093d5d7028dc5f8648cdcd667e0276 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Thu, 7 Apr 2022 17:21:37 +0800 Subject: [PATCH 060/206] Remove unnecessary --working-dir arg from woocommerce php lint-staged command --- plugins/woocommerce/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index 6ae8e8e0ace..079fd9ccd4f 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -72,7 +72,7 @@ "lint-staged": { "*.php": [ "php -d display_errors=1 -l", - "composer --working-dir=./plugins/woocommerce run-script phpcs-pre-commit" + "composer run-script phpcs-pre-commit" ], "!(*min).js": [ "eslint --fix" From 473d63d043a2a925b90a142454c4dd3e20fd9dd4 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 7 Apr 2022 16:58:21 -0300 Subject: [PATCH 061/206] Remove can_view condition on tracking completion --- .../woocommerce/src/Admin/Features/OnboardingTasks/Task.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php index f6300aa5ea1..df315157545 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Task.php @@ -358,10 +358,6 @@ abstract class Task { * Track task completion if task is viewable. */ public function possibly_track_completion() { - if ( ! $this->can_view() ) { - return; - } - if ( ! $this->is_complete() ) { return; } From 3159bde89501af06c7fbe5389510f34a4e13bc29 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 7 Apr 2022 18:07:00 -0300 Subject: [PATCH 062/206] Add Pinterest to free extensions list --- packages/js/data/src/plugins/constants.ts | 4 ++++ plugins/woocommerce/src/Admin/API/OnboardingProfile.php | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/js/data/src/plugins/constants.ts b/packages/js/data/src/plugins/constants.ts index 1050edb792c..50fbb1985f2 100644 --- a/packages/js/data/src/plugins/constants.ts +++ b/packages/js/data/src/plugins/constants.ts @@ -55,4 +55,8 @@ export const pluginNames = { 'google-listings-and-ads': __( 'Google Listings and Ads', 'woocommerce' ), 'woo-razorpay': __( 'Razorpay', 'woocommerce' ), mailpoet: __( 'MailPoet', 'woocommerce' ), + 'pinterest-for-woocommerce': __( + 'Pinterest for WooCommerce', + 'woocommerce' + ), }; diff --git a/plugins/woocommerce/src/Admin/API/OnboardingProfile.php b/plugins/woocommerce/src/Admin/API/OnboardingProfile.php index a148810ed2e..d30956054d3 100644 --- a/plugins/woocommerce/src/Admin/API/OnboardingProfile.php +++ b/plugins/woocommerce/src/Admin/API/OnboardingProfile.php @@ -397,6 +397,7 @@ class OnboardingProfile extends \WC_REST_Data_Controller { 'creative-mail-by-constant-contact', 'facebook-for-woocommerce', 'google-listings-and-ads', + 'pinterest-for-woocommerce', 'mailpoet', ), 'type' => 'string', From 63f33b86dee6630febb35e8ab6f49d60bb2bc3b4 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 7 Apr 2022 18:07:47 -0300 Subject: [PATCH 063/206] Add free extensions list fallback --- .../DefaultFreeExtensions.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index 6ec1c56dccb..1134defbd16 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -36,6 +36,7 @@ class DefaultFreeExtensions { 'plugins' => [ self::get_plugin( 'mailpoet' ), self::get_plugin( 'google-listings-and-ads' ), + self::get_plugin( 'pinterest-for-woocommerce' ), ], ], [ @@ -52,6 +53,7 @@ class DefaultFreeExtensions { 'title' => __( 'Grow your store', 'woocommerce' ), 'plugins' => [ self::get_plugin( 'google-listings-and-ads:alt' ), + self::get_plugin( 'pinterest-for-woocommerce:alt' ), ], ], ]; @@ -98,6 +100,36 @@ class DefaultFreeExtensions { 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, ], + 'pinterest-for-woocommerce' => [ + 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), + 'description' => sprintf( + /* translators: 1: opening product link tag. 2: closing link tag */ + __( 'aaaa Inspire shoppers with %1$sPinterest for WooCommerce%2$s', 'woocommerce' ), + '', + '' + ), + 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'manage_url' => 'admin.php?page=pinterest-for-woocommerce', + 'is_visible' => [ + [ + 'type' => 'not', + 'operand' => [ + [ + 'type' => 'plugins_activated', + 'plugins' => [ 'pinterest-for-woocommerce' ], + ], + ], + ], + ], + 'is_built_by_wc' => false, + ], + 'pinterest-for-woocommerce:alt' => [ + 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), + 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce-admin' ), + 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', + 'is_built_by_wc' => false, + ], 'mailpoet' => [ 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), From a649fe6ae275d68244507233251305e1b727d0de Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 5 Apr 2022 14:30:22 +0800 Subject: [PATCH 064/206] Remove no needed commands and fields in package.json --- plugins/woocommerce-admin/package.json | 35 +++++--------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 759909df858..a5da4f8a2d0 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -1,22 +1,13 @@ { "name": "@woocommerce/admin-library", "version": "3.3.0", - "homepage": "https://woocommerce.github.io/woocommerce-admin/", - "repository": { - "type": "git", - "url": "https://github.com:woocommerce/woocommerce-admin.git" - }, "license": "GPL-3.0-or-later", "author": "Automattic", - "files": [ - "dist/**/*.css", - "dist/**/*.js", - "dist/feature-config-core.php", - "includes/class-wc-admin-loader.php", - "includes/features/**/*.php", - "languages/**/*.json", - "license.txt" - ], + "homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-admin/README.md", + "repository": { + "type": "git", + "url": "https://github.com:woocommerce/woocommerce.git" + }, "scripts": { "preinstall": "npx only-allow pnpm", "prebuild": "pnpm run install-if-deps-outdated", @@ -27,7 +18,6 @@ "postbuild": "pnpm run -s i18n:pot && pnpm run -s i18n:build", "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", - "build:release": "./bin/build-plugin-zip.sh", "clean": "rimraf ./dist && pnpm run:packages -- clean --parallel", "predev": "pnpm run -s install-if-deps-outdated", "dev": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development webpack", @@ -42,8 +32,6 @@ "i18n:pot": "grunt makepot", "install-if-deps-outdated": "node bin/install-if-deps-outdated.js", "install-if-no-packages": "node bin/install-if-no-packages.js", - "labels:dry": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels --dry-run woocommerce/woocommerce-admin", - "labels:sync": "github-label-sync --labels ./.github/label-sync-config.json --allow-added-labels woocommerce/woocommerce-admin", "lint": "pnpm run lint:js && pnpm run lint:css", "lint:css": "stylelint '**/*.scss'", "lint:css-fix": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", @@ -51,8 +39,6 @@ "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", "lint:js-fix": "pnpm run lint:js -- --fix --ext=js,ts,tsx", - "lint:php": "./vendor/bin/phpcs --standard=phpcs.xml.dist $(git ls-files | grep .php$)", - "lint:php-fix": "./vendor/bin/phpcbf --standard=phpcs.xml.dist $(git ls-files | grep .php$)", "ts:check": "tsc --build ./tsconfig.json --pretty", "ts:check:watch": "npm run ts:check -- --watch", "reformat-files": "wp-scripts format-js -- --ignore-path .eslintignore", @@ -68,26 +54,17 @@ "test:client": "jest --config client/jest.config.js", "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", "test": "pnpm nx build @woocommerce/js-tests && pnpm run test:client", - "test:e2e": "pnpm run build && test -z \"$(docker ps | grep woocommerce-admin-e2e)\" || pnpm exec wc-e2e docker:down && pnpm run e2e:docker-up && pnpm exec wc-e2e test:e2e", - "e2e:docker-up": "WC_E2E_FOLDER=../../../ pnpm exec wc-e2e docker:up ./tests/e2e/docker/initialize.sh", "test-staged": "pnpm run test:client -- --bail --findRelatedTests", "test:help": "wp-scripts test-unit-js --help", - "test:php": "docker-compose -f docker/wc-admin-php-test-suite/docker-compose.yml run --rm phpunit", - "posttest:php": "docker-compose -f docker/wc-admin-php-test-suite/docker-compose.yml down", "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", "test:watch": "tsc --build || concurrently \"pnpm run test:client -- --watch\" \"pnpm run:packages -- test:nobuild --parallel -- --watch\"", - "test:zip": "pnpm run clean && composer i && ./bin/build-test-zip.sh", "example": "webpack --config docs/examples/extensions/examples.config.js --watch", "pre-release": "./bin/pre-release.sh", "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", "storybook": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true start-storybook -c ./storybook/.storybook -p 6007 --ci", "storybook-rtl": "USE_RTL_STYLE=true pnpm run storybook", "build-storybook": "build-storybook -c ./storybook/.storybook", - "changelog": "node ./bin/changelog --changelogSrcType='ZENHUB_RELEASE'", - "wp-env-mysql-port": "node ./docker/wc-admin-wp-env/mysql-port.js", - "create-hook-reference": "node ./bin/hook-reference/index.js", - "changelogger": "./vendor/bin/changelogger", - "test-instruction-logger": "./bin/test-instruction-logger/bin/test-instruction-logger" + "create-hook-reference": "node ./bin/hook-reference/index.js" }, "dependencies": { "@automattic/explat-client": "^0.0.3", From 870d52345528115e49cd50a647f3f04f78d399b6 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 5 Apr 2022 14:31:13 +0800 Subject: [PATCH 065/206] Reorder woocommerce admin package.json scripts --- plugins/woocommerce-admin/package.json | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index a5da4f8a2d0..1f1719ec77f 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -9,62 +9,62 @@ "url": "https://github.com:woocommerce/woocommerce.git" }, "scripts": { - "preinstall": "npx only-allow pnpm", - "prebuild": "pnpm run install-if-deps-outdated", - "run:packages": "pnpm run --filter ../../packages/js/", - "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "build": "pnpm run build:feature-config && cross-env NODE_ENV=production webpack", "analyze": "cross-env NODE_ENV=production ANALYZE=true webpack", + "prebuild": "pnpm run install-if-deps-outdated", + "build": "pnpm run build:feature-config && cross-env NODE_ENV=production webpack", "postbuild": "pnpm run -s i18n:pot && pnpm run -s i18n:build", + "build-storybook": "build-storybook -c ./storybook/.storybook", "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", "clean": "rimraf ./dist && pnpm run:packages -- clean --parallel", + "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", + "create-hook-reference": "node ./bin/hook-reference/index.js", + "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", "predev": "pnpm run -s install-if-deps-outdated", "dev": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development webpack", - "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", - "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", "docs": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true pnpm exec build-storybook -c storybook/.storybook -o ./docs/components/storybook", + "example": "webpack --config docs/examples/extensions/examples.config.js --watch", "i18n": "pnpm run -s i18n:js && pnpm run -s i18n:check && pnpm run -s i18n:pot && pnpm run -s i18n:build", "i18n:build": "php bin/combine-pot-files.php languages/woocommerce-admin.po languages/woocommerce-admin.pot", "i18n:check": "grunt checktextdomain", "i18n:js": "pnpm run clean && cross-env NODE_ENV=production babel client packages -o /dev/null", "i18n:json": "./bin/make-i18n-json.sh", "i18n:pot": "grunt makepot", + "preinstall": "npx only-allow pnpm", "install-if-deps-outdated": "node bin/install-if-deps-outdated.js", "install-if-no-packages": "node bin/install-if-no-packages.js", "lint": "pnpm run lint:js && pnpm run lint:css", "lint:css": "stylelint '**/*.scss'", "lint:css-fix": "stylelint '**/*.scss' --fix --ip 'storybook/wordpress'", "lint:js": "wp-scripts lint-js ./client --ext=js,ts,tsx", - "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", - "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", "lint:js-fix": "pnpm run lint:js -- --fix --ext=js,ts,tsx", - "ts:check": "tsc --build ./tsconfig.json --pretty", - "ts:check:watch": "npm run ts:check -- --watch", - "reformat-files": "wp-scripts format-js -- --ignore-path .eslintignore", + "lint:js-packages": "wp-scripts lint-js ../../packages/js --ext=js,ts,tsx", + "lint:js-pre-commit": "wp-scripts lint-js --ext=js,ts,tsx", "prepack": "pnpm install && pnpm run lint && pnpm run test && cross-env WC_ADMIN_PHASE=core pnpm run build", + "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", + "packages:watch": "cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel", + "pre-release": "./bin/pre-release.sh", "publish-packages:check": "pnpm run build:packages && pnpm publish --dry-run --filter ../../packages/js/ --publish-branch main --report-summary && cat ../../pnpm-publish-summary.json && rimraf ../../pnpm-publish-summary.json", "publish-packages:dev": "pnpm run build:packages && pnpm publish --filter ../../packages/js/ --publish-branch main --tag next", "publish-packages:prod": "pnpm run build:packages && pnpm publish --filter ../../packages/js/ --publish-branch main", + "reformat-files": "wp-scripts format-js -- --ignore-path .eslintignore", + "run:packages": "pnpm run --filter ../../packages/js/", "prestart": "pnpm run install-if-deps-outdated", "start": "cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && concurrently \"cross-env WC_ADMIN_PHASE=development webpack --watch\" \"cross-env WC_ADMIN_PHASE=development pnpm run:packages -- start --parallel\"", "start:package": "pnpm run:packages -- start --parallel", - "pretest": "pnpm run -s install-if-no-packages", - "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", - "test:client": "jest --config client/jest.config.js", - "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", - "test": "pnpm nx build @woocommerce/js-tests && pnpm run test:client", - "test-staged": "pnpm run test:client -- --bail --findRelatedTests", - "test:help": "wp-scripts test-unit-js --help", - "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", - "test:watch": "tsc --build || concurrently \"pnpm run test:client -- --watch\" \"pnpm run:packages -- test:nobuild --parallel -- --watch\"", - "example": "webpack --config docs/examples/extensions/examples.config.js --watch", - "pre-release": "./bin/pre-release.sh", - "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", "storybook": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true start-storybook -c ./storybook/.storybook -p 6007 --ci", "storybook-rtl": "USE_RTL_STYLE=true pnpm run storybook", - "build-storybook": "build-storybook -c ./storybook/.storybook", - "create-hook-reference": "node ./bin/hook-reference/index.js" + "pretest": "pnpm run -s install-if-no-packages", + "test": "pnpm nx build @woocommerce/js-tests && pnpm run test:client", + "test-staged": "pnpm run test:client -- --bail --findRelatedTests", + "test:client": "jest --config client/jest.config.js", + "test:debug": "node --inspect-brk ./node_modules/.bin/jest --config client/jest.config.js --watch --runInBand --no-cache", + "test:help": "wp-scripts test-unit-js --help", + "test:packages": "pnpm run --filter ../../packages/js/ --filter !api-core-tests test", + "test:update-snapshots": "pnpm run test:client -- --updateSnapshot && pnpm run --filter @woocommerce/components test:update-snapshots", + "test:watch": "tsc --build || concurrently \"pnpm run test:client -- --watch\" \"pnpm run:packages -- test:nobuild --parallel -- --watch\"", + "ts:check": "tsc --build ./tsconfig.json --pretty", + "ts:check:watch": "npm run ts:check -- --watch" }, "dependencies": { "@automattic/explat-client": "^0.0.3", From f3e460f055ee52eac824ad0b67ba4a318ceb3e60 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 8 Apr 2022 12:35:00 +0800 Subject: [PATCH 066/206] Remove i18 commands and their dependencies i18 routines have been handled inside plugins/woocommerce. --- plugins/woocommerce-admin/Gruntfile.js | 62 ------ plugins/woocommerce-admin/babel.config.js | 11 +- .../bin/combine-pot-files.php | 104 ---------- .../woocommerce-admin/bin/make-i18n-json.sh | 23 --- plugins/woocommerce-admin/languages/README.md | 22 --- plugins/woocommerce-admin/package.json | 11 -- .../src/Internal/Admin/FeaturePlugin.php | 9 - pnpm-lock.yaml | 183 ++---------------- 8 files changed, 19 insertions(+), 406 deletions(-) delete mode 100755 plugins/woocommerce-admin/Gruntfile.js delete mode 100644 plugins/woocommerce-admin/bin/combine-pot-files.php delete mode 100755 plugins/woocommerce-admin/bin/make-i18n-json.sh delete mode 100644 plugins/woocommerce-admin/languages/README.md diff --git a/plugins/woocommerce-admin/Gruntfile.js b/plugins/woocommerce-admin/Gruntfile.js deleted file mode 100755 index 3f0fd4dbdbf..00000000000 --- a/plugins/woocommerce-admin/Gruntfile.js +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable */ -module.exports = function ( grunt ) { - 'use strict'; - - // Project configuration - grunt.initConfig( { - makepot: { - target: { - options: { - domainPath: '/languages', - exclude: [ '.git/*', 'bin/*', 'node_modules/*', 'tests/*' ], - mainFile: '../woocommerce/woocommerce.php', - potFilename: 'woocommerce-admin.pot', - potHeaders: { - poedit: true, - 'x-poedit-keywordslist': true, - }, - type: 'wp-plugin', - updateTimestamp: true, - }, - }, - }, - - checktextdomain: { - options: { - text_domain: 'woocommerce', - keywords: [ - '__:1,2d', - '_e:1,2d', - '_x:1,2c,3d', - 'esc_html__:1,2d', - 'esc_html_e:1,2d', - 'esc_html_x:1,2c,3d', - 'esc_attr__:1,2d', - 'esc_attr_e:1,2d', - 'esc_attr_x:1,2c,3d', - '_ex:1,2c,3d', - '_n:1,2,4d', - '_nx:1,2,4c,5d', - '_n_noop:1,2,3d', - '_nx_noop:1,2,3c,4d', - ], - }, - files: { - src: [ - '**/*.php', // Include all files/ - '!node_modules/**', // Exclude node_modules/ - '!tests/**', // Exclude tests/ - '!vendor/**', // Exclude vendor/ - '!tmp/**', // Exclude tmp/ - ], - expand: true, - }, - }, - } ); - - // Load NPM tasks to be used here. - grunt.loadNpmTasks( 'grunt-wp-i18n' ); - grunt.loadNpmTasks( 'grunt-checktextdomain' ); - - grunt.util.linefeed = '\n'; -}; diff --git a/plugins/woocommerce-admin/babel.config.js b/plugins/woocommerce-admin/babel.config.js index 93a8add31f2..70215466892 100644 --- a/plugins/woocommerce-admin/babel.config.js +++ b/plugins/woocommerce-admin/babel.config.js @@ -22,16 +22,7 @@ module.exports = function ( api ) { ], ignore: [ 'packages/**/node_modules' ], env: { - production: { - plugins: [ - [ - '@wordpress/babel-plugin-makepot', - { - output: 'languages/woocommerce-admin.po', - }, - ], - ], - }, + production: {}, storybook: { plugins: [ diff --git a/plugins/woocommerce-admin/bin/combine-pot-files.php b/plugins/woocommerce-admin/bin/combine-pot-files.php deleted file mode 100644 index fa82f605ddf..00000000000 --- a/plugins/woocommerce-admin/bin/combine-pot-files.php +++ /dev/null @@ -1,104 +0,0 @@ - $original ) { - // Use the complete message section to match strings to be translated. - if ( isset( $originals_1[ $message ] ) ) { - $original = array_merge( $original, $originals_1[ $message ] ); - unset( $originals_1[ $message ] ); - } - - fwrite( $fh, implode( "\n", $original ) ); - fwrite( $fh, "\n" . $message ."\n\n" ); -} - -foreach ( $originals_1 as $message => $original ) { - fwrite( $fh, implode( "\n", $original ) ); - fwrite( $fh, "\n" . $message ."\n\n" ); -} - -fclose( $fh ); - -echo "Created {$target_file}\n"; diff --git a/plugins/woocommerce-admin/bin/make-i18n-json.sh b/plugins/woocommerce-admin/bin/make-i18n-json.sh deleted file mode 100755 index 5bc91bacef1..00000000000 --- a/plugins/woocommerce-admin/bin/make-i18n-json.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -# Check for required version -WPCLI_VERSION=`wp cli version | cut -f2 -d' '` -if [ ${WPCLI_VERSION:0:1} -lt "2" -o ${WPCLI_VERSION:0:1} -eq "2" -a ${WPCLI_VERSION:2:1} -lt "1" ]; then - echo WP-CLI version 2.1.0 or greater is required to make JSON translation files - exit -fi - -# Substitute JS source references with build references -for T in `find languages -name "*.po"` - do - sed \ - -e 's/ client\/[^:]*:/ dist\/app\/index.js:/gp' \ - -e 's/ packages\/components[^:]*:/ dist\/components\/index.js:/gp' \ - -e 's/ packages\/date[^:]*:/ dist\/date\/index.js:/gp' \ - $T | uniq > $T-build - rm $T - mv $T-build $T - done - -# Make the JSON files -wp i18n make-json languages --no-purge \ No newline at end of file diff --git a/plugins/woocommerce-admin/languages/README.md b/plugins/woocommerce-admin/languages/README.md deleted file mode 100644 index 4c0bdd32b0e..00000000000 --- a/plugins/woocommerce-admin/languages/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Languages - -## Contributing a Translation -If you'd like to contribute a translation, please follow the Localizing section in [CONTRIBUTING.md](https://github.com/woocommerce/woocommerce-admin/blob/main/CONTRIBUTING.md). - -## Generating POT - -The generated POT template file is not included in this repository. To create this file locally, follow instructions from [README.md](https://github.com/woocommerce/woocommerce-admin/blob/main/README.md) to install the project, then run the following command: - -``` -pnpm run i18n lang=xx_YY -``` - -After the build completes, you'll find a `woocommerce-admin-xx_YY.po` (eg. `woocommerce-admin-fr_FR.po`) strings file in this directory. - -## Generating JSON - -To generate JSON from your translations, save your translation file in this directory then run the following command: - -``` -pnpm run i18n:json -``` diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index 1f1719ec77f..d8270041af0 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -12,7 +12,6 @@ "analyze": "cross-env NODE_ENV=production ANALYZE=true webpack", "prebuild": "pnpm run install-if-deps-outdated", "build": "pnpm run build:feature-config && cross-env NODE_ENV=production webpack", - "postbuild": "pnpm run -s i18n:pot && pnpm run -s i18n:build", "build-storybook": "build-storybook -c ./storybook/.storybook", "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", @@ -24,12 +23,6 @@ "dev": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development pnpm run build:packages && cross-env WC_ADMIN_PHASE=development webpack", "docs": "./bin/import-wp-css-storybook.sh && BABEL_ENV=storybook STORYBOOK=true pnpm exec build-storybook -c storybook/.storybook -o ./docs/components/storybook", "example": "webpack --config docs/examples/extensions/examples.config.js --watch", - "i18n": "pnpm run -s i18n:js && pnpm run -s i18n:check && pnpm run -s i18n:pot && pnpm run -s i18n:build", - "i18n:build": "php bin/combine-pot-files.php languages/woocommerce-admin.po languages/woocommerce-admin.pot", - "i18n:check": "grunt checktextdomain", - "i18n:js": "pnpm run clean && cross-env NODE_ENV=production babel client packages -o /dev/null", - "i18n:json": "./bin/make-i18n-json.sh", - "i18n:pot": "grunt makepot", "preinstall": "npx only-allow pnpm", "install-if-deps-outdated": "node bin/install-if-deps-outdated.js", "install-if-no-packages": "node bin/install-if-no-packages.js", @@ -183,7 +176,6 @@ "@woocommerce/onboarding": "workspace:*", "@woocommerce/style-build": "workspace:*", "@woocommerce/tracks": "workspace:*", - "@wordpress/babel-plugin-makepot": "^2.1.3", "@wordpress/babel-preset-default": "^6.5.1", "@wordpress/browserslist-config": "^4.1.1", "@wordpress/custom-templated-path-webpack-plugin": "^2.1.2", @@ -216,9 +208,6 @@ "expose-loader": "^3.1.0", "fork-ts-checker-webpack-plugin": "^6.5.0", "fs-extra": "^8.1.0", - "grunt": "^1.4.1", - "grunt-checktextdomain": "^1.0.1", - "grunt-wp-i18n": "^1.0.3", "jest": "^27.5.1", "jest-environment-jsdom": "~27.5.0", "jest-environment-node": "^27.5.1", diff --git a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php index 98cd6b3ae4e..f914a0bfc32 100644 --- a/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php +++ b/plugins/woocommerce/src/Internal/Admin/FeaturePlugin.php @@ -126,8 +126,6 @@ class FeaturePlugin { * @return void */ public function on_plugins_loaded() { - $this->load_plugin_textdomain(); - $this->hooks(); $this->includes(); } @@ -152,13 +150,6 @@ class FeaturePlugin { define( 'WC_ADMIN_VERSION_NUMBER', '3.3.0' ); } - /** - * Load Localisation files. - */ - protected function load_plugin_textdomain() { - load_plugin_textdomain( 'woocommerce-admin', false, basename( dirname( __DIR__ ) ) . '/languages' ); - } - /** * Include WC Admin classes. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af82cec33c9..9baed0c32c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1228,7 +1228,6 @@ importers: '@woocommerce/tracks': workspace:* '@wordpress/a11y': ^3.5.0 '@wordpress/api-fetch': ^6.0.1 - '@wordpress/babel-plugin-makepot': ^2.1.3 '@wordpress/babel-preset-default': ^6.5.1 '@wordpress/base-styles': ^4.3.0 '@wordpress/browserslist-config': ^4.1.1 @@ -1288,9 +1287,6 @@ importers: github-label-sync: ^2.0.2 grapheme-splitter: ^1.0.4 gridicons: ^3.4.0 - grunt: ^1.4.1 - grunt-checktextdomain: ^1.0.1 - grunt-wp-i18n: ^1.0.3 history: ^4.10.1 jest: ^27.5.1 jest-environment-jsdom: ~27.5.0 @@ -1454,7 +1450,6 @@ importers: '@woocommerce/onboarding': link:../../packages/js/onboarding '@woocommerce/style-build': link:../../packages/js/style-build '@woocommerce/tracks': link:../../packages/js/tracks - '@wordpress/babel-plugin-makepot': 2.1.3_@babel+core@7.17.8 '@wordpress/babel-preset-default': 6.6.1 '@wordpress/browserslist-config': 4.1.2 '@wordpress/custom-templated-path-webpack-plugin': 2.1.2_webpack@5.70.0 @@ -1487,9 +1482,6 @@ importers: expose-loader: 3.1.0_webpack@5.70.0 fork-ts-checker-webpack-plugin: 6.5.0_10568ae13669cc833891d65cd6879aa0 fs-extra: 8.1.0 - grunt: 1.4.1 - grunt-checktextdomain: 1.0.1_grunt@1.4.1 - grunt-wp-i18n: 1.0.3 jest: 27.5.1 jest-environment-jsdom: 27.5.1 jest-environment-node: 27.5.1 @@ -2208,7 +2200,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.3 + debug: 4.3.2 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -2226,7 +2218,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.3 + debug: 4.3.2 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -2244,7 +2236,7 @@ packages: '@babel/helper-module-imports': 7.16.0 '@babel/helper-plugin-utils': 7.14.5 '@babel/traverse': 7.16.3 - debug: 4.3.3 + debug: 4.3.2 lodash.debounce: 4.0.8 resolve: 1.20.0 semver: 6.3.0 @@ -8600,7 +8592,7 @@ packages: react-refresh: 0.11.0 schema-utils: 3.1.1 source-map: 0.7.3 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /@pmmmwh/react-refresh-webpack-plugin/0.5.1_92cb4b81c6b9f71cf92f0bdb85e4210c: @@ -8988,7 +8980,7 @@ packages: peerDependencies: '@storybook/addon-actions': '*' dependencies: - '@storybook/addon-actions': 6.4.19 + '@storybook/addon-actions': 6.4.19_react-dom@17.0.2+react@17.0.2 global: 4.4.0 dev: true @@ -11301,7 +11293,7 @@ packages: react-docgen-typescript: 2.2.2_typescript@4.6.2 tslib: 2.3.1 typescript: 4.6.2 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 transitivePeerDependencies: - supports-color dev: true @@ -13275,7 +13267,7 @@ packages: dependencies: '@typescript-eslint/types': 5.4.0 '@typescript-eslint/visitor-keys': 5.4.0 - debug: 4.3.3 + debug: 4.3.2 globby: 11.0.4 is-glob: 4.0.3 semver: 7.3.5 @@ -13836,17 +13828,6 @@ packages: dependencies: '@babel/core': 7.17.8 - /@wordpress/babel-plugin-makepot/2.1.3_@babel+core@7.17.8: - resolution: {integrity: sha512-8ijU4bYUmJuXPnHS47X9Y5OrESLmgx3VVGb+9tNO5hyPoXnZj+ELw9+SB4fJtg0Ur1MDNKRLz4ruJS4Y0tRnNQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.17.8 - '@babel/runtime': 7.17.7 - gettext-parser: 1.4.0 - lodash: 4.17.21 - dev: true - /@wordpress/babel-preset-default/3.0.2: resolution: {integrity: sha512-bsa4piS4GU02isj2XJNUgSEC7MpzdYNy9wOFySrp8G6IHAvwrlwcPEXJf5EuwE8ZqTMmFAzPyKOHFEAx/j+J1A==} engines: {node: '>=8'} @@ -15670,11 +15651,6 @@ packages: engines: {node: '>=12'} dev: true - /ansi-styles/0.2.0: - resolution: {integrity: sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=} - engines: {node: '>=0.8.0'} - dev: true - /ansi-styles/2.2.1: resolution: {integrity: sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=} engines: {node: '>=0.10.0'} @@ -17657,14 +17633,6 @@ packages: type-detect: 4.0.8 dev: true - /chalk/0.2.1: - resolution: {integrity: sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=} - engines: {node: '>=0.8.0'} - dependencies: - ansi-styles: 0.2.0 - has-color: 0.1.7 - dev: true - /chalk/1.1.3: resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} engines: {node: '>=0.10.0'} @@ -22363,7 +22331,7 @@ packages: dependencies: loader-utils: 2.0.2 schema-utils: 3.1.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /file-loader/6.2.0_webpack@5.64.1: @@ -22570,16 +22538,6 @@ packages: resolve-dir: 1.0.1 dev: true - /findup-sync/4.0.0: - resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} - engines: {node: '>= 8'} - dependencies: - detect-file: 1.0.0 - is-glob: 4.0.3 - micromatch: 4.0.4 - resolve-dir: 1.0.1 - dev: true - /findup/0.1.5: resolution: {integrity: sha1-itkpozk7rGJ5V6fl3kYjsGsOLOs=} engines: {node: '>=0.6'} @@ -23215,14 +23173,6 @@ packages: encoding: 0.1.13 safe-buffer: 5.2.1 - /gettext-parser/3.1.1: - resolution: {integrity: sha512-vNhWcqXEtZPs5Ft1ReA34g7ByWotpcOIeJvXVy2jF3/G2U9v6W0wG4Z4hXzcU8R//jArqkgHcVCGgGqa4vxVlQ==} - dependencies: - encoding: 0.1.13 - readable-stream: 3.6.0 - safe-buffer: 5.2.1 - dev: true - /github-label-sync/2.0.2: resolution: {integrity: sha512-xDxlGG6s9LVfMNQexatne0bMUrwyYyTma9cC04b82zbEMFoy8rxSlag4eUYYF++ThMxvJp577Wk+uAv0mjRsNg==} engines: {node: '>=12'} @@ -23657,17 +23607,6 @@ packages: /growly/1.3.0: resolution: {integrity: sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=} - /grunt-checktextdomain/1.0.1_grunt@1.4.1: - resolution: {integrity: sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=} - engines: {node: '>= 0.8.0'} - peerDependencies: - grunt: '>=0.4.1' - dependencies: - chalk: 0.2.1 - grunt: 1.4.1 - text-table: 0.2.0 - dev: true - /grunt-cli/1.3.2: resolution: {integrity: sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==} engines: {node: '>=4'} @@ -23680,18 +23619,6 @@ packages: v8flags: 3.1.3 dev: true - /grunt-cli/1.4.3: - resolution: {integrity: sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==} - engines: {node: '>=10'} - hasBin: true - dependencies: - grunt-known-options: 2.0.0 - interpret: 1.1.0 - liftup: 3.0.1 - nopt: 4.0.3 - v8flags: 3.2.0 - dev: true - /grunt-contrib-clean/2.0.0_grunt@1.3.0: resolution: {integrity: sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==} engines: {node: '>=6'} @@ -23756,11 +23683,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /grunt-known-options/2.0.0: - resolution: {integrity: sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==} - engines: {node: '>=0.10.0'} - dev: true - /grunt-legacy-log-utils/2.1.0: resolution: {integrity: sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==} engines: {node: '>=10'} @@ -23855,14 +23777,6 @@ packages: stylelint: 13.8.0 dev: true - /grunt-wp-i18n/1.0.3: - resolution: {integrity: sha512-CJNbEKeBeOSAPeaJ9B8iCgSwtaG63UR9/uT46a4OsIqnFhOJpeAi138JTlvjfIbnDVoBrzvdrKJe1svveLjUtA==} - engines: {node: '>=0.12.0'} - dependencies: - grunt: 1.4.1 - node-wp-i18n: 1.2.6 - dev: true - /grunt/1.3.0: resolution: {integrity: sha512-6ILlMXv11/4cxuhSMfSU+SfvbxrPuqZrAtLN64+tZpQ3DAKfSQPQHRbTjSbdtxfyQhGZPtN0bDZJ/LdCM5WXXA==} engines: {node: '>=8'} @@ -23885,28 +23799,6 @@ packages: rimraf: 3.0.2 dev: true - /grunt/1.4.1: - resolution: {integrity: sha512-ZXIYXTsAVrA7sM+jZxjQdrBOAg7DyMUplOMhTaspMRExei+fD0BTwdWXnn0W5SXqhb/Q/nlkzXclSi3IH55PIA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dateformat: 3.0.3 - eventemitter2: 0.4.14 - exit: 0.1.2 - findup-sync: 0.3.0 - glob: 7.1.7 - grunt-cli: 1.4.3 - grunt-known-options: 2.0.0 - grunt-legacy-log: 3.0.0 - grunt-legacy-util: 2.0.1 - iconv-lite: 0.4.24 - js-yaml: 3.14.1 - minimatch: 3.0.4 - mkdirp: 1.0.4 - nopt: 3.0.6 - rimraf: 3.0.2 - dev: true - /gruntify-eslint/5.0.0_grunt@1.3.0: resolution: {integrity: sha512-pa2sXHK9+U4dCGdGSIMkpJARNwRStdLBsddNxmSHSSWROUdhWMrXvFWm6pj48zJhyV3Qy068VIuF1seYIvc0cw==} engines: {node: '>=0.10.0'} @@ -23981,11 +23873,6 @@ packages: /has-bigints/1.0.1: resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==} - /has-color/0.1.7: - resolution: {integrity: sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=} - engines: {node: '>=0.10.0'} - dev: true - /has-flag/1.0.0: resolution: {integrity: sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=} engines: {node: '>=0.10.0'} @@ -24318,7 +24205,7 @@ packages: pretty-error: 2.1.2 tapable: 1.1.3 util.promisify: 1.0.0 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /html-webpack-plugin/5.5.0_webpack@5.70.0: @@ -28285,20 +28172,6 @@ packages: resolve: 1.20.0 dev: true - /liftup/3.0.1: - resolution: {integrity: sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==} - engines: {node: '>=10'} - dependencies: - extend: 3.0.2 - findup-sync: 4.0.0 - fined: 1.2.0 - flagged-respawn: 1.0.1 - is-plain-object: 2.0.4 - object.map: 1.0.1 - rechoir: 0.7.1 - resolve: 1.20.0 - dev: true - /lilconfig/2.0.4: resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==} engines: {node: '>=10'} @@ -29916,19 +29789,6 @@ packages: engines: {node: '>=6'} dev: true - /node-wp-i18n/1.2.6: - resolution: {integrity: sha512-aLutjDB1rMJ3FNlNcs/XjmaejED1/y30uLYQrmkXpeUj1NH/SA6pI94CUz3iI7fbQd63lTGg0YNvOQAT8cWdIw==} - hasBin: true - dependencies: - bluebird: 3.7.2 - gettext-parser: 3.1.1 - glob: 7.2.0 - lodash: 4.17.21 - minimist: 1.2.5 - mkdirp: 1.0.4 - tmp: 0.2.1 - dev: true - /node.extend/2.0.2: resolution: {integrity: sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==} engines: {node: '>=0.4.0'} @@ -31427,7 +31287,7 @@ packages: postcss: 7.0.39 schema-utils: 3.1.1 semver: 7.3.5 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /postcss-loader/6.2.0_postcss@8.3.0+webpack@5.64.1: @@ -32900,7 +32760,7 @@ packages: dependencies: loader-utils: 2.0.2 schema-utils: 3.1.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /raw-loader/4.0.2_webpack@5.64.1: @@ -35882,7 +35742,7 @@ packages: dependencies: loader-utils: 2.0.2 schema-utils: 2.7.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /style-loader/2.0.0_webpack@5.70.0: @@ -36628,7 +36488,7 @@ packages: serialize-javascript: 5.0.1 source-map: 0.6.1 terser: 5.10.0_acorn@7.4.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 webpack-sources: 1.4.3 transitivePeerDependencies: - acorn @@ -36648,7 +36508,7 @@ packages: serialize-javascript: 5.0.1 source-map: 0.6.1 terser: 5.10.0 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 webpack-sources: 1.4.3 transitivePeerDependencies: - acorn @@ -37893,7 +37753,7 @@ packages: loader-utils: 2.0.2 mime-types: 2.1.34 schema-utils: 3.1.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /url-parse-lax/1.0.0: @@ -38094,13 +37954,6 @@ packages: homedir-polyfill: 1.0.3 dev: true - /v8flags/3.2.0: - resolution: {integrity: sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==} - engines: {node: '>= 0.10'} - dependencies: - homedir-polyfill: 1.0.3 - dev: true - /validate-npm-package-license/3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -38448,7 +38301,7 @@ packages: mime: 2.5.2 mkdirp: 0.5.5 range-parser: 1.2.1 - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 webpack-log: 2.0.0 dev: true @@ -38531,7 +38384,7 @@ packages: peerDependencies: webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 dependencies: - webpack: 4.46.0_webpack-cli@3.3.12 + webpack: 4.46.0_webpack-cli@4.9.2 dev: true /webpack-fix-style-only-entries/0.6.1: @@ -38715,7 +38568,7 @@ packages: tapable: 1.1.3 terser-webpack-plugin: 1.4.5_webpack@4.46.0 watchpack: 1.7.5 - webpack-cli: 3.3.12_webpack@5.70.0 + webpack-cli: 3.3.12_webpack@4.46.0 webpack-sources: 1.4.3 dev: true From 2e09cd824e2a678148f9a0389146444c7d20f835 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 8 Apr 2022 12:48:14 +0800 Subject: [PATCH 067/206] Add changelog --- .../changelog/dev-32417-clean-up-wc-admin-package-json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/dev-32417-clean-up-wc-admin-package-json diff --git a/plugins/woocommerce/changelog/dev-32417-clean-up-wc-admin-package-json b/plugins/woocommerce/changelog/dev-32417-clean-up-wc-admin-package-json new file mode 100644 index 00000000000..498e9121c9e --- /dev/null +++ b/plugins/woocommerce/changelog/dev-32417-clean-up-wc-admin-package-json @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove load_plugin_textdomain method from admin plugin. From b2effe67c99b32a1fbce6060fd18c4bd479c6683 Mon Sep 17 00:00:00 2001 From: Sakri Koskimies Date: Fri, 8 Apr 2022 10:48:12 +0300 Subject: [PATCH 068/206] Fix phpdoc return type for WC_Order_Refund::get_reason --- plugins/woocommerce/includes/class-wc-order-refund.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-order-refund.php b/plugins/woocommerce/includes/class-wc-order-refund.php index c384d044d65..b24ccc60619 100644 --- a/plugins/woocommerce/includes/class-wc-order-refund.php +++ b/plugins/woocommerce/includes/class-wc-order-refund.php @@ -83,7 +83,7 @@ class WC_Order_Refund extends WC_Abstract_Order { * * @since 2.2 * @param string $context What the value is for. Valid values are view and edit. - * @return int|float + * @return string */ public function get_reason( $context = 'view' ) { return $this->get_prop( 'reason', $context ); @@ -219,7 +219,7 @@ class WC_Order_Refund extends WC_Abstract_Order { * Get refund reason. * * @deprecated 3.0 - * @return int|float + * @return string */ public function get_refund_reason() { wc_deprecated_function( 'get_refund_reason', '3.0', 'get_reason' ); From b6310900de5bcfde23e6e26cd449d50c2636eb02 Mon Sep 17 00:00:00 2001 From: Sakri Koskimies Date: Fri, 8 Apr 2022 11:21:53 +0300 Subject: [PATCH 069/206] Fix TableCard component docs --- packages/js/components/src/table/README.md | 4 ++-- packages/js/components/src/table/index.js | 5 +++-- .../client/analytics/components/leaderboard/index.js | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/js/components/src/table/README.md b/packages/js/components/src/table/README.md index d24269043b8..21e30c4a83f 100644 --- a/packages/js/components/src/table/README.md +++ b/packages/js/components/src/table/README.md @@ -67,8 +67,8 @@ Name | Type | Default | Description `downloadable` | Boolean | `false` | Whether the table must be downloadable. If true, the download button will appear `onClickDownload` | Function | `null` | A callback function called when the "download" button is pressed. Optional, if used, the download button will appear `query` | Object | `{}` | An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }` -`rowHeader` | One of type: number, bool | `0` | An array of arrays of display/value object pairs (see `Table` props) -`rows` | Array | `[]` | Which column should be the row header, defaults to the first item (`0`) (see `Table` props) +`rowHeader` | One of type: number, bool | `0` | Which column should be the row header, defaults to the first item (`0`) (see `Table` props) +`rows` | Array | `[]` | (required) An array of arrays of display/value object pairs (see `Table` props) `rowsPerPage` | Number | `null` | (required) The total number of rows to display per page `searchBy` | String | `null` | The string to use as a query parameter when searching row items `showMenu` | Boolean | `true` | Boolean to determine whether or not ellipsis menu is shown diff --git a/packages/js/components/src/table/index.js b/packages/js/components/src/table/index.js index 0763f56c16a..b40d94b87d4 100644 --- a/packages/js/components/src/table/index.js +++ b/packages/js/components/src/table/index.js @@ -308,11 +308,12 @@ TableCard.propTypes = { */ query: PropTypes.object, /** - * An array of arrays of display/value object pairs (see `Table` props). + * Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col + * is checkboxes, for example). Set to false to disable row headers. */ rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ), /** - * Which column should be the row header, defaults to the first item (`0`) (see `Table` props). + * An array of arrays of display/value object pairs (see `Table` props). */ rows: PropTypes.arrayOf( PropTypes.arrayOf( diff --git a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js index ce28b7673a4..7ee8b89db32 100644 --- a/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js +++ b/plugins/woocommerce-admin/client/analytics/components/leaderboard/index.js @@ -121,7 +121,7 @@ Leaderboard.propTypes = { */ query: PropTypes.object, /** - * Which column should be the row header, defaults to the first item (`0`) (see `Table` props). + * An array of arrays of display/value object pairs (see `Table` props). */ rows: PropTypes.arrayOf( PropTypes.arrayOf( From b6611fb7c1ac7d032766366049d194f63509a9bc Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Wed, 6 Apr 2022 13:07:02 -0300 Subject: [PATCH 070/206] Add e2e tests --- .../admin-e2e-tests/src/pages/WcHomescreen.ts | 18 ++++-- .../src/specs/homescreen/welcome-modal.ts | 55 +++++++++++++++++++ .../admin-homescreen/welcome-modal.test.js | 5 ++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts create mode 100644 plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js diff --git a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts index a79b6852e19..0f2e4ccc480 100644 --- a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts +++ b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts @@ -24,12 +24,7 @@ export class WcHomescreen extends BasePage { } async possiblyDismissWelcomeModal(): Promise< void > { - const modalText = 'Welcome to your WooCommerce store’s online HQ!'; - const modal = await waitForElementByTextWithoutThrow( - 'h2', - modalText, - 10 - ); + const modal = await this.isWelcomeModalVisible(); if ( modal ) { await this.clickButtonWithText( 'Next' ); @@ -41,6 +36,17 @@ export class WcHomescreen extends BasePage { } } + async isWelcomeModalVisible(): Promise< boolean > { + const modalText = 'Welcome to your WooCommerce store’s online HQ!'; + const modal = await waitForElementByTextWithoutThrow( + 'h2', + modalText, + 10 + ); + return modal; + } + + async getTaskList(): Promise< Array< string | null > > { await page.waitForSelector( '.woocommerce-task-card .woocommerce-task-list__item-title' diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts new file mode 100644 index 00000000000..81c26317df6 --- /dev/null +++ b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { withRestApi } from '@woocommerce/e2e-utils'; + +/** + * Internal dependencies + */ + import { Login } from '../../pages/Login'; + import { OnboardingWizard } from '../../pages/OnboardingWizard'; + import { WcHomescreen } from '../../pages/WcHomescreen'; +import { + removeAllOrders, + unhideTaskList, + runActionScheduler, + updateOption, + resetWooCommerceState, +} from '../../fixtures'; + +/* eslint-disable @typescript-eslint/no-var-requires */ +const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); +/* eslint-enable @typescript-eslint/no-var-requires */ + +const testAdminHomescreenWelcomeModal = () => { + describe( 'Homescreen welcome modal', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + } ); + + afterAll( async () => { + await withRestApi.deleteAllProducts(); + await removeAllOrders(); + await unhideTaskList( 'setup' ); + await runActionScheduler(); + await updateOption( 'woocommerce_task_list_hidden', 'no' ); + await login.logout(); + } ); + + it( 'should not show welcome modal', async () => { + await homeScreen.isDisplayed(); + await expect( homeScreen.isWelcomeModalVisible() ).resolves.toBe( + false + ); + } ); + } ); +}; + +module.exports = { testAdminHomescreenWelcomeModal }; diff --git a/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js new file mode 100644 index 00000000000..c85d7381958 --- /dev/null +++ b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js @@ -0,0 +1,5 @@ +const { + testAdminHomescreenWelcomeModal, +} = require( '@woocommerce/admin-e2e-tests' ); + +testAdminHomescreenWelcomeModal(); From 00578d472fb4c8d8ee649e59864d95e7269d7e85 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 7 Apr 2022 10:39:03 -0300 Subject: [PATCH 071/206] Add welcome-modal e2e test --- packages/js/admin-e2e-tests/src/specs/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/admin-e2e-tests/src/specs/index.ts b/packages/js/admin-e2e-tests/src/specs/index.ts index dcb88e59853..8a2952d63ed 100644 --- a/packages/js/admin-e2e-tests/src/specs/index.ts +++ b/packages/js/admin-e2e-tests/src/specs/index.ts @@ -7,3 +7,4 @@ export * from './tasks/payment'; export * from './tasks/purchase'; export * from './homescreen/task-list'; export * from './homescreen/activity-panel'; +export * from './homescreen/welcome-modal'; From bf12533f0fe1b63baf9df1a36afa92c790560ef1 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 7 Apr 2022 10:39:16 -0300 Subject: [PATCH 072/206] Clean test --- .../src/specs/homescreen/welcome-modal.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts index 81c26317df6..8e69819c149 100644 --- a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts +++ b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { withRestApi } from '@woocommerce/e2e-utils'; - /** * Internal dependencies */ @@ -10,10 +5,6 @@ import { withRestApi } from '@woocommerce/e2e-utils'; import { OnboardingWizard } from '../../pages/OnboardingWizard'; import { WcHomescreen } from '../../pages/WcHomescreen'; import { - removeAllOrders, - unhideTaskList, - runActionScheduler, - updateOption, resetWooCommerceState, } from '../../fixtures'; @@ -35,11 +26,6 @@ const testAdminHomescreenWelcomeModal = () => { } ); afterAll( async () => { - await withRestApi.deleteAllProducts(); - await removeAllOrders(); - await unhideTaskList( 'setup' ); - await runActionScheduler(); - await updateOption( 'woocommerce_task_list_hidden', 'no' ); await login.logout(); } ); From 956a0dffa711c49623ac5c478f6580a3812cbcea Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Thu, 7 Apr 2022 11:23:46 -0300 Subject: [PATCH 073/206] Fix lint --- packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts | 1 - .../src/specs/homescreen/welcome-modal.ts | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts index 0f2e4ccc480..d1c6c90e06a 100644 --- a/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts +++ b/packages/js/admin-e2e-tests/src/pages/WcHomescreen.ts @@ -46,7 +46,6 @@ export class WcHomescreen extends BasePage { return modal; } - async getTaskList(): Promise< Array< string | null > > { await page.waitForSelector( '.woocommerce-task-card .woocommerce-task-list__item-title' diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts index 8e69819c149..60c8c9ba76e 100644 --- a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts +++ b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts @@ -1,12 +1,10 @@ /** * Internal dependencies */ - import { Login } from '../../pages/Login'; - import { OnboardingWizard } from '../../pages/OnboardingWizard'; - import { WcHomescreen } from '../../pages/WcHomescreen'; -import { - resetWooCommerceState, -} from '../../fixtures'; +import { Login } from '../../pages/Login'; +import { OnboardingWizard } from '../../pages/OnboardingWizard'; +import { WcHomescreen } from '../../pages/WcHomescreen'; +import { resetWooCommerceState } from '../../fixtures'; /* eslint-disable @typescript-eslint/no-var-requires */ const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); From 49aff7e3dc3f1af9bda68fee8bf0ce6dc99ae6aa Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 8 Apr 2022 09:17:49 -0300 Subject: [PATCH 074/206] Add changelog --- .../changelog/add-32157_tests_to_disable_welcome_modals | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-32157_tests_to_disable_welcome_modals diff --git a/plugins/woocommerce/changelog/add-32157_tests_to_disable_welcome_modals b/plugins/woocommerce/changelog/add-32157_tests_to_disable_welcome_modals new file mode 100644 index 00000000000..21116006a6f --- /dev/null +++ b/plugins/woocommerce/changelog/add-32157_tests_to_disable_welcome_modals @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add E2E tests to disabled welcome modal #32505 From 0ccf740ef6a5c24f43818ff52d26300a5f860c0f Mon Sep 17 00:00:00 2001 From: Bero Date: Fri, 8 Apr 2022 13:25:01 +0200 Subject: [PATCH 075/206] Remove 'WooCommerce Addons Page' user-agent --- .../woocommerce/includes/admin/class-wc-admin-addons.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php index 9fca11d5ec8..ffe87e84046 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php @@ -38,8 +38,7 @@ class WC_Admin_Addons { $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/featured', array( - 'headers' => $headers, - 'user-agent' => 'WooCommerce Addons Page', + 'headers' => $headers, ) ); @@ -82,8 +81,7 @@ class WC_Admin_Addons { $raw_featured = wp_safe_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, array( - 'headers' => $headers, - 'user-agent' => 'WooCommerce Addons Page', + 'headers' => $headers, ) ); @@ -258,7 +256,7 @@ class WC_Admin_Addons { if ( ! empty( $section->endpoint ) ) { $section_data = get_transient( 'wc_addons_section_' . $section_id ); if ( false === $section_data ) { - $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) ); + $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ) ); if ( ! is_wp_error( $raw_section ) ) { $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); From f08edf3f4b9d163050ba5f451813609d4f411074 Mon Sep 17 00:00:00 2001 From: Bero Date: Fri, 8 Apr 2022 14:03:42 +0200 Subject: [PATCH 076/206] Add changelog --- .../remove-32444-featured-request-user-agent-headers | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/remove-32444-featured-request-user-agent-headers diff --git a/plugins/woocommerce/changelog/remove-32444-featured-request-user-agent-headers b/plugins/woocommerce/changelog/remove-32444-featured-request-user-agent-headers new file mode 100644 index 00000000000..8aabb35193e --- /dev/null +++ b/plugins/woocommerce/changelog/remove-32444-featured-request-user-agent-headers @@ -0,0 +1,4 @@ +Significance: minor +Type: tweak + +Remove custom user-agent from featured extensions requests. From 11239494ef45b7b4a41e440137104f7ae204aa88 Mon Sep 17 00:00:00 2001 From: Bero Date: Fri, 8 Apr 2022 14:20:11 +0200 Subject: [PATCH 077/206] Fix Code sniff errors --- .../includes/admin/class-wc-admin-addons.php | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php index ffe87e84046..b47f5ecd3e1 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-addons.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-addons.php @@ -104,6 +104,7 @@ class WC_Admin_Addons { /* translators: %d: HTTP error code. */ $message = sprintf( esc_html( + /* translators: Error code */ __( 'Our request to the featured API got error code %d.', 'woocommerce' @@ -134,6 +135,13 @@ class WC_Admin_Addons { self::output_featured( $featured ); } + /** + * Check if the error is due to an SSL error + * + * @param string $error_message Error message. + * + * @return bool True if SSL error, false otherwise + */ public static function is_ssl_error( $error_message ) { return false !== stripos( $error_message, 'cURL error 35' ); } @@ -190,14 +198,23 @@ class WC_Admin_Addons { $response_code = (int) wp_remote_retrieve_response_code( $raw_extensions ); if ( 200 !== $response_code ) { do_action( 'woocommerce_page_wc-addons_connection_error', $response_code ); - return new WP_Error( 'error', __( "Our request to the search API got response code $response_code.", 'woocommerce' ) ); + return new WP_Error( + 'error', + sprintf( + esc_html( + /* translators: Error code */ + __( 'Our request to the search API got response code %s.', 'woocommerce' ) + ), + $response_code + ) + ); } $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); if ( ! is_object( $addons ) || ! isset( $addons->products ) ) { do_action( 'woocommerce_page_wc-addons_connection_error', 'Empty or malformed response' ); - return new WP_Error( 'error', __( "Our request to the search API got a malformed response.", 'woocommerce' ) ); + return new WP_Error( 'error', __( 'Our request to the search API got a malformed response.', 'woocommerce' ) ); } return $addons; @@ -961,6 +978,13 @@ class WC_Admin_Addons {
@@ -970,9 +994,9 @@ class WC_Admin_Addons {

WooCommerce.com, where you\'ll find the most popular WooCommerce extensions.', 'woocommerce' From 9e905ea713afc77fa872afbd887df317a2e247bf Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 8 Apr 2022 14:03:55 -0300 Subject: [PATCH 078/206] Add Pinterest logo --- .../assets/images/onboarding/pinterest.png | Bin 0 -> 4959 bytes .../DefaultFreeExtensions.php | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/assets/images/onboarding/pinterest.png diff --git a/plugins/woocommerce/assets/images/onboarding/pinterest.png b/plugins/woocommerce/assets/images/onboarding/pinterest.png new file mode 100644 index 0000000000000000000000000000000000000000..2951353d8fdc53724d79af42a3ab830d76260dbd GIT binary patch literal 4959 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 N0H&q`t%R5b{tuJ;;', '' ), - 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( 'images/onboarding/pinterest.png', WC_ADMIN_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=pinterest-for-woocommerce', 'is_visible' => [ [ @@ -126,7 +126,7 @@ class DefaultFreeExtensions { 'pinterest-for-woocommerce:alt' => [ 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce-admin' ), - 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( 'images/onboarding/pinterest.png', WC_ADMIN_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => false, ], From ecbd8541052ee2efebec7f9e06eeca21e11930a7 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 8 Apr 2022 15:07:37 -0300 Subject: [PATCH 079/206] Add changelog --- .../changelog/add-32141_pinterest_extension_to_obw | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-32141_pinterest_extension_to_obw diff --git a/plugins/woocommerce/changelog/add-32141_pinterest_extension_to_obw b/plugins/woocommerce/changelog/add-32141_pinterest_extension_to_obw new file mode 100644 index 00000000000..83bce430bb6 --- /dev/null +++ b/plugins/woocommerce/changelog/add-32141_pinterest_extension_to_obw @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Pinterest extension to onboarding wizard and marketing task #32527 From da5dc21acc7db3628d8f65d5d89bd392f2e05f24 Mon Sep 17 00:00:00 2001 From: Christopher Allford Date: Fri, 8 Apr 2022 11:32:14 -0700 Subject: [PATCH 080/206] Replaced Accent Removal The current `iconv` based implementation does not seem to work in all environments. This replaces it with WordPress' native `remove_accents` function. --- plugins/woocommerce/changelog/fix-ascii-uasort | 4 ++++ .../woocommerce/includes/wc-core-functions.php | 15 ++------------- 2 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-ascii-uasort diff --git a/plugins/woocommerce/changelog/fix-ascii-uasort b/plugins/woocommerce/changelog/fix-ascii-uasort new file mode 100644 index 00000000000..dbd0e8f6d4d --- /dev/null +++ b/plugins/woocommerce/changelog/fix-ascii-uasort @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +String sorting when using different locales. diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php index 83ffafe812e..177dbf2eff3 100644 --- a/plugins/woocommerce/includes/wc-core-functions.php +++ b/plugins/woocommerce/includes/wc-core-functions.php @@ -1775,19 +1775,8 @@ function wc_uasort_comparison( $a, $b ) { * @return int */ function wc_ascii_uasort_comparison( $a, $b ) { - // 'setlocale' is required for compatibility with PHP 8. - // Without it, 'iconv' will return '?'s instead of transliterated characters. - $prev_locale = setlocale( LC_CTYPE, 0 ); - setlocale( LC_ALL, 'C.UTF-8' ); - - // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged - if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) { - $a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a ); - $b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b ); - } - // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged - - setlocale( LC_ALL, $prev_locale ); + $a = remove_accents( $a ); + $b = remove_accents( $b ); return strcmp( $a, $b ); } From bc11e57e14888a42ec34a7f77795534e5565f848 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Thu, 31 Mar 2022 17:04:03 -0700 Subject: [PATCH 081/206] Support protocol-less URLs from the URL util. --- .../src/Internal/Utilities/URL.php | 22 +++++++++++-------- .../php/src/Internal/Utilities/URLTest.php | 2 ++ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce/src/Internal/Utilities/URL.php b/plugins/woocommerce/src/Internal/Utilities/URL.php index 26b429e9f2a..60963b09ffa 100644 --- a/plugins/woocommerce/src/Internal/Utilities/URL.php +++ b/plugins/woocommerce/src/Internal/Utilities/URL.php @@ -97,8 +97,12 @@ class URL { $this->components['drive'] = $matches[2]; } - // If there is no scheme, assume and prepend "file://". - if ( ! preg_match( '#^[a-z]+://#i', $this->url ) ) { + /* + * If there is no scheme, assume and prepend "file://". An exception is made for cases where the URL simply + * starts with exactly two forward slashes, which indicates 'any scheme' (most commonly, that is used when + * there is freedom to switch between 'http' and 'https'). + */ + if ( ! preg_match( '#^[a-z]+://#i', $this->url ) && ! preg_match( '#^//(?!/)#', $this->url ) ) { $this->url = 'file://' . $this->url; } @@ -115,19 +119,19 @@ class URL { ); } + $this->components = array_merge( $this->components, $parsed_components ); + // File URLs cannot have a host. However, the initial path segment *or* the Windows drive letter // (if present) may be incorrectly be interpreted as the host name. - if ( 'file' === $parsed_components['scheme'] && ! empty( $parsed_components['host'] ) ) { + if ( 'file' === $this->components['scheme'] && ! empty( $this->components['host'] ) ) { // If we do not have a drive letter, then simply merge the host and the path together. if ( null === $this->components['drive'] ) { - $parsed_components['path'] = $parsed_components['host'] . ( $parsed_components['path'] ?? '' ); + $this->components['path'] = $this->components['host'] . ( $this->components['path'] ?? '' ); } - // Always unset the host in this situation. - unset( $parsed_components['host'] ); + // Restore the host to null in this situation. + $this->components['host'] = null; } - - $this->components = array_merge( $this->components, $parsed_components ); } /** @@ -325,7 +329,7 @@ class URL { public function get_url( array $component_overrides = array() ): string { $components = array_merge( $this->components, $component_overrides ); - $scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : ''; + $scheme = null !== $components['scheme'] ? $components['scheme'] . '://' : '//'; $host = null !== $components['host'] ? $components['host'] : ''; $port = null !== $components['port'] ? ':' . $components['port'] : ''; $path = $this->get_path( $components['path'] ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php index 659da935540..95ab9838164 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php @@ -88,6 +88,8 @@ class URLTest extends WC_Unit_Test_Case { array( 'https://foo.bar/parent/%2E%2e/asset.txt', 'https://foo.bar/asset.txt' ), array( 'https://foo.bar/parent/%2E.%2fasset.txt', 'https://foo.bar/parent/%2E.%2fasset.txt' ), array( 'http://localhost?../../bar', 'http://localhost/?../../bar' ), + array( '//http.or.https/', '//http.or.https/' ), + array( '//schemaless/with-path', '//schemaless/with-path' ), ); } From 6dcb796a890743098e7a3660093f3c0347f3ecb7 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:51:18 -0700 Subject: [PATCH 082/206] No changelog required (but let's satisfy the linter). --- plugins/woocommerce/changelog/fix-protocol-agnostic-urls | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-protocol-agnostic-urls diff --git a/plugins/woocommerce/changelog/fix-protocol-agnostic-urls b/plugins/woocommerce/changelog/fix-protocol-agnostic-urls new file mode 100644 index 00000000000..740508dbf36 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-protocol-agnostic-urls @@ -0,0 +1,5 @@ +Significance: patch +Type: tweak +Comment: Changelog entry not needed, because this is an adjustment to unreleased code. + + From 35a2eb1f9fa48b2591a3fad0c8b8e94c53c3e6f5 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 8 Apr 2022 16:10:43 -0300 Subject: [PATCH 083/206] Move test to `complete-onboarding-wizard` --- .../complete-onboarding-wizard.ts | 27 +++++++++++++ .../src/specs/homescreen/welcome-modal.ts | 39 ------------------- .../js/admin-e2e-tests/src/specs/index.ts | 1 - .../complete-onboarding-wizard.test.js | 2 + 4 files changed, 29 insertions(+), 40 deletions(-) delete mode 100644 packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts index f0772672b86..5a8e48dd0b9 100644 --- a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts +++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts @@ -597,10 +597,37 @@ const testBusinessDetailsForm = () => { } ); }; +const testAdminHomescreenWelcomeModal = () => { + describe( 'Homescreen', () => { + const profileWizard = new OnboardingWizard( page ); + const homeScreen = new WcHomescreen( page ); + const login = new Login( page ); + + beforeAll( async () => { + await login.login(); + await resetWooCommerceState(); + await profileWizard.navigate(); + await profileWizard.skipStoreSetup(); + } ); + + afterAll( async () => { + await login.logout(); + } ); + + it( 'should not show welcome modal', async () => { + await homeScreen.isDisplayed(); + await expect( homeScreen.isWelcomeModalVisible() ).resolves.toBe( + false + ); + } ); + } ); +}; + module.exports = { testAdminOnboardingWizard, testSelectiveBundleWCPay, testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, + testAdminHomescreenWelcomeModal, }; diff --git a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts b/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts deleted file mode 100644 index 60c8c9ba76e..00000000000 --- a/packages/js/admin-e2e-tests/src/specs/homescreen/welcome-modal.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Internal dependencies - */ -import { Login } from '../../pages/Login'; -import { OnboardingWizard } from '../../pages/OnboardingWizard'; -import { WcHomescreen } from '../../pages/WcHomescreen'; -import { resetWooCommerceState } from '../../fixtures'; - -/* eslint-disable @typescript-eslint/no-var-requires */ -const { afterAll, beforeAll, describe, it } = require( '@jest/globals' ); -/* eslint-enable @typescript-eslint/no-var-requires */ - -const testAdminHomescreenWelcomeModal = () => { - describe( 'Homescreen welcome modal', () => { - const profileWizard = new OnboardingWizard( page ); - const homeScreen = new WcHomescreen( page ); - const login = new Login( page ); - - beforeAll( async () => { - await login.login(); - await resetWooCommerceState(); - await profileWizard.navigate(); - await profileWizard.skipStoreSetup(); - } ); - - afterAll( async () => { - await login.logout(); - } ); - - it( 'should not show welcome modal', async () => { - await homeScreen.isDisplayed(); - await expect( homeScreen.isWelcomeModalVisible() ).resolves.toBe( - false - ); - } ); - } ); -}; - -module.exports = { testAdminHomescreenWelcomeModal }; diff --git a/packages/js/admin-e2e-tests/src/specs/index.ts b/packages/js/admin-e2e-tests/src/specs/index.ts index 8a2952d63ed..dcb88e59853 100644 --- a/packages/js/admin-e2e-tests/src/specs/index.ts +++ b/packages/js/admin-e2e-tests/src/specs/index.ts @@ -7,4 +7,3 @@ export * from './tasks/payment'; export * from './tasks/purchase'; export * from './homescreen/task-list'; export * from './homescreen/activity-panel'; -export * from './homescreen/welcome-modal'; diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js index 77af197d123..d69d0eb855a 100644 --- a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js @@ -4,6 +4,7 @@ const { testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, + testAdminHomescreenWelcomeModal, } = require( '@woocommerce/admin-e2e-tests' ); const { withRestApi, IS_RETEST_MODE } = require( '@woocommerce/e2e-utils' ); @@ -17,3 +18,4 @@ testSelectiveBundleWCPay(); testDifferentStoreCurrenciesWCPay(); testSubscriptionsInclusion(); testBusinessDetailsForm(); +testAdminHomescreenWelcomeModal(); From 4449a669c84f850b482a0f3209cb4505e3cc7579 Mon Sep 17 00:00:00 2001 From: Josh Betz Date: Fri, 8 Apr 2022 15:31:03 -0500 Subject: [PATCH 084/206] Add order_item_display_meta description --- .../Version2/class-wc-rest-orders-v2-controller.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index 421a55e0c11..30b012c9096 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -1850,6 +1850,13 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { 'sanitize_callback' => 'absint', 'validate_callback' => 'rest_validate_request_arg', ); + $params['order_item_display_meta'] = array( + 'default' => false, + 'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocomerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + 'validate_callback' => 'rest_validate_request_arg', + ); return $params; } From 92a2035896a56c2b3219090e3ec6b4a53a8a0cfd Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Fri, 8 Apr 2022 16:20:44 -0700 Subject: [PATCH 085/206] Update PHPCS ruleset for PHP 7.2. --- phpcs.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpcs.xml b/phpcs.xml index 525a3e938d6..74e638dd71a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -27,7 +27,7 @@ - + From c5cf0fbd795cf39f40a5de697e56a872c9bb30d3 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 11 Apr 2022 10:50:26 +0800 Subject: [PATCH 086/206] Fix admin js-tests config path --- packages/js/js-tests/src/setup-globals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/js-tests/src/setup-globals.js b/packages/js/js-tests/src/setup-globals.js index 201fc59265f..b95d9e74d2c 100644 --- a/packages/js/js-tests/src/setup-globals.js +++ b/packages/js/js-tests/src/setup-globals.js @@ -107,7 +107,7 @@ wooCommercePackages.forEach( ( lib ) => { } ); } ); -const config = require( '../../../../plugins/woocommerce-admin/config/development.json' ); +const config = require( '../../../../plugins/woocommerce/client/admin/config/development.json' ); // Check if test is jsdom or node if ( global.window ) { From 90c61e4bada3bb7d2e3306630619afe8e8368fbe Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 11 Apr 2022 11:51:55 +0800 Subject: [PATCH 087/206] Revert WCPay suggestion UI changes --- .../src/components/WCPayAcceptedMethods.js | 25 +- .../src/components/WCPayCard/WCPayCard.js | 147 +++------- .../src/components/WCPayCard/WCPayCard.scss | 155 +++-------- .../js/onboarding/src/images/cards/amex.js | 23 +- .../onboarding/src/images/cards/applepay.js | 21 +- packages/js/onboarding/src/images/cards/cb.js | 33 +-- .../js/onboarding/src/images/cards/diners.js | 251 ++++++++++++++++-- .../onboarding/src/images/cards/discover.js | 148 +++-------- .../js/onboarding/src/images/cards/giropay.js | 49 ---- .../onboarding/src/images/cards/googlepay.js | 47 ++-- .../js/onboarding/src/images/cards/jcb.js | 72 ++--- .../onboarding/src/images/cards/mastercard.js | 35 +-- .../js/onboarding/src/images/cards/sofort.js | 35 --- .../onboarding/src/images/cards/unionpay.js | 103 ++++++- .../js/onboarding/src/images/cards/visa.js | 35 +-- .../onboarding/src/images/wcpay-benefit-1.js | 134 ---------- .../onboarding/src/images/wcpay-benefit-2.js | 102 ------- .../onboarding/src/images/wcpay-benefit-3.js | 188 ------------- .../onboarding/src/images/wcpay-hero-image.js | 140 ---------- .../components/WCPay/Suggestion.js | 67 +++-- .../PaymentGatewaySuggestions/test/index.js | 6 +- 21 files changed, 599 insertions(+), 1217 deletions(-) delete mode 100644 packages/js/onboarding/src/images/cards/giropay.js delete mode 100644 packages/js/onboarding/src/images/cards/sofort.js delete mode 100644 packages/js/onboarding/src/images/wcpay-benefit-1.js delete mode 100644 packages/js/onboarding/src/images/wcpay-benefit-2.js delete mode 100644 packages/js/onboarding/src/images/wcpay-benefit-3.js delete mode 100644 packages/js/onboarding/src/images/wcpay-hero-image.js diff --git a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js index cea52fdb3d9..f104c24e4e9 100644 --- a/packages/js/onboarding/src/components/WCPayAcceptedMethods.js +++ b/packages/js/onboarding/src/components/WCPayAcceptedMethods.js @@ -10,39 +10,32 @@ import { __ } from '@wordpress/i18n'; */ import Visa from '../images/cards/visa.js'; import MasterCard from '../images/cards/mastercard.js'; +import Maestro from '../images/cards/maestro.js'; import Amex from '../images/cards/amex.js'; import ApplePay from '../images/cards/applepay.js'; -import GooglePay from '../images/cards/googlepay.js'; import CB from '../images/cards/cb.js'; import DinersClub from '../images/cards/diners.js'; import Discover from '../images/cards/discover.js'; import JCB from '../images/cards/jcb.js'; import UnionPay from '../images/cards/unionpay.js'; -import GiroPay from '../images/cards/giropay.js'; -import Sofort from '../images/cards/sofort.js'; export const WCPayAcceptedMethods = () => ( -

- - { __( 'Accepted payment methods include:', 'woocommerce' ) } + <> + + { __( 'Accepted payment methods', 'woocommerce' ) } -
+
+ - - - - + - -
- & more. -
+
-
+ ); diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js index ca4f5cf634b..88a79a7ad1c 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.js @@ -1,126 +1,63 @@ /** * External dependencies */ -import { Card, CardBody, CardFooter } from '@wordpress/components'; +import { Card, CardBody, CardHeader, CardFooter } from '@wordpress/components'; import { Text } from '@woocommerce/experimental'; import { createElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; -import interpolateComponents from '@automattic/interpolate-components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { WCPayAcceptedMethods } from '../WCPayAcceptedMethods'; -import WCPayHeroImage from '../../images/wcpay-hero-image'; -import WCPayBenefit1 from '../../images/wcpay-benefit-1'; -import WCPayBenefit2 from '../../images/wcpay-benefit-2'; -import WCPayBenefit3 from '../../images/wcpay-benefit-3'; +import WCPayLogo from '../../images/wcpay-logo'; -export const WCPayCardBody = ( { children, onLinkClick = () => {} } ) => ( +export const WCPayCardHeader = ( { + logoWidth = 196, + logoHeight = 41, + children, +} ) => ( + + + { children } + +); + +export const WCPayCardBody = ( { + description, + heading, + onLinkClick = () => {}, +} ) => ( -
-
- - { __( - 'Accept payments and manage your business.', - 'woocommerce' - ) } - - - { interpolateComponents( { - mixedString: __( - 'By using WooCommerce Payments you agree to be bound by our {{tosLink /}} and acknowledge that you have read our {{privacyLink /}}', - 'woocommerce' - ), - components: { - tosLink: ( - - { __( 'Terms of Service', 'woocommerce' ) } - - ), - privacyLink: ( - - { __( 'Privacy Policy', 'woocommerce' ) } - - ), - }, - } ) } -
-
-
{ children }
-
-
- -
-
+ { heading && { heading } } + + + { description } +
+ + { __( 'Learn more', 'woocommerce' ) } + +
+ +
); -export const WCPayCardFooter = () => ( - - - +export const WCPayCardFooter = ( { children } ) => ( + { children } ); export const WCPayCard = ( { children } ) => { - return ( - - { children } - - ); -}; - -export const WCPayBenefitCard = () => { - return ( - - -
-
- - { __( - 'Offer your customers their preferred way to pay including debit and credit card payments, Apple Pay, Sofort, SEPA, iDeal and many more.', - 'woocommerce' - ) } -
-
- - { __( - 'Sell to international markets and accept more than 135 currencies with local payment methods.', - 'woocommerce' - ) } -
-
- - { __( - 'Earn and manage recurring revenue and get automatic deposits into your nominated bank account.', - 'woocommerce' - ) } -
-
-
-
- ); + return { children }; }; diff --git a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss index 6e50f1f3edd..c21e835ff68 100644 --- a/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss +++ b/packages/js/onboarding/src/components/WCPayCard/WCPayCard.scss @@ -1,126 +1,43 @@ -.woocommerce-task-payments { - .woocommerce-task-payment-wcpay { - .vstack, - .hstack { - display: flex; +.woocommerce-task-payments .woocommerce-task-payment-wcpay { + .woocommerce-task-payment-wcpay__description { + font-size: 16px; + margin-bottom: $gap-large; + } - &.content-center { - justify-content: center; - } + .components-card__header { + margin-bottom: $gap-small; + justify-content: flex-start; + padding: 25px; - &.content-around { - justify-content: space-around; - } - - &.flex-1 { - flex: 1; - } - } - - .vstack { - flex-direction: column; - } - - .hstack { - flex-direction: row; - } - - .woocommerce-task-payment-wcpay__hero-image-container { - display: flex; - flex-direction: column; - justify-content: flex-end; - - svg { - margin-right: -32px; - margin-bottom: -24px; - } - - @media screen and (max-width: 600px) { - max-width: 200px; - } - } - - .woocommerce-task-payment-wcpay__heading { - letter-spacing: 0.6px; - max-width: 250px; - margin-right: $gap-small; - margin-bottom: $gap; - } - - .woocommerce-task-payment-wcpay__description { - font-size: 13px; - max-width: 325px; - margin-right: $gap-small; - margin-bottom: $gap-large; - } - - .components-card__header { - margin-bottom: $gap-small; - justify-content: flex-start; - padding: 25px; - - .woocommerce-pill { - margin-left: $gap-small; - } - } - - .components-card__footer { - flex-direction: column; - align-items: flex-start; - - .components-button { - margin-top: $gap; - margin-left: 0; - } - - &, - h3 { - color: #757575; - } - - svg { - height: 24px; - } - } - - .components-card__body { - h2 { - margin: 0 0 20px 0; - } - } - - .woocommerce-task-payment-wcpay__accepted-icons { - h3 { - color: #40464d; - } - - display: flex; - flex-wrap: wrap; - margin-top: $gap-small; - margin-left: 0; - gap: 8px; - justify-content: flex-start; - align-items: flex-end; - } - - .woocommerce-task-payment-wcpay__benefit { - svg { - margin: 0 auto; - } - - max-width: 170px; - text-align: center; - font-size: 15px; - line-height: 24px; - letter-spacing: normal; - } - - .woocommerce-task-payment-wcpay__accepted-and-more { - white-space: nowrap; + .woocommerce-pill { + margin-left: $gap-small; } } - .woocommerce-task-payment-wcpay__benefits-card { - margin-bottom: 0; + .components-card__footer { + flex-direction: column; + align-items: flex-start; + + .components-button { + margin-top: $gap; + margin-left: 0; + } + } + + .components-card__body { + h2 { + margin: 0 0 20px 0; + } + } + + .woocommerce-task-payment-wcpay__accepted { + display: flex; + margin-top: $gap-small; + flex-wrap: wrap; + gap: $gap-small; + + h3 { + color: #40464d; + } } } diff --git a/packages/js/onboarding/src/images/cards/amex.js b/packages/js/onboarding/src/images/cards/amex.js index 49facfe9d6e..daa085352db 100644 --- a/packages/js/onboarding/src/images/cards/amex.js +++ b/packages/js/onboarding/src/images/cards/amex.js @@ -5,25 +5,26 @@ import { createElement } from '@wordpress/element'; export default () => ( - - ); diff --git a/packages/js/onboarding/src/images/cards/applepay.js b/packages/js/onboarding/src/images/cards/applepay.js index edb77a8a8e8..74febe62299 100644 --- a/packages/js/onboarding/src/images/cards/applepay.js +++ b/packages/js/onboarding/src/images/cards/applepay.js @@ -5,25 +5,26 @@ import { createElement } from '@wordpress/element'; export default () => ( - - ); diff --git a/packages/js/onboarding/src/images/cards/cb.js b/packages/js/onboarding/src/images/cards/cb.js index b3dcb6cadcd..da48c9a5b13 100644 --- a/packages/js/onboarding/src/images/cards/cb.js +++ b/packages/js/onboarding/src/images/cards/cb.js @@ -5,33 +5,34 @@ import { createElement } from '@wordpress/element'; export default () => ( - - diff --git a/packages/js/onboarding/src/images/cards/diners.js b/packages/js/onboarding/src/images/cards/diners.js index 5630431f0eb..039bad8b238 100644 --- a/packages/js/onboarding/src/images/cards/diners.js +++ b/packages/js/onboarding/src/images/cards/diners.js @@ -5,37 +5,254 @@ import { createElement } from '@wordpress/element'; export default () => ( - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/packages/js/onboarding/src/images/cards/discover.js b/packages/js/onboarding/src/images/cards/discover.js index a9e764ba46c..51479f917aa 100644 --- a/packages/js/onboarding/src/images/cards/discover.js +++ b/packages/js/onboarding/src/images/cards/discover.js @@ -5,134 +5,46 @@ import { createElement } from '@wordpress/element'; export default () => ( - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + + - - - - - - - - - ); diff --git a/packages/js/onboarding/src/images/cards/giropay.js b/packages/js/onboarding/src/images/cards/giropay.js deleted file mode 100644 index 6979d1911c5..00000000000 --- a/packages/js/onboarding/src/images/cards/giropay.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default () => ( - - - - - - - - - -); diff --git a/packages/js/onboarding/src/images/cards/googlepay.js b/packages/js/onboarding/src/images/cards/googlepay.js index d7e00b3b269..329ec0e3641 100644 --- a/packages/js/onboarding/src/images/cards/googlepay.js +++ b/packages/js/onboarding/src/images/cards/googlepay.js @@ -5,61 +5,48 @@ import { createElement } from '@wordpress/element'; export default () => ( - - ); diff --git a/packages/js/onboarding/src/images/cards/jcb.js b/packages/js/onboarding/src/images/cards/jcb.js index 36c6569289e..ee332408bba 100644 --- a/packages/js/onboarding/src/images/cards/jcb.js +++ b/packages/js/onboarding/src/images/cards/jcb.js @@ -5,78 +5,38 @@ import { createElement } from '@wordpress/element'; export default () => ( - - - - - - - - - - - - - - - - - ); diff --git a/packages/js/onboarding/src/images/cards/mastercard.js b/packages/js/onboarding/src/images/cards/mastercard.js index d6755de398d..8fbde439e41 100644 --- a/packages/js/onboarding/src/images/cards/mastercard.js +++ b/packages/js/onboarding/src/images/cards/mastercard.js @@ -5,37 +5,38 @@ import { createElement } from '@wordpress/element'; export default () => ( - ); diff --git a/packages/js/onboarding/src/images/cards/sofort.js b/packages/js/onboarding/src/images/cards/sofort.js deleted file mode 100644 index 9d651368c38..00000000000 --- a/packages/js/onboarding/src/images/cards/sofort.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default () => ( - - - - - - - -); diff --git a/packages/js/onboarding/src/images/cards/unionpay.js b/packages/js/onboarding/src/images/cards/unionpay.js index 457b182fbbb..0be12014c7b 100644 --- a/packages/js/onboarding/src/images/cards/unionpay.js +++ b/packages/js/onboarding/src/images/cards/unionpay.js @@ -5,35 +5,110 @@ import { createElement } from '@wordpress/element'; export default () => ( + + + + + + + + + + ); diff --git a/packages/js/onboarding/src/images/cards/visa.js b/packages/js/onboarding/src/images/cards/visa.js index dbae529ccd4..cd81d8a8fa3 100644 --- a/packages/js/onboarding/src/images/cards/visa.js +++ b/packages/js/onboarding/src/images/cards/visa.js @@ -5,39 +5,40 @@ import { createElement } from '@wordpress/element'; export default () => ( - - ); diff --git a/packages/js/onboarding/src/images/wcpay-benefit-1.js b/packages/js/onboarding/src/images/wcpay-benefit-1.js deleted file mode 100644 index e0dc078c0f7..00000000000 --- a/packages/js/onboarding/src/images/wcpay-benefit-1.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default ( { width = 141, height = 148, ...props } ) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/js/onboarding/src/images/wcpay-benefit-2.js b/packages/js/onboarding/src/images/wcpay-benefit-2.js deleted file mode 100644 index d9ffafc23cd..00000000000 --- a/packages/js/onboarding/src/images/wcpay-benefit-2.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default ( { width = 140, height = 148, ...props } ) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/js/onboarding/src/images/wcpay-benefit-3.js b/packages/js/onboarding/src/images/wcpay-benefit-3.js deleted file mode 100644 index 9d29102f3e8..00000000000 --- a/packages/js/onboarding/src/images/wcpay-benefit-3.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default ( { width = 157, height = 148, ...props } ) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/packages/js/onboarding/src/images/wcpay-hero-image.js b/packages/js/onboarding/src/images/wcpay-hero-image.js deleted file mode 100644 index 2d70f3d126d..00000000000 --- a/packages/js/onboarding/src/images/wcpay-hero-image.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * External dependencies - */ -import { createElement } from '@wordpress/element'; - -export default ( { width = 293, height = 275, ...props } ) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js index 40da21f5430..ee4ad87a861 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/WCPay/Suggestion.js @@ -2,12 +2,16 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import interpolateComponents from '@automattic/interpolate-components'; +import { Link, Pill } from '@woocommerce/components'; import { recordEvent } from '@woocommerce/tracks'; +import { Text } from '@woocommerce/experimental'; import { WCPayCard, + WCPayCardHeader, WCPayCardFooter, WCPayCardBody, - WCPayBenefitCard, + SetupRequired, } from '@woocommerce/onboarding'; import { useDispatch } from '@wordpress/data'; @@ -18,8 +22,26 @@ import { useDispatch } from '@wordpress/data'; import { Action } from '../Action'; import { connectWcpay } from './utils'; +const TosPrompt = () => + interpolateComponents( { + mixedString: __( + 'Upon clicking "Get started", you agree to the {{link}}Terms of service{{/link}}. Next we’ll ask you to share a few details about your business to create your account.', + 'woocommerce' + ), + components: { + link: ( + + ), + }, + } ); + export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { const { + description, id, needsSetup, installed, @@ -40,14 +62,27 @@ export const Suggestion = ( { paymentGateway, onSetupCallback = null } ) => { } return ( - <> - - { - recordEvent( 'tasklist_payment_learn_more' ); - } } - > + + + { installed && needsSetup ? ( + + ) : ( + { __( 'Recommended', 'woocommerce' ) } + ) } + + + { + recordEvent( 'tasklist_payment_learn_more' ); + } } + /> + + + <> + + + { isRecommended={ true } isInstalled={ isInstalled } hasPlugins={ true } - setupButtonText={ - installed && needsSetup - ? __( 'Finish setup', 'woocommerce' ) - : __( 'Install', 'woocommerce' ) - } + setupButtonText={ __( 'Get started', 'woocommerce' ) } onSetupCallback={ onSetupCallback } /> - - - - - + + + ); }; diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index 3fdf6c29efd..7df91e7a82a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -118,10 +118,8 @@ describe( 'PaymentGatewaySuggestions', () => { expect( paymentTitles ).toEqual( [] ); expect( - container.getElementsByClassName( - 'woocommerce-task-payment-wcpay' - )[ 0 ].textContent - ).toContain( 'By using WooCommerce Payments' ); + container.getElementsByTagName( 'title' )[ 0 ].textContent + ).toBe( 'WooCommerce Payments' ); } ); test( 'should render all payment gateways if no WCPay', () => { From c8b6bd91e037f33a0ee81054f3d4b06af0975400 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 11 Apr 2022 14:24:08 +0800 Subject: [PATCH 088/206] Update payment gateway logos --- .../components/List/List.scss | 5 ++-- .../fills/PaymentGatewaySuggestions/index.js | 4 +-- .../images/payment_methods/72x72/affirm.png | Bin 0 -> 972 bytes .../images/payment_methods/72x72/afterpay.png | Bin 0 -> 674 bytes .../payment_methods/72x72/amazonpay.png | Bin 0 -> 1100 bytes .../images/payment_methods/72x72/bacs.png | Bin 0 -> 485 bytes .../images/payment_methods/72x72/cod.png | Bin 0 -> 537 bytes .../images/payment_methods/72x72/klarna.png | Bin 0 -> 710 bytes .../payment_methods/72x72/mercadopago.png | Bin 0 -> 1435 bytes .../images/payment_methods/72x72/mollie.png | Bin 0 -> 637 bytes .../images/payment_methods/72x72/payfast.png | Bin 0 -> 766 bytes .../images/payment_methods/72x72/paypal.png | Bin 0 -> 1229 bytes .../images/payment_methods/72x72/paystack.png | Bin 0 -> 736 bytes .../images/payment_methods/72x72/payu.png | Bin 0 -> 673 bytes .../images/payment_methods/72x72/razorpay.png | Bin 0 -> 706 bytes .../images/payment_methods/72x72/square.png | Bin 0 -> 609 bytes .../images/payment_methods/72x72/stripe.png | Bin 0 -> 2783 bytes .../DefaultPaymentGateways.php | 26 +++++++++--------- 18 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/afterpay.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/amazonpay.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/bacs.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/cod.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/mercadopago.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/payfast.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/paypal.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/payu.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/square.png create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss index dd2b03ecafe..cb524da6945 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.scss @@ -6,14 +6,15 @@ overflow: hidden; .components-card__media { - width: 170px; + width: 85px; flex-shrink: 0; + align-self: flex-start; img, svg, .is-placeholder { margin: auto; - max-width: 100px; + max-width: 36px; display: block; } diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index b79adf43dac..9c552cd887d 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -309,15 +309,15 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { onToggle={ trackToggle } > { additionalSection } - { enabledSection } { offlineSection } + { enabledSection } ) : ( <> { additionalSection } - { enabledSection } { offlineSection } + { enabledSection } ) }
diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png b/plugins/woocommerce/assets/images/payment_methods/72x72/affirm.png new file mode 100644 index 0000000000000000000000000000000000000000..9401ad1b670d7b66138fb5e29451005fcb38e57a GIT binary patch literal 972 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 WLRB?bfTtQ5UkskEelF{r5}E)SrR~H3 literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png b/plugins/woocommerce/assets/images/payment_methods/72x72/cod.png new file mode 100644 index 0000000000000000000000000000000000000000..f2ba5f688886bb7dc1a34959e4257aecd7899fe0 GIT binary patch literal 537 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAjKY=BRQE0F&A{>RU^AoTy;{~vF^ z1JU=l-+&}gcl-7D5FH_k6F)<4(TBuw%8zZ#$gLM1_d!3+xb`vn%fPdIAux`S- zBZ)bRhkKZg&FvG-U1k-P9@3+y?R7kB>#-?QJa_6XUy#Ga>(A}qx3c$D;o{v9fv`)vGn*n8ekI}~hxp5bKag5yeiwB@F2+*CO9^M9@NUgtSWI(}uwb;e&a zJm%Te!8XS(aFcIK?5vq>7M}l7`VY3-`NjQFPZD1%kga%RmefwO(-S%5S53aD(wsN9 zRW$p$Dm%w2?GDf6ls28-X_CK9bjt-=s~o1=OBrYNZkuv0y0fUEaN9Z|mG{f|Uz$jqwboFyt=akR{04y>B6951J literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png b/plugins/woocommerce/assets/images/payment_methods/72x72/klarna.png new file mode 100644 index 0000000000000000000000000000000000000000..a97abed5981cb73b7da2e6b80d1794e0f47d6273 GIT binary patch literal 710 zcmV;%0y+JOP)YT5!d4_>jV1QL%dsAF= zOIBz{O=Lq$V?s(^Jw-$DT>(0#A~=iAqTdz0 z+!pk!%_B@AGtrHbQeIb^!y-|cp-7^rN**!UGKeB9FQSWr9FvePnqoozkSucNMX0tw z7HuRFl10M-hF(Gyr5;9V21L=)Qy9%-n#c|F(vzn^6Kz!K8BBmE+8wlf1F8rnKCb?X zEP{7jl^2jjK_-zXrix(Ymo-hQhZo=)ieR?;a7&EGU&my^hd;WMz z{`#NxEcy>3^A^I@S_rnLhON!wYy6xBaA0G%&Va?%u5%6$gu^h>;2pBi$3EPHD8w-z sjoCt{W^z}0?R`1M$M(w-s=v z&(honpV#T~^*X2APF8_6J#X9W^M;G5n9u3L#nY~`#)5~Y$;{ccxyqcNxU<^reZ=Mr zkjH3mlLe>W^78e~=JBP}>urM#S=w~ET>RkPp|c&-;{o*P}0B|LF8K5yLK zC4pImH|L12#`QHDWMfwJ1|gOjm; zkF7&~sU>x&S#_LVWQB+XbxF~WH8ee%Vm>RaFY+kN7IQxFxcYkKL=f?&TBQ@=HNB*ww|PkLV?Z0ezFjZ{25US(T}|nWr02$bVdfr zWaSjwC)y-Ffn-r3$hF1wy-4~{XwhyPYjvRnDG6isZmM&D1%5_Wbfl))}ysQ9W#4jaYNzBfvM;(@QHdM}zWO8qsYFc@u} z*Q6X=FfXq3UUV$9=GD=#pbeu!$EkS)AMTP9ZxYjgSLUeIMj1jwjAFdImCblsP9#{p;;K02t5FQ$+j8sG@5BIeL29sYq+v2vn{A4R)jjhCWH zgD;d|S&@|i`?NHD1V-ST6)Y>d0qqt0=6B~C-W0*IqW=_(t$-0^tZ1brtfpmb1#}gV zXJrf&Bh?@TN6kZB(H|I2cma+sgn|w-&p1HbLkGV(+*Iq-P_3Iqy^bFfGgS-%cGx+n z<1+>ZR%#f;%D_MsCvi}BNP~fu(m@th1}NaCY}kh%WCq+#l)DEKP8?*1Gx~rXD>-?E pnFsD1l;J~mPEv*s1-Ouh4*`z1w%YBMLP`Jt002ovPDHLkV1j(M#^(S4 literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png b/plugins/woocommerce/assets/images/payment_methods/72x72/mollie.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec678962371b897a673caf825735de3b41c3e2d GIT binary patch literal 637 zcmV-@0)qXCP)fUY;1dce0zI) z$jQlEU0r;9f`R}501$LiPE!EipAe7l&tM=R5buyMFfaj$Q6T^T0jfzvK~z}7?U>ze z+b|4;8Ou(bblrF?(UE`BIBEC(uQ=IjF$-M^C*s90_`47!UVJzrU;`BW!R)ea!HX%j z+0M{s_IbhglkF+SeB%en<2W0Bn9VHIkKjo#eUI~`XEMQ)Xfl&1>T^QJ_pHEpxr|vQ zl3@*=4DDEj9c8RSMq5^4O9iX&M6ARrT1BhqpAuc#E8DhZY5OKgbVSV+z!s=gM6UBH ziI(@sof{d~v%H_1vyDU|ZY0iU7;(fM(iP6;9KAaf(D1XzRLe zar#O$TdP-e9I$|kMIS5`g+%Tzi3E7D);88I6*-UI%GINeMB&g0yRL7RinzkWAhGDp z13lnnv51=i@U`Pu#Bv01l)F9=C+Xo=)P@?hF-vdoGdfK&M7hq* zh)PzVBvOf#YdvWkXrkOCdP;idd6VRMra*e8kY|R;AMW#kV#U<1|L_zr^7t zI`!%5^V{9&Yk2LWtK>ma^V{C+m7nT)h2>CR=1*VmvAFAplJ29c=Ur^_&C%sgU-8Y+ zsJG_K0000EbW%=J0N)VrkIx_=pkSXckY5qyV)y_60y9ZOK~z}7)tK3Gqc9LfWq~m` zGunkf?3w@n$7v`+jDac%^pM;)Q|8p|))iXNw{Z5nN5omtJJdl8U+Lowga43| zir02-Cv14&+L!zhSVeaX*)cqQmcLh}=#j=`IC*(eD=)X>;^a@Ki*WJPJ+ z$knqVAFI^T2UIZ1_R7cwm{J}fbM*HDNR>ONqckV`YM{E#8PrqS9DoV{Rn8y-Jv{)G zV+@=@6xh-Cul32rGD3k&+Ns`e< zN+G+b;z49K7#c)8l zJkp^upj9Gb$s~xzp=c6pv%yJ(bWn+5t;;i@Lt*n7Fg*)-uK~w_-RWQ!4W^M0Bmrh6 zn4gF2A;|6r{wqj%4hfIp%FALzG{zQCY*Js@ zjoE-ARgg~Ljn*j2a*w=^3G+Ka`|#rN#SBi0)52dt@M~8WBGsIx0Nak4Y5SwMB)x5qs+b^#N9Xq>@Rl?j5fO2iVfR`e|}2Jf69;s!-UPX$D5|N%~i#d z9Q@Ve&zlpqgW82TVd_S_FG82vC5{dOp5` z^Uuv|5sA?aU|wDpIWH^iZAs!awfEsrCa1D>jElrtWf9!I7QyX^9I1%A2IGQ?>oXKP zp`}+SGhX7)jCb8l`N34rloUUx;A4w?x;g-vQqH$v~CkX3ETc_!NnER5xw%q4C66mIdjM$N^KR#R&6OWHQctv z_+)2N4@WE@xdo%=>+%DP7G-?r*@W!s%fnCWqt>%5cnPMSbiw;9?88sG`C|gYJl%J8 zJ3BY)X|?is!O{+8R|7RhU;I(2pf(+JjVkom_5^i5>&twPXJ+NXkY3xZesqk~o4a<# zaDW?_;`z4mLtVb3y}!l3u^Ez(TnPp9h$W$o9B8E3KNu#9@e@qZJe!L9dZu6|<8)d8 z#v>pt6HRsv>nwPs*GAVTzc?g4Tj&vt?dPMEIH&RS9S<6{+w``cl2&cmPK`Vk9;Z7* z_$eu_i`{iD^N8A{QT_gs5PF3cIj>Ta&{M)2_PfU;uZa zj2z6nLcF8CiT%s%oKa(za(2b5n#Dt#BYZh!ZrdHc4?5aQn~<>;N>pp7d3=+r@?LGN zfrX(}a>i%6{Br8;)b*)P?BDAZ_u*2-UL}m1AvFtDEKP;wm`}7dL%w#}VPlcPLDpjw Qf_zF;M;iJ1KA+_O03WO-S^xk5 literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png b/plugins/woocommerce/assets/images/payment_methods/72x72/paystack.png new file mode 100644 index 0000000000000000000000000000000000000000..362446dfb00748432b989e3910bc042b69767a98 GIT binary patch literal 736 zcmV<60w4W}P))?KTy1=dm!Zhc(tC-OU2S|G%J+PUl@TaFsLx000kkQchC< z-yjf=?$4iKpfK;CUtl7?sw)5h0qsddK~z}7?U>zi+At7C9Sj)Kr29okviuWc7ds|N zllJ|et}EP3P~B!4*o$Pwhd~CJIoe&}3vKjDyh@e}`fnD?^>rMLnavmUe%7-nB%afW z%)@Xl=wvRAg1ThS8Kz5tc!HQ>_MB-Nb}y*&85N(A06w`jkw{1}N$@3(RwTlTGZP_+ zmL$S5T98bUA`)T}O`=IOi9V$$|49Kg?4P2$n}Iue3k3?WW6|xgD7!CIxhK=A%vpC& zRJpWOU2Zk_o9)WNQOZ9tEHW+&MBg{Rc{nVLEfi@U;S`KPQ^IN80$Nqr@-%7iD}zfm=B#WF>T zB~I&1B6eJtY9NTUG^Cm=Qf(ipMwC!%ERI5Er#vf*`79a}f4zEN`!HD@Me_?^m@cv+ Szn=F100007Qqc9LfLBtq4$@GnZaO8nvU1?B;Qv zXfsbYp^ohNf$1oaXO>B^=ZB`Wy?M{u8_!9AFkDF_5=oH+g~}=tq2kFzD6>6@u+It- zp~y&xhv*^tzarqbiHIDWj0k{?$lOchHRI_Uw7xvlxZly_5HJIMMFSX5Ob62UMGm92 zwgyi01tEq`L;Y)OFcj$F;*{TudTqJ|BF?TyFx?y3I#o7yx1x||iKvFQcA6~EHh7Qq zqTV7hpvW7V7R~u-ok%mBqNYd->zzp(YgdU9>?42T{f)3;%-EV=C32Q&22bw}rmH&^ zop2)Zobf7hOFN1Q+B90MnJb!zMj#f}18_@4#?V^ZH` z8(eUDsi=2=i>-4kxU@z+B4K*EFB%5ApeL?<@bOpMVLJQ6E}NFN{%6;iN7yLR;RO{C zHK8gAQ6FB=kNm<9b2Y#DL$%!7$GQ6sDfwI00000NkvXX Hu0mjf9|}_& literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png b/plugins/woocommerce/assets/images/payment_methods/72x72/razorpay.png new file mode 100644 index 0000000000000000000000000000000000000000..01dedd7bfa6d6a2e77d02a56d9430cbf0ac8d5d6 GIT binary patch literal 706 zcmV;z0zLhSP)>xyy2H+Xj;3IAk>~2|Z-JX(bdkEk&NG$& z*xll*wZvDa{zsqwmB#yIt^Kpr`rhmK(dGDilCh4Qu#KFse7gLh%=&q=@otyKjhnD? zh^BCXoDOni<^TWy5_D2dQve|E-=7f9kYL_05D<@F&ySC^5JYSM00Fv5L_t(o!|j;I zZlq8UMISVDc6;Vly+BT#oG1VPrwvFW4K4S=hT2%>NN)#6w+h~(O8qBX2Dd)mjDH*K zLiHMN;p6=Tp1LIbf`Qyz#+hL-vtFom>ltBMOXTTi3R}-eQ>Z$6eIC_u4+3<>1rT7N z)D{x>p;|(Or3xTIpym)^u6&5#D+n=(Ceb9CME`jBE+aah?j=Nz$EJBqnK}M7PIPy^ zJK@EpY0Tq9PrsXQ&>BUY?=@9iSIle^O>7ZI&bEgvo*ij@V1p{VE^6QX+1!I#wBg9) zRG5@?!kLMql*-gnmbl1LnoyollCilesvXgf<{_mzC3~O*Rg68Cumr?;=76$2To3G47l7e$XOlGR3L$+Gff);_DX%p$7I(__0B+~V!Z literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/square.png b/plugins/woocommerce/assets/images/payment_methods/72x72/square.png new file mode 100644 index 0000000000000000000000000000000000000000..3ffe48772223206ab451e309721f00d94b8c0bf0 GIT binary patch literal 609 zcmV-n0-pVeP)`O@9!5E7at!Vz`?<$r>ByVlJWBL+S}Xn^z_Zn&aber zZf%X~U1_3? z0000IbW%=J03Z} z>Mp0ClgR)PoH})v|Nq|>n=E7Cnzoz|y7_)JdD2D*?IGV$FBo&sj*nfBl5P&;V4mx= z7|Osh_H#J{ux`*;)JQtk(tT?^z1Kvg(@UZfV$eG#HwFkDH|GMNEQta}LrOeA$s;Zf z6c8T=@(IU*h6IrMcTl$8PEWO~%EAPCo{C?4$@QVE7K-9R8%l+;R2!P8p`|vo7qUar z>L6rWZRkN9+pMBi`BMHcF@ur_I<0J=N9Y#)DX2_kZ@r-Av$^OAJ$*s*D|9)~JJ#P1 zuXEYrd|r_=f|3k61LP)Xjt@Tp0GwtflIq);00000NkvXXu0mjfmTV}= literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png b/plugins/woocommerce/assets/images/payment_methods/72x72/stripe.png new file mode 100644 index 0000000000000000000000000000000000000000..0175f2947d6d670265d5c6f0780450607e91346c GIT binary patch literal 2783 zcmV<53Ly1~P)XIJX+^(fc<*Iw z>1ag3VSMssY3OE2#sJg*V|MLiZ|h)r@o7Z9Wozdv!~Zy{`e$Y3WKPTg&i^CE|2M1q z6UqN3w*6yt?qzN20LuRW-2VX5|6+CWV{q(iL&5+5|7LFMWpeFhaO`Gm>SS~7V1V;q zg7ju*=4nR3XKLwYY3E^n@@8V+StGsryHu`)Eta0M`FtqW?^v z{7{tlQI7PXrt^Dv=F;5#*WCQx-}}tc`^Culnx^%&we**q@ppjkWlhWZ`Tz3r{pacZ z;O70n(EQZb`@PBgJ*E1$!ug-L`PkU_!^HPxkoU^U_Nub>RF3wLp!H#X^NW}Ad5Q9g zi|%oI?T(4)8NUBRt^ZV||6QB^Or-uTwEa|_{iVG6vc36oj`V1F@NjnOYiQ(hZQ=mO z|JC6BTb2ASultj*`az-ke4qGxp7?~A^QIHSE<;EtsN8T8a{mZgbCwe%}k!Td-v+q zt5>WD3kyr?nG_P@Ubk|YS_O#hDq60hzrTMc0VxL=aFTZ;15i>@Qi!{I-;NzScInc& zOhGHAcsWp%GkOOy=rAZbIT_<%9+#wGbox>UR#Rh~LYz^Ma%RGUJU%M|fN^mN36^;} z4{Y1EZAmRuYikD}@ovGvG?Ck8ordLim@RvgqLcRR_ z`t%7x2J&EC(B@#-X5DGirU@;~UxR3W_Kykm8srB~P|%FtJ^&33qj|7B%Argmb=tKn z4^V08fU^JOkkC*sFRwv^Fb?LSal)7*+avlz8GyQ1gAUO^IPsHXhV<_rDjYxV$UGt7 zi1u*DiN$Hxy?coQq$Vh%_X9En7;wDM!S+B|P#5H34)mw5Rb4C&I29izy#9?h#bwQhzgLQ2;nLIta5vx3+&pvGK@BZYR zNIex3lahAgYQ{aa4aL|YPc$;!x)Q5A$jCeouEaR%E9QLeU(La~q9G4DU5RkKq$B$L z?uw)BTIR&+5_wF}A8-&-4j(%*P}hS!j&>k5_GpZQPW$!@GHf4~=Yguq2qh}VRXPA6 zqt!KoZ^NUf9rK5rjgBWU=yV{4ak1M6uAi{$j&>kEb~!H0!yt9Mf>?j>SR8e<3-NKW z{)jvsIuJu1c`S15LSh@5urB3z5OplXErz$DKKaz^SFdKC*ux=S7ck73MxEszMBP5< z@Js%!*6JKM6}X-Aa?i&A1PrT7wg=*Pcnl!=Htd0Y#@kS9Q4Dq1Hhi6ZW|=r$-Bew` z4Dcpw8|uf@#R$ET4z}l~I$l^7I?K`VCXfdlzJ0`PcvRbs$KRlnXgKY>J)j6Dl8Eh$ zapdj%HdSkOT1Fl{UGf(Dg>htmm}4e#JEJ2mRd?flOv%i>kB%A_I;yTnw}f$oe*|$} z+D9kXdjIUny2BiR4D*UiNH{WX!Pv2+kA6Et9(JpCIcvKY&t$u9$L*|*D-sY`3l@xx zAYvPeJi+Wzy?4sdcAdCu@-$(b+iVvm967?6u@Mmj9o}Z_v%KZ+<#&z5+@g7=(Rq0Q z1kAW`-~hz8q3y>#FHHez-O{#?M*=zvs56q#Tb?@j)?jrZ1iLUJ#TMH0ir0n5B0b!$QVD$chdCQsN)Te`6fEZAjD1= z?K5w)`k2a(`)^IkMY?WS_(6L3@<`Bu%zXakPAW;eIN5!<$LC!BD{ z-T0=Q6b+I5&Bu=Tmee6Q# zEK#&BbOPh3gUqmD1naVK>_TU+nSo)>l)zZ&03_vd#`TW|V=&6ubCj_}R6iA!e=FaT9B+&=u6 z%145@4K0?aC^Ilyr4tty8=KA?3{;0y7LNM3>u*DGi$Pty(Y!wZ#ivu};=FnL_BAAB z&`C@LMt86-RH%cDnKQzHJn`}A()rYw)Y9uxZy!|`-ag)#$J#k`C>@=B&mO*?wX!)W zPyrbA6Jl+?y`Xj7Xt>1b3kW1-#)j& z5pPV3nK`~6pFG(+ckY}y`9g(@5WZSEbl0|_bg(~WZ1d8kOO_n83l(lb 'payfast', 'title' => __( 'PayFast', 'woocommerce' ), 'content' => __( 'The PayFast extension for WooCommerce enables you to accept payments by Credit Card and EFT via one of South Africa’s most popular payment gateways. No setup fees or monthly subscription costs. Selecting this extension will configure your store to use South African rands as the selected currency.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/payfast.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/payfast.png', 'plugins' => array( 'woocommerce-payfast-gateway' ), 'is_visible' => array( (object) array( @@ -40,7 +40,7 @@ class DefaultPaymentGateways { 'id' => 'stripe', 'title' => __( ' Stripe', 'woocommerce' ), 'content' => __( 'Accept debit and credit cards in 135+ currencies, methods such as Alipay, and one-touch checkout with Apple Pay.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/stripe.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/stripe.png', 'plugins' => array( 'woocommerce-gateway-stripe' ), 'is_visible' => array( // https://stripe.com/global. @@ -96,7 +96,7 @@ class DefaultPaymentGateways { 'id' => 'paystack', 'title' => __( 'Paystack', 'woocommerce' ), 'content' => __( 'Paystack helps African merchants accept one-time and recurring payments online with a modern, safe, and secure payment gateway.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/paystack.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/paystack.png', 'plugins' => array( 'woo-paystack' ), 'is_visible' => array( self::get_rules_for_countries( array( 'ZA', 'GH', 'NG' ) ), @@ -107,7 +107,7 @@ class DefaultPaymentGateways { 'id' => 'kco', 'title' => __( 'Klarna Checkout', 'woocommerce' ), 'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/klarna.png', 'plugins' => array( 'klarna-checkout-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( array( 'SE', 'FI', 'NO' ) ), @@ -118,7 +118,7 @@ class DefaultPaymentGateways { 'id' => 'klarna_payments', 'title' => __( 'Klarna Payments', 'woocommerce' ), 'content' => __( 'Choose the payment that you want, pay now, pay later or slice it. No credit card numbers, no passwords, no worries.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/klarna-black.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/klarna.png', 'plugins' => array( 'klarna-payments-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( @@ -143,7 +143,7 @@ class DefaultPaymentGateways { 'id' => 'mollie_wc_gateway_banktransfer', 'title' => __( 'Mollie', 'woocommerce' ), 'content' => __( 'Effortless payments by Mollie: Offer global and local payment methods, get onboarded in minutes, and supported in your language.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mollie.svg', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/mollie.png', 'plugins' => array( 'mollie-payments-for-woocommerce' ), 'is_visible' => array( self::get_rules_for_countries( @@ -167,7 +167,7 @@ class DefaultPaymentGateways { 'id' => 'woo-mercado-pago-custom', 'title' => __( 'Mercado Pago Checkout Pro & Custom', 'woocommerce' ), 'content' => __( 'Accept credit and debit cards, offline (cash or bank transfer) and logged-in payments with money in Mercado Pago. Safe and secure payments with the leading payment processor in LATAM.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/mercadopago.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/mercadopago.png', 'plugins' => array( 'woocommerce-mercadopago' ), 'is_visible' => array( self::get_rules_for_countries( array( 'AR', 'BR', 'CL', 'CO', 'MX', 'PE', 'UY' ) ), @@ -179,7 +179,7 @@ class DefaultPaymentGateways { 'id' => 'ppcp-gateway', 'title' => __( 'PayPal Payments', 'woocommerce' ), 'content' => __( "Safe and secure payments using credit cards or your customer's PayPal account.", 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/paypal.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/paypal.png', 'plugins' => array( 'woocommerce-paypal-payments' ), 'is_visible' => array( (object) array( @@ -194,7 +194,7 @@ class DefaultPaymentGateways { 'id' => 'cod', 'title' => __( 'Cash on delivery', 'woocommerce' ), 'content' => __( 'Take payments in cash upon delivery.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/cod.svg', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/cod.png', 'is_visible' => array( self::get_rules_for_cbd( false ), ), @@ -204,7 +204,7 @@ class DefaultPaymentGateways { 'id' => 'bacs', 'title' => __( 'Direct bank transfer', 'woocommerce' ), 'content' => __( 'Take payments via bank transfer.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/bacs.svg', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/bacs.png', 'is_visible' => array( self::get_rules_for_cbd( false ), ), @@ -324,7 +324,7 @@ class DefaultPaymentGateways { 'id' => 'razorpay', 'title' => __( 'Razorpay', 'woocommerce' ), 'content' => __( 'The official Razorpay extension for WooCommerce allows you to accept credit cards, debit cards, netbanking, wallet, and UPI payments.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/razorpay.svg', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/razorpay.png', 'plugins' => array( 'woo-razorpay' ), 'is_visible' => array( (object) array( @@ -339,7 +339,7 @@ class DefaultPaymentGateways { 'id' => 'payubiz', 'title' => __( 'PayU for WooCommerce', 'woocommerce' ), 'content' => __( 'Enable PayU’s exclusive plugin for WooCommerce to start accepting payments in 100+ payment methods available in India including credit cards, debit cards, UPI, & more!', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/payu.svg', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/payu.png', 'plugins' => array( 'payu-india' ), 'is_visible' => array( (object) array( @@ -365,7 +365,7 @@ class DefaultPaymentGateways { 'id' => 'square_credit_card', 'title' => __( 'Square', 'woocommerce' ), 'content' => __( 'Securely accept credit and debit cards with one low rate, no surprise fees (custom rates available). Sell online and in store and track sales and inventory in one place.', 'woocommerce' ), - 'image' => WC()->plugin_url() . '/assets/images/square-black.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/square.png', 'plugins' => array( 'woocommerce-square' ), 'is_visible' => array( (object) array( From adfdeabc6c6040b59e719a661cd6a0ebb90ea12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Mon, 11 Apr 2022 09:40:48 +0200 Subject: [PATCH 089/206] Add a couple of missing spaces (code formatting). --- .../importers/class-wc-product-csv-importer-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php index 97acf002e73..17b13f81653 100644 --- a/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/plugins/woocommerce/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -97,7 +97,7 @@ class WC_Product_CSV_Importer_Controller { * @param bool $check_import_file_path If the import file path should be checked. * @param string $file Path of the file to be checked. */ - return wc_is_file_valid_csv($file, $check_path); + return wc_is_file_valid_csv( $file, $check_path ); } /** From 1f77200e7050880f54a33c9270929c7ed9686de0 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 11 Apr 2022 15:57:26 +0800 Subject: [PATCH 090/206] Add missing track prop --- .../fills/PaymentGatewaySuggestions/index.js | 22 ++++++++++--------- .../PaymentGatewaySuggestions/test/index.js | 1 + 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index 9c552cd887d..084a4711efa 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -161,16 +161,6 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { [ paymentGateways ] ); - const trackSeeMore = () => { - recordEvent( 'tasklist_payment_see_more', {} ); - }; - - const trackToggle = ( isShow ) => { - recordEvent( 'tasklist_payment_show_toggle', { - toggle: isShow ? 'hide' : 'show', - } ); - }; - const recommendation = useMemo( () => Array.from( paymentGateways.values() ) @@ -243,6 +233,18 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { const isEligibleWCPay = !! wcPayGateway.length; + const trackSeeMore = () => { + recordEvent( 'tasklist_payment_see_more', {} ); + }; + + const trackToggle = ( isShow ) => { + recordEvent( 'tasklist_payment_show_toggle', { + toggle: isShow ? 'hide' : 'show', + payment_method_count: + offlineGateways.length + additionalGateways.length, + } ); + }; + if ( query.id && ! currentGateway ) { return ; } diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js index 7df91e7a82a..c2ca2617ff0 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/test/index.js @@ -270,6 +270,7 @@ describe( 'PaymentGatewaySuggestions', () => { 'tasklist_payment_show_toggle', { toggle: 'show', + payment_method_count: paymentGatewaySuggestions.length - 1, // Minus one for WCPay since it's not counted in "other payment methods". } ); } ); From 52732f2bc1072f7d6d9ff06ea65f86198d4d6990 Mon Sep 17 00:00:00 2001 From: Sakri Koskimies Date: Mon, 11 Apr 2022 11:06:08 +0300 Subject: [PATCH 091/206] Add changelog entry --- packages/js/components/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/components/CHANGELOG.md b/packages/js/components/CHANGELOG.md index 1453039b5d5..c0d21596f66 100644 --- a/packages/js/components/CHANGELOG.md +++ b/packages/js/components/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Fix documentation for `TableCard` component - Update dependency `@wordpress/hooks` to ^3.5.0 - Update dependency `@wordpress/icons` to ^8.1.0 From 812a11e31b7ea0b6437180255002de662caa4de0 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Mon, 11 Apr 2022 16:21:48 +0800 Subject: [PATCH 092/206] Add changelog --- .../changelog/dev-32131-ui-changes-additional-payment-section | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/dev-32131-ui-changes-additional-payment-section diff --git a/plugins/woocommerce/changelog/dev-32131-ui-changes-additional-payment-section b/plugins/woocommerce/changelog/dev-32131-ui-changes-additional-payment-section new file mode 100644 index 00000000000..f914a935799 --- /dev/null +++ b/plugins/woocommerce/changelog/dev-32131-ui-changes-additional-payment-section @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +UI changes for set up payments task From 21ecb88c4c167f33555cf9ad246a8def22350653 Mon Sep 17 00:00:00 2001 From: Nestor Soriano Date: Mon, 11 Apr 2022 10:22:13 +0200 Subject: [PATCH 093/206] Fix PHPCS issues in URLTests.php --- .../php/src/Internal/Utilities/URLTest.php | 108 +++++++++--------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php index 95ab9838164..70f89a06cc0 100644 --- a/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php +++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/URLTest.php @@ -9,9 +9,13 @@ use WC_Unit_Test_Case; * A collection of tests for the filepath utility class. */ class URLTest extends WC_Unit_Test_Case { + + /** + * @testdox Test if it can be determined whether a URL is absolute or relative. + */ public function test_if_absolute_or_relative() { $this->assertTrue( - ( new URL( '/etc/foo/bar' ) )->is_absolute() , + ( new URL( '/etc/foo/bar' ) )->is_absolute(), 'Correctly determines if a Unix-style path is absolute.' ); @@ -29,8 +33,8 @@ class URLTest extends WC_Unit_Test_Case { /** * @dataProvider path_expectations * - * @param string $source_path - * @param string $expected_resolution + * @param string $source_path Source path to test. + * @param string $expected_resolution Expected result of the test. */ public function test_path_resolution( $source_path, $expected_resolution ) { $this->assertEquals( $expected_resolution, ( new URL( $source_path ) )->get_path() ); @@ -43,22 +47,22 @@ class URLTest extends WC_Unit_Test_Case { */ public function path_expectations(): array { return array( - array( '/var/foo/bar/baz/../../foobar', '/var/foo/foobar' ), - array( '/var/foo/../../../../bazbar', '/bazbar' ), - array( '././././.', './' ), - array( 'empty/segments//are/stripped', 'empty/segments/are/stripped' ), - array( '///nonempty/ /whitespace/ /is//kept', '/nonempty/ /whitespace/ /is/kept' ), - array( 'relative/../../should/remain/relative', '../should/remain/relative' ), + array( '/var/foo/bar/baz/../../foobar', '/var/foo/foobar' ), + array( '/var/foo/../../../../bazbar', '/bazbar' ), + array( '././././.', './' ), + array( 'empty/segments//are/stripped', 'empty/segments/are/stripped' ), + array( '///nonempty/ /whitespace/ /is//kept', '/nonempty/ /whitespace/ /is/kept' ), + array( 'relative/../../should/remain/relative', '../should/remain/relative' ), array( 'relative/../../../should/remain/relative', '../../should/remain/relative' ), - array( 'c:\\Windows\Server\HTTP\dump.xml', 'c:/Windows/Server/HTTP/dump.xml') + array( 'c:\\Windows\Server\HTTP\dump.xml', 'c:/Windows/Server/HTTP/dump.xml' ), ); } /** * @dataProvider url_expectations * - * @param string $source_url - * @param string $expected_resolution + * @param string $source_url URL to test. + * @param string $expected_resolution Expected result of the test. */ public function test_url_resolution( $source_url, $expected_resolution ) { $this->assertEquals( $expected_resolution, ( new URL( $source_url ) )->get_url() ); @@ -71,34 +75,34 @@ class URLTest extends WC_Unit_Test_Case { */ public function url_expectations(): array { return array( - array( '/../foo/bar/baz/bazooka/../../baz', 'file:///foo/bar/baz' ), - array( './a/b/c/./../././../b/c', 'file://a/b/c' ), - array( 'relative/path', 'file://relative/path' ), - array( '/absolute/path', 'file:///absolute/path' ), - array( '/var/www/network/%2econfig', 'file:///var/www/network/%2econfig' ), - array( '///foo', 'file:///foo' ), - array( '~/foo.txt', 'file://~/foo.txt' ), - array( 'baz///foo', 'file://baz/foo' ), - array( 'file:///etc/foo/bar', 'file:///etc/foo/bar' ), - array( 'foo://bar', 'foo://bar/' ), - array( 'foo://bar/baz-file', 'foo://bar/baz-file' ), - array( 'foo://bar/baz-dir/', 'foo://bar/baz-dir/' ), - array( 'https://foo.bar/parent/.%2e/asset.txt', 'https://foo.bar/asset.txt' ), - array( 'https://foo.bar/parent/%2E./asset.txt', 'https://foo.bar/asset.txt' ), + array( '/../foo/bar/baz/bazooka/../../baz', 'file:///foo/bar/baz' ), + array( './a/b/c/./../././../b/c', 'file://a/b/c' ), + array( 'relative/path', 'file://relative/path' ), + array( '/absolute/path', 'file:///absolute/path' ), + array( '/var/www/network/%2econfig', 'file:///var/www/network/%2econfig' ), + array( '///foo', 'file:///foo' ), + array( '~/foo.txt', 'file://~/foo.txt' ), + array( 'baz///foo', 'file://baz/foo' ), + array( 'file:///etc/foo/bar', 'file:///etc/foo/bar' ), + array( 'foo://bar', 'foo://bar/' ), + array( 'foo://bar/baz-file', 'foo://bar/baz-file' ), + array( 'foo://bar/baz-dir/', 'foo://bar/baz-dir/' ), + array( 'https://foo.bar/parent/.%2e/asset.txt', 'https://foo.bar/asset.txt' ), + array( 'https://foo.bar/parent/%2E./asset.txt', 'https://foo.bar/asset.txt' ), array( 'https://foo.bar/parent/%2E%2e/asset.txt', 'https://foo.bar/asset.txt' ), array( 'https://foo.bar/parent/%2E.%2fasset.txt', 'https://foo.bar/parent/%2E.%2fasset.txt' ), - array( 'http://localhost?../../bar', 'http://localhost/?../../bar' ), - array( '//http.or.https/', '//http.or.https/' ), - array( '//schemaless/with-path', '//schemaless/with-path' ), + array( 'http://localhost?../../bar', 'http://localhost/?../../bar' ), + array( '//http.or.https/', '//http.or.https/' ), + array( '//schemaless/with-path', '//schemaless/with-path' ), ); } /** * @dataProvider parent_url_expectations * - * @param string $source_path - * @param int $parent_level - * @param string|false $expectation + * @param string $source_path Path to test. + * @param int $parent_level Parent level to use for the test. + * @param string|false $expectation Expected result of the test. */ public function test_can_obtain_parent_url( string $source_path, int $parent_level, $expectation ) { $this->assertEquals( $expectation, ( new URL( $source_path ) )->get_parent_url( $parent_level ) ); @@ -111,32 +115,32 @@ class URLTest extends WC_Unit_Test_Case { */ public function parent_url_expectations(): array { return array( - array( '/', 1, false ), - array( '/', 2, false ), - array( './', 1, 'file://../' ), - array( '../', 1, 'file://../../' ), - array( 'relative-file.png', 1, 'file://./' ), - array( 'relative-file.png', 2, 'file://../' ), - array( '/var/dev/', 1, 'file:///var/' ), - array( '/var/../dev/./../foo/bar', 1, 'file:///foo/' ), - array( 'https://example.com', 1, false ), - array( 'https://example.com/foo', 1, 'https://example.com/' ), - array( 'https://example.com/foo/bar/baz/../cat/', 2, 'https://example.com/foo/' ), + array( '/', 1, false ), + array( '/', 2, false ), + array( './', 1, 'file://../' ), + array( '../', 1, 'file://../../' ), + array( 'relative-file.png', 1, 'file://./' ), + array( 'relative-file.png', 2, 'file://../' ), + array( '/var/dev/', 1, 'file:///var/' ), + array( '/var/../dev/./../foo/bar', 1, 'file:///foo/' ), + array( 'https://example.com', 1, false ), + array( 'https://example.com/foo', 1, 'https://example.com/' ), + array( 'https://example.com/foo/bar/baz/../cat/', 2, 'https://example.com/foo/' ), array( 'https://example.com/foo/bar/baz/%2E%2E/dog/', 2, 'https://example.com/foo/' ), - array( 'file://./', 1, 'file://../' ), - array( 'file://./', 2, 'file://../../' ), - array( 'file://../../foo', 1, 'file://../../' ), - array( 'file://../../foo', 2, 'file://../../../' ), - array( 'file://../../', 1, 'file://../../../' ), - array( 'file://./../', 2, 'file://../../../' ), + array( 'file://./', 1, 'file://../' ), + array( 'file://./', 2, 'file://../../' ), + array( 'file://../../foo', 1, 'file://../../' ), + array( 'file://../../foo', 2, 'file://../../../' ), + array( 'file://../../', 1, 'file://../../../' ), + array( 'file://./../', 2, 'file://../../../' ), ); } /** * @dataProvider all_parent_url_expectations * - * @param string $source_path - * @param array $expectation + * @param string $source_path Path to test. + * @param array $expectation Expected result of the test. */ public function test_can_obtain_all_parent_urls( string $source_path, array $expectation ) { $this->assertEquals( $expectation, ( new URL( $source_path ) )->get_all_parent_urls() ); @@ -193,7 +197,7 @@ class URLTest extends WC_Unit_Test_Case { array( '../../some.file', array( - 'file://../../' + 'file://../../', ), ), ); From 700633957cb4e3591c82ae8e1cbba06ca8ade035 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 11 Apr 2022 08:08:28 -0400 Subject: [PATCH 094/206] Update progress header bar styles in task list (#32498) * Update progress header bar styles in task list * Remove units on margin * Add changelog entry --- .../client/task-lists/progress-header/progress-header.scss | 4 ++-- plugins/woocommerce/changelog/fix-32425 | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-32425 diff --git a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss index e51ab9b22a7..4d45cfea491 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss +++ b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.scss @@ -19,9 +19,9 @@ $progress-complete-color: #007cba; appearance: none; border: 1px solid #ddd; border-radius: 16px; - height: 10px; + height: 12px; width: 100%; - margin-bottom: 20px; + margin-bottom: 0; // Firefox & { diff --git a/plugins/woocommerce/changelog/fix-32425 b/plugins/woocommerce/changelog/fix-32425 new file mode 100644 index 00000000000..ebb4792044a --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32425 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Update progress header bar styles in task list #32498 From 10e9c6c5f9534b39c7eb0082b640eedc9f78881b Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 14:01:26 -0300 Subject: [PATCH 095/206] Fix experiment UI issues --- .../client/two-column-tasks/style.scss | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/two-column-tasks/style.scss index bf3a58ac0db..6b6caf8c4d0 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/style.scss @@ -19,10 +19,10 @@ flex: 1; } - .woocommerce-ellipsis-menu.setup { + .woocommerce-ellipsis-menu { position: absolute; - top: 20px; - right: 16px; + top: $gap; + right: $gap-large; } .woocommerce-task-card.is-loading { @@ -130,11 +130,11 @@ } .woocommerce-task-header__contents { - max-width: 380px; + max-width: calc( 60% - 2% ); } .svg-background { - right: 0.5%; + right: 2%; width: 40%; } } @@ -143,6 +143,13 @@ @include single-column; } + &.two-columns .svg-background { + top: 50%; + bottom: 50%; + margin-top: auto; + margin-bottom: auto; + } + ul { display: flex; li { @@ -229,10 +236,10 @@ position: absolute; z-index: 0; right: 6%; - top: 50%; - bottom: 50%; - margin-top: auto; - margin-bottom: auto; + // top: 50%; + // bottom: 50%; + // margin-top: auto; + // margin-bottom: auto; .admin-theme-color { fill: var(--wp-admin-theme-color); From 263bd5776ed66e5cb888f529583f2142f40b04a3 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Mon, 11 Apr 2022 06:16:52 -0700 Subject: [PATCH 096/206] Correct text-domain. --- .../Controllers/Version2/class-wc-rest-orders-v2-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php index 30b012c9096..b1eb4ee5745 100644 --- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php +++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -1852,7 +1852,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller { ); $params['order_item_display_meta'] = array( 'default' => false, - 'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocomerce' ), + 'description' => __( 'Only show meta which is meant to be displayed for an order.', 'woocommerce' ), 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'validate_callback' => 'rest_validate_request_arg', From c3b1352f904ff43348c2f3a9ff00a473254f5184 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 14:02:02 -0300 Subject: [PATCH 097/206] Make sure activity header does not show when displaying task list with progressive header --- .../client/homescreen/layout.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/homescreen/layout.js b/plugins/woocommerce-admin/client/homescreen/layout.js index 282572d6b7b..e996bb08b09 100644 --- a/plugins/woocommerce-admin/client/homescreen/layout.js +++ b/plugins/woocommerce-admin/client/homescreen/layout.js @@ -64,6 +64,8 @@ export const Layout = ( { query, taskListComplete, hasTaskList, + showingProgressHeader, + isLoadingTaskLists, shouldShowWelcomeModal, shouldShowWelcomeFromCalypsoModal, isTaskListHidden, @@ -141,7 +143,9 @@ export const Layout = ( { { ! isLoadingExperimentAssignment && ! isLoadingTwoColExperimentAssignment && - ! isRunningTaskListExperiment && ( + ! isRunningTaskListExperiment && + ! isLoadingTaskLists && + ! showingProgressHeader && ( 0, + showingProgressHeader: !! taskLists.find( + ( list ) => list.isVisible && list.displayProgressHeader + ), taskListComplete: getTaskList( 'setup' )?.isComplete, installTimestamp, installTimestampHasResolved, From 5251aa6e97d3940b11cd09a5eb6dc0674edfe8f3 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 6 Apr 2022 14:33:30 -0300 Subject: [PATCH 098/206] Fix lint issue --- plugins/woocommerce-admin/client/two-column-tasks/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/two-column-tasks/style.scss index 6b6caf8c4d0..59bb4369e6c 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/style.scss @@ -130,7 +130,7 @@ } .woocommerce-task-header__contents { - max-width: calc( 60% - 2% ); + max-width: calc(60% - 2%); } .svg-background { From 8eb03eba4627e5d0ab05f82711e8dc48559b0dcc Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 11 Apr 2022 10:19:30 -0300 Subject: [PATCH 099/206] Remove commented out code --- plugins/woocommerce-admin/client/two-column-tasks/style.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/two-column-tasks/style.scss index 59bb4369e6c..a6f1dc686a1 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/style.scss @@ -236,10 +236,6 @@ position: absolute; z-index: 0; right: 6%; - // top: 50%; - // bottom: 50%; - // margin-top: auto; - // margin-bottom: auto; .admin-theme-color { fill: var(--wp-admin-theme-color); From 7065f678b72956106951805e6b579f70c75b49c3 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Mon, 11 Apr 2022 06:20:09 -0700 Subject: [PATCH 100/206] Add changelog. --- .../update-filter-out-product-variation-line-item-meta | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-filter-out-product-variation-line-item-meta diff --git a/plugins/woocommerce/changelog/update-filter-out-product-variation-line-item-meta b/plugins/woocommerce/changelog/update-filter-out-product-variation-line-item-meta new file mode 100644 index 00000000000..dac8fdd6c51 --- /dev/null +++ b/plugins/woocommerce/changelog/update-filter-out-product-variation-line-item-meta @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add `order_item_display_meta` option to orders endpoint (REST API), to osupport filtering out variation meta. From d58bbc112fbbe3a30d9c9f865806ad2990285727 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 4 Mar 2022 12:50:30 +0530 Subject: [PATCH 101/206] Add error logger class for custom table migration. --- .../Migrations/MigrationErrorLogger.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php new file mode 100644 index 00000000000..af3288beac8 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php @@ -0,0 +1,17 @@ + Date: Fri, 4 Mar 2022 20:24:32 +0530 Subject: [PATCH 102/206] Add generic meta to custom table migrator class, to be used in meta to custom table migrations. --- .../MetaToCustomTableMigrator.php | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php new file mode 100644 index 00000000000..7e353203474 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -0,0 +1,374 @@ + '%d', + 'decimal' => '%f', + 'string' => '%s', + 'date' => '%s', + ); + + /** + * MetaToCustomTableMigrator constructor. + * + * @param array $schema_config This parameters provides general but essential information about tables under migrations. Must be of the form- + * array( + * 'entity_schema' => + * array ( + * 'primary_id' => 'primary_id column name of source table', + * 'table_name' => 'name of the source table'. + * ), + * 'entity_meta_schema' => + * array ( + * 'meta_key_column' => 'name of meta_key column in source meta table', + * 'meta_value_column' => 'name of meta_value column in source meta table', + * 'table_name' => 'name of source meta table', + * ), + * 'destination_table' => 'name of destination custom table', + * 'entity_meta_relation' => + * array ( + * 'entity' => 'name of column in source table which is used in source meta table', + * 'meta' => 'name of column in source meta table which contains key of records in source table', + * ) + * ) + * ). + * + * @param array $meta_column_mapping Mapping information of keys in source meta table. Must be of the form: + * array( + * '$meta_key_1' => array( // $meta_key_1 is the name of meta_key in source meta table. + * 'type' => 'type of value, could be string/int/date/float', + * 'destination' => 'name of the column in column name where this data should be inserted in.', + * ), + * '$meta_key_2' => array( + * ...... + * ), + * .... + * ). + * + * @param array $core_column_mapping Mapping of keys in source table, similar to meta_column_mapping param, must be of the form: + * array( + * '$source_column_name_1' => array( // $source_column_name_1 is column name in source table. + * 'type' => 'type of value, could be string/int/date/float.', + * 'destination' => 'name of the column in column name where this data should be inserted in.', + * ), + * '$source_column_name_2' => array( + * ...... + * ), + * .... + * ). + */ + public function __construct( $schema_config, $meta_column_mapping, $core_column_mapping ) { + // TODO: Add code to validate params. + $this->schema_config = $schema_config; + $this->meta_column_mapping = $meta_column_mapping; + $this->core_column_mapping = $core_column_mapping; + } + + /** + * Generate SQL for data insertion. + * + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. + * @param string $insert_switch Insert command to use in generating queries, could be insert, insert_ignore, or replace. + * + * @return string Generated queries for insertion for this batch, would be of the form: + * INSERT/INSERT IGNORE/REPLACE INTO $table_name ($columns) values + * ($value for row 1) + * ($value for row 2) + * ... + */ + public function generate_insert_sql_for_batch( $batch, $insert_switch ) { + global $wpdb; + + // TODO: Add code to validate params. + $table = $this->schema_config['destination_table']; + + switch ( $insert_switch ) { + case 'insert_ignore': + $insert_query = 'INSERT IGNORE'; + break; + case 'replace': + $insert_query = 'REPLACE'; + break; + case 'insert': + default: + $insert_query = 'INSERT'; + } + + $columns = array(); + $placeholders = array(); + foreach ( array_merge( $this->core_column_mapping, $this->meta_column_mapping ) as $prev_column => $schema ) { + $columns[] = $schema['destination']; + $placeholders[] = $this->wpdb_placeholder_for_type[ $schema['type'] ]; + } + $placeholders = "'" . implode( "', '", $placeholders ) . "'"; + + $values = array(); + foreach ( array_values( $batch ) as $row ) { + $query_params = array(); + foreach ( $columns as $column ) { + $query_params[] = $row[ $column ] ?? null; + } + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in $this->>wpdb_placeholder_for_type. + $value_string = '(' . $wpdb->prepare( $placeholders, $query_params ) . ')'; + $values[] = $value_string; + } + + $value_sql = implode( ',', $values ); + + $column_sql = implode( '`, `', $this->escape_backtick( $columns ) ); + + return "$insert_query INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. + } + + /** + * Fetch data for migration. + * + * @param string $where_clause Where conditions to use while selecting data from source table. + * @param string $batch_size Batch size, will be used in LIMIT clause. + * @param string $order_by Will be used in ORDER BY clause. + * + * @return array[] Data along with errors (if any), will of the form: + * array( + * 'data' => array( + * 'id_1' => array( 'column1' => value1, 'column2' => value2, ...), + * ..., + * ), + * 'errors' => array( + * 'id_1' => array( 'column1' => error1, 'column2' => value2, ...), + * ..., + * ) + */ + public function fetch_data_for_migration( $where_clause, $batch_size, $order_by ) { + global $wpdb; + + // TODO: Add code to validate params. + $entity_table_query = $this->build_entity_table_query( $where_clause, $batch_size, $order_by ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared. + $entity_data = $wpdb->get_results( $entity_table_query ); + if ( empty( $entity_data ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + $entity_ids = array_column( $entity_data, 'primary_key_id' ); + + $meta_table_query = $this->build_meta_data_query( $entity_ids ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared. + $meta_data = $wpdb->get_results( $meta_table_query ); + + return $this->process_and_sanitize_data( $entity_data, $meta_data ); + } + + /** + * Helper method to build query used to fetch data from core source table. + * + * @param string $where_clause Where conditions to use while selecting data from source table. + * @param string $batch_size Batch size, will be used in LIMIT clause. + * @param string $order_by Will be used in ORDER BY clause. + * + * @return string Query that can be used to fetch data. + */ + private function build_entity_table_query( $where_clause, $batch_size, $order_by ) { + global $wpdb; + $entity_table = $this->escape_backtick( $this->schema_config['entity_schema']['table_name'] ); + $primary_id_column = $this->escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); + $entity_keys = $this->escape_backtick( array_keys( $this->core_column_mapping ) ); + $entity_column_string = '`' . implode( '`, `', $entity_keys ) . '`'; + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $entity_table, $primary_id_column and $entity_column_string is escaped for backticks. $where clause and $order_by should already be escaped. + $query = $wpdb->prepare( + " +SELECT `$primary_id_column` as primary_key_id, $entity_column_string FROM $entity_table WHERE $where_clause ORDER BY $order_by LIMIT %d; +", + array( + $batch_size, + ) + ); + // phpcs:enable + + return $query; + } + + /** + * Helper method to escape backtick in column and table names. + * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506 + * + * @param string|array $identifier Column or table name. + * + * @return array|string|string[] Escaped identifier. + */ + private function escape_backtick( $identifier ) { + return str_replace( '`', '``', $identifier ); + } + + /** + * Helper method to build query that will be used to fetch data from source meta table. + * + * @param array $entity_ids List of IDs to fetch metadata for. + * + * @return string|void Query for fetching meta data. + */ + private function build_meta_data_query( $entity_ids ) { + global $wpdb; + $meta_table = $this->escape_backtick( $this->schema_config['entity_meta_schema']['table_name'] ); + $meta_keys = array_keys( $this->meta_column_mapping ); + $meta_key_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_key_column'] ); + $meta_value_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_value_column'] ); + $meta_table_relational_key = $this->escape_backtick( $this->schema_config['entity_meta_relation']['meta'] ); + + $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); + $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders. + $query = $wpdb->prepare( + " +SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value +FROM `$meta_table` +WHERE + `$meta_table_relational_key` IN ( $entity_id_string ) + AND `$meta_key_column` IN ( $meta_column_string ); +", + array_merge( + $entity_ids, + $meta_keys + ) + ); + // phpcs:enable + + return $query; + } + + /** + * Helper function to validate and combine data before we try to insert. + * + * @param array $entity_data Data from source table. + * @param array $meta_data Data from meta table. + * + * @return array[] Validated and combined data with errors. + */ + private function process_and_sanitize_data( $entity_data, $meta_data ) { + /** + * TODO: Add more validations for: + * 1. Column size + * 2. Value limits + */ + $sanitized_entity_data = array(); + $error_records = array(); + $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data ); + $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data ); + + return array( + 'data' => $sanitized_entity_data, + 'errors' => $error_records, + ); + } + + /** + * Helper method to sanitize core source table. + * + * @param array $sanitized_entity_data Array containing sanitized data for insertion. + * @param array $error_records Error records. + * @param array $entity_data Original source data. + */ + private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, $entity_data ) { + foreach ( $entity_data as $entity ) { + $row_data = array(); + foreach ( $this->core_column_mapping as $column_name => $schema ) { + $custom_table_column_name = $schema['destination'] ?? $column_name; + $value = $entity->$column_name; + $value = $this->validate_data( $value, $schema['type'] ); + if ( is_wp_error( $value ) ) { + $error_records[ $entity->primary_key_id ][ $custom_table_column_name ] = $value->get_error_message(); + } else { + $row_data[ $custom_table_column_name ] = $value; + } + } + $sanitized_entity_data[ $entity->primary_key_id ] = $row_data; + } + } + + /** + * Helper method to sanitize soure meta data. + * + * @param array $sanitized_entity_data Array containing sanitized data for insertion. + * @param array $error_records Error records. + * @param array $meta_data Original source data. + */ + private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, $meta_data ) { + foreach ( $meta_data as $datum ) { + $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; + $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); + if ( is_wp_error( $value ) ) { + $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = $value->get_error_message(); + } else { + $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value; + } + } + } + + /** + * Validate and transform data so that we catch as many errors as possible before inserting. + * + * @param mixed $value Actual data value. + * @param string $type Type of data, could be decimal, int, date, string. + * + * @return float|int|mixed|string|\WP_Error + */ + private function validate_data( $value, $type ) { + switch ( $type ) { + case 'decimal': + $value = (float) $value; + break; + case 'int': + $value = (int) $value; + break; + case 'date': + // TODO: Test this validation in unit tests. + try { + $value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' ); + } catch ( \Exception $e ) { + return new \WP_Error( $e->getMessage() ); + } + break; + } + + return $value; + } +} From bdff0100185b592f11a23ac352fea25a23d40d00 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 7 Mar 2022 17:05:19 +0530 Subject: [PATCH 103/206] Add class WPPostToCOTMigrator --- .../CustomOrderTable/WPPostToCOTMigrator.php | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php new file mode 100644 index 00000000000..c3d3b375470 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -0,0 +1,217 @@ + array( + 'primary_id' => 'ID', + 'table_name' => $wpdb->posts, + ), + 'entity_meta_schema' => array( + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'table_name' => $wpdb->postmeta, + ), + 'destination_table' => $wpdb->prefix . 'wc_orders', + 'entity_meta_relation' => array( + 'entity' => 'ID', + 'meta' => 'post_id', + ), + + ); + + $order_table_core_config = array( + 'ID' => array( + 'type' => 'int', + 'destination' => 'post_id', + ), + 'post_status' => array( + 'type' => 'string', + 'destination' => 'status', + ), + 'post_date_gmt' => array( + 'type' => 'date', + 'destination' => 'date_created_gmt', + ), + 'post_modified_gmt' => array( + 'type' => 'date', + 'destination' => 'date_updated_gmt', + ), + 'post_parent' => array( + 'type' => 'int', + 'destination' => 'parent_order_id', + ), + ); + + $order_table_meta_config = array( + '_order_currency' => array( + 'type' => 'string', + 'destination' => 'currency', + ), + '_order_tax' => array( + 'type' => 'decimal', + 'destination' => 'tax_amount', + ), + '_order_total' => array( + 'type' => 'decimal', + 'destination' => 'total_amount', + ), + '_customer_user' => array( + 'type' => 'int', + 'destination' => 'customer_id', + ), + '_billing_email' => array( + 'type' => 'string', + 'destination' => 'billing_email', + ), + '_payment_method' => array( + 'type' => 'string', + 'destination' => 'payment_method', + ), + '_payment_method_title' => array( + 'type' => 'string', + 'destination' => 'payment_method_title', + ), + '_customer_ip_address' => array( + 'type' => 'string', + 'destination' => 'ip_address', + ), + '_customer_user_agent' => array( + 'type' => 'string', + 'destination' => 'user_agent', + ), + ); + $this->order_table_migrator = new MetaToCustomTableMigrator( $order_table_schema_config, $order_table_meta_config, $order_table_core_config ); + $this->error_logger = new MigrationErrorLogger(); + } + + /** + * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. + * + * @param int $batch_size Batch size of records to migrate. + * + * @return bool True if migration is completed, false if there are still records to process. + */ + public function process_next_migration_batch( $batch_size = 500 ) { + global $wpdb; + $order_by = 'ID ASC'; + + $data = $this->order_table_migrator->fetch_data_for_migration( $this->get_where_clause(), $batch_size, $order_by ); + + foreach ( $data['errors'] as $post_id => $error ) { + $this->error_logger->log( 'info', "Error in importing post id $post_id: " . print_r( $error, true ) ); + } + + if ( count( $data['data'] ) === 0 ) { + return true; + } + + $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $data['data'], 'insert' ); + $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. + if ( count( $data['data'] ) !== $result ) { + // Some rows were not inserted. + // TODO: Find and log the entity ids that were not inserted. + echo ' error '; + } + + $last_post_migrated = max( array_keys( $data['data'] ) ); + $this->update_checkpoint( $last_post_migrated ); + + return false; + } + + /** + * Method to migrate single record. + * + * @param int $post_id Post ID of record to migrate. + * + * @return bool|\WP_Error + */ + public function process_single( $post_id ) { + global $wpdb; + + $where_clause = $wpdb->prepare( 'ID = %d', $post_id ); + $data = $this->order_table_migrator->fetch_data_for_migration( $where_clause, 1, 'ID ASC' ); + if ( isset( $data['errors'][ $post_id ] ) ) { + return new \WP_Error( $data['errors'][ $post_id ] ); + } + + $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $data['data'], 'replace' ); + $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. + if ( 1 !== $result ) { + // TODO: Fetch and return last error. + echo 'error'; + + return new \WP_Error( 'error' ); + } + + return true; + } + + /** + * Helper function to get where clause to send to MetaToCustomTableMigrator instance. + * + * @return string|void Where clause. + */ + private function get_where_clause() { + global $wpdb; + + $checkpoint = $this->get_checkpoint(); + $where_clause = $wpdb->prepare( + 'post_type = "shop_order" AND ID > %d', + $checkpoint['id'] + ); + + return $where_clause; + } + + /** + * Current checkpoint status. + * + * @return false|mixed|void + */ + private function get_checkpoint() { + return get_option( 'wc_cot_migration', array( 'id' => 0 ) ); + } + + /** + * Updates current checkpoint + * + * @param int $id Order ID. + */ + public function update_checkpoint( $id ) { + update_option( 'wc_cot_migration', array( 'id' => $id ) ); + } +} From 17b1623071308f7ce4c6fb164ae3a8e79f68368b Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 10 Mar 2022 17:19:06 +0530 Subject: [PATCH 104/206] Add support for select clause in column name. --- .../MetaToCustomTableMigrator.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 7e353203474..0f0833e6bb9 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -188,7 +188,7 @@ class MetaToCustomTableMigrator { 'errors' => array(), ); } - $entity_ids = array_column( $entity_data, 'primary_key_id' ); + $entity_ids = array_column( $entity_data, 'entity_rel_column' ); $meta_table_query = $this->build_meta_data_query( $entity_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared. @@ -210,12 +210,21 @@ class MetaToCustomTableMigrator { global $wpdb; $entity_table = $this->escape_backtick( $this->schema_config['entity_schema']['table_name'] ); $primary_id_column = $this->escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); - $entity_keys = $this->escape_backtick( array_keys( $this->core_column_mapping ) ); - $entity_column_string = '`' . implode( '`, `', $entity_keys ) . '`'; + $entity_rel_column = $this->escape_backtick( $this->schema_config['entity_meta_relation']['entity_rel_column'] ); + $entity_keys = array(); + foreach ( $this->core_column_mapping as $column_name => $column_schema ) { + if ( isset( $column_schema['select_clause'] ) ) { + $select_clause = $column_schema['select_clause']; + $entity_keys[] = "$select_clause AS $column_name"; + } else { + $entity_keys[] = '`' . $this->escape_backtick( $column_name ) . '`'; + } + } + $entity_column_string = implode( ', ', $entity_keys ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $entity_table, $primary_id_column and $entity_column_string is escaped for backticks. $where clause and $order_by should already be escaped. $query = $wpdb->prepare( " -SELECT `$primary_id_column` as primary_key_id, $entity_column_string FROM $entity_table WHERE $where_clause ORDER BY $order_by LIMIT %d; +SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_rel_column, $entity_column_string FROM $entity_table WHERE $where_clause ORDER BY $order_by LIMIT %d; ", array( $batch_size, @@ -251,7 +260,7 @@ SELECT `$primary_id_column` as primary_key_id, $entity_column_string FROM $entit $meta_keys = array_keys( $this->meta_column_mapping ); $meta_key_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_key_column'] ); $meta_value_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_value_column'] ); - $meta_table_relational_key = $this->escape_backtick( $this->schema_config['entity_meta_relation']['meta'] ); + $meta_table_relational_key = $this->escape_backtick( $this->schema_config['entity_meta_relation']['meta_rel_column'] ); $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); From 32d1162a017be2863e1d0211ba3b26dad5561038 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 10 Mar 2022 17:19:36 +0530 Subject: [PATCH 105/206] Add migration support for billing address. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 189 ++++++++++++++++-- 1 file changed, 177 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index c3d3b375470..8dba1643868 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -22,18 +22,53 @@ class WPPostToCOTMigrator { private $error_logger; /** - * Migrator instance to actually perform migrations. + * Migrator instance to migrate data into wc_order table. * * @var MetaToCustomTableMigrator */ private $order_table_migrator; + /** + * Migrator instance to migrate data into address table. + * + * @var MetaToCustomTableMigrator + */ + private $address_table_migrator; + + /** + * Names of different order tables. + * + * @var array + */ + private $table_names; + /** * WPPostToCOTMigrator constructor. */ public function __construct() { global $wpdb; + // TODO: Remove hardcoding. + $this->table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + ); + + $order_config = $this->get_config_for_order_table(); + $address_config = $this->get_config_for_address_table_billing(); + $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); + $this->address_table_migrator = new MetaToCustomTableMigrator( $address_config['schema'], $address_config['meta'], $address_config['core'] ); + $this->error_logger = new MigrationErrorLogger(); + } + + /** + * Returns migration configuration for order table. + * + * @return array Config for order table. + */ + public function get_config_for_order_table() { + global $wpdb; $order_table_schema_config = array( 'entity_schema' => array( 'primary_id' => 'ID', @@ -46,8 +81,8 @@ class WPPostToCOTMigrator { ), 'destination_table' => $wpdb->prefix . 'wc_orders', 'entity_meta_relation' => array( - 'entity' => 'ID', - 'meta' => 'post_id', + 'entity_rel_column' => 'ID', + 'meta_rel_column' => 'post_id', ), ); @@ -75,7 +110,7 @@ class WPPostToCOTMigrator { ), ); - $order_table_meta_config = array( + $order_table_meta_config = array( '_order_currency' => array( 'type' => 'string', 'destination' => 'currency', @@ -113,8 +148,104 @@ class WPPostToCOTMigrator { 'destination' => 'user_agent', ), ); - $this->order_table_migrator = new MetaToCustomTableMigrator( $order_table_schema_config, $order_table_meta_config, $order_table_core_config ); - $this->error_logger = new MigrationErrorLogger(); + + return array( + 'schema' => $order_table_schema_config, + 'core' => $order_table_core_config, + 'meta' => $order_table_meta_config, + ); + } + + /** + * Get configuration for billing addresses for migration. + * + * @return array Billing address migration config. + */ + public function get_config_for_address_table_billing() { + global $wpdb; + // We join order core table and post meta table to get data for address, since we need order ID. + // So order core record needs to be already present. + $schema_config = array( + 'entity_schema' => array( + 'primary_id' => 'id', + 'table_name' => $this->table_names['orders'], + ), + 'entity_meta_schema' => array( + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'table_name' => $wpdb->postmeta, + ), + 'destination_table' => $this->table_names['addresses'], + 'entity_meta_relation' => array( + 'entity_rel_column' => 'post_id', + 'meta_rel_column' => 'post_id', + ), + ); + + $core_config = array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + 'billing' => array( + 'type' => 'string', + 'destination' => 'address_type', + 'select_clause' => "'billing'", + ), + ); + + $meta_config = array( + '_billing_first_name' => array( + 'type' => 'string', + 'destination' => 'first_name', + ), + '_billing_last_name' => array( + 'type' => 'string', + 'destination' => 'last_name', + ), + '_billing_company' => array( + 'type' => 'string', + 'destination' => 'company', + ), + '_billing_address_1' => array( + 'type' => 'string', + 'destination' => 'address_1', + ), + '_billing_address_2' => array( + 'type' => 'string', + 'destination' => 'address_2', + ), + '_billing_city' => array( + 'type' => 'string', + 'destination' => 'city', + ), + '_billing_state' => array( + 'type' => 'string', + 'destination' => 'state', + ), + '_billing_postcode' => array( + 'type' => 'string', + 'destination' => 'postcode', + ), + '_billing_country' => array( + 'type' => 'string', + 'destination' => 'country', + ), + '_billing_email' => array( + 'type' => 'string', + 'destination' => 'email', + ), + '_billing_phone' => array( + 'type' => 'string', + 'destination' => 'phone', + ), + ); + + return array( + 'schema' => $schema_config, + 'core' => $core_config, + 'meta' => $meta_config, + ); } /** @@ -128,25 +259,39 @@ class WPPostToCOTMigrator { global $wpdb; $order_by = 'ID ASC'; - $data = $this->order_table_migrator->fetch_data_for_migration( $this->get_where_clause(), $batch_size, $order_by ); + $order_data = $this->order_table_migrator->fetch_data_for_migration( $this->get_where_clause(), $batch_size, $order_by ); - foreach ( $data['errors'] as $post_id => $error ) { + foreach ( $order_data['errors'] as $post_id => $error ) { $this->error_logger->log( 'info', "Error in importing post id $post_id: " . print_r( $error, true ) ); } - if ( count( $data['data'] ) === 0 ) { + if ( count( $order_data['data'] ) === 0 ) { return true; } - $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $data['data'], 'insert' ); + $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $order_data['data'], 'insert' ); $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. - if ( count( $data['data'] ) !== $result ) { + if ( count( $order_data['data'] ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. echo ' error '; } - $last_post_migrated = max( array_keys( $data['data'] ) ); + $order_post_ids = array_column( $order_data['data'], 'post_id' ); + $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); + $address_data = $this->address_table_migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); + foreach ( $address_data['errors'] as $order_id => $error ) { + $this->error_logger->log( 'info', "Error in importing address data for Order ID $order_id: " . print_r( $error, true ) ); + } + $address_queries = $this->address_table_migrator->generate_insert_sql_for_batch( $address_data['data'], 'insert' ); + $result = $wpdb->query( $address_queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. + if ( count( $address_data['data'] ) !== $result ) { + // Some rows were not inserted. + // TODO: Find and log the entity ids that were not inserted. + echo 'error'; + } + + $last_post_migrated = max( array_keys( $order_data['data'] ) ); $this->update_checkpoint( $last_post_migrated ); return false; @@ -197,6 +342,26 @@ class WPPostToCOTMigrator { return $where_clause; } + /** + * Helper method to create `ID in (.., .., ...)` clauses. + * + * @param array $ids List of IDs. + * @param string $column_name Name of the ID column. + * + * @return string Prepared clause for where. + */ + private function get_where_id_clause( $ids, $column_name = 'ID' ) { + global $wpdb; + + if ( 0 === count( $ids ) ) { + return ''; + } + + $id_placeholder_array = '(' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ')'; + + return $wpdb->prepare( "`$column_name` IN $id_placeholder_array", $ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Both $column_name and $id_placeholder_array should already be prepared. + } + /** * Current checkpoint status. * From 6cc34e8616c5cf464d72c7a0fe895304471eb4a4 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 10 Mar 2022 18:35:36 +0530 Subject: [PATCH 106/206] Add support for migrating shipping addresses. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 105 ++++++++++++------ 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index 8dba1643868..5eda1f77a0d 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -29,11 +29,18 @@ class WPPostToCOTMigrator { private $order_table_migrator; /** - * Migrator instance to migrate data into address table. + * Migrator instance to migrate billing data into address table. * * @var MetaToCustomTableMigrator */ - private $address_table_migrator; + private $billing_address_table_migrator; + + /** + * Migrator instance to migrate shipping data into address table. + * + * @var MetaToCustomTableMigrator + */ + private $shipping_address_table_migrator; /** * Names of different order tables. @@ -55,11 +62,13 @@ class WPPostToCOTMigrator { 'op_data' => $wpdb->prefix . 'wc_order_operational_data', ); - $order_config = $this->get_config_for_order_table(); - $address_config = $this->get_config_for_address_table_billing(); - $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); - $this->address_table_migrator = new MetaToCustomTableMigrator( $address_config['schema'], $address_config['meta'], $address_config['core'] ); - $this->error_logger = new MigrationErrorLogger(); + $order_config = $this->get_config_for_order_table(); + $billing_address_config = $this->get_config_for_address_table_billing(); + $shipping_address_config = $this->get_config_for_address_table_shipping(); + $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); + $this->billing_address_table_migrator = new MetaToCustomTableMigrator( $billing_address_config['schema'], $billing_address_config['meta'], $billing_address_config['core'] ); + $this->shipping_address_table_migrator = new MetaToCustomTableMigrator( $shipping_address_config['schema'], $shipping_address_config['meta'], $shipping_address_config['core'] ); + $this->error_logger = new MigrationErrorLogger(); } /** @@ -162,12 +171,32 @@ class WPPostToCOTMigrator { * @return array Billing address migration config. */ public function get_config_for_address_table_billing() { + return $this->get_config_for_address_table( 'billing' ); + } + + /** + * Get configuration for shipping addresses for migration. + * + * @return array Shipping address migration config. + */ + public function get_config_for_address_table_shipping() { + return $this->get_config_for_address_table( 'shipping' ); + } + + /** + * Generate config for address data. + * + * @param string $type Type of address, this will be using in fetching meta keys. + * + * @return array Config for address table. + */ + private function get_config_for_address_table( $type ) { global $wpdb; // We join order core table and post meta table to get data for address, since we need order ID. // So order core record needs to be already present. $schema_config = array( 'entity_schema' => array( - 'primary_id' => 'id', + 'primary_id' => 'post_id', 'table_name' => $this->table_names['orders'], ), 'entity_meta_schema' => array( @@ -183,59 +212,59 @@ class WPPostToCOTMigrator { ); $core_config = array( - 'id' => array( + 'id' => array( 'type' => 'int', 'destination' => 'order_id', ), - 'billing' => array( + 'type' => array( 'type' => 'string', 'destination' => 'address_type', - 'select_clause' => "'billing'", + 'select_clause' => "'$type'", ), ); $meta_config = array( - '_billing_first_name' => array( + "_{$type}_first_name" => array( 'type' => 'string', 'destination' => 'first_name', ), - '_billing_last_name' => array( + "_{$type}_last_name" => array( 'type' => 'string', 'destination' => 'last_name', ), - '_billing_company' => array( + "_{$type}_company" => array( 'type' => 'string', 'destination' => 'company', ), - '_billing_address_1' => array( + "_{$type}_address_1" => array( 'type' => 'string', 'destination' => 'address_1', ), - '_billing_address_2' => array( + "_{$type}_address_2" => array( 'type' => 'string', 'destination' => 'address_2', ), - '_billing_city' => array( + "_{$type}_city" => array( 'type' => 'string', 'destination' => 'city', ), - '_billing_state' => array( + "_{$type}_state" => array( 'type' => 'string', 'destination' => 'state', ), - '_billing_postcode' => array( + "_{$type}_postcode" => array( 'type' => 'string', 'destination' => 'postcode', ), - '_billing_country' => array( + "_{$type}_country" => array( 'type' => 'string', 'destination' => 'country', ), - '_billing_email' => array( + "_{$type}_email" => array( 'type' => 'string', 'destination' => 'email', ), - '_billing_phone' => array( + "_{$type}_phone" => array( 'type' => 'string', 'destination' => 'phone', ), @@ -255,7 +284,7 @@ class WPPostToCOTMigrator { * * @return bool True if migration is completed, false if there are still records to process. */ - public function process_next_migration_batch( $batch_size = 500 ) { + public function process_next_migration_batch( $batch_size = 100 ) { global $wpdb; $order_by = 'ID ASC'; @@ -277,24 +306,38 @@ class WPPostToCOTMigrator { echo ' error '; } - $order_post_ids = array_column( $order_data['data'], 'post_id' ); + $order_post_ids = array_column( $order_data['data'], 'post_id' ); + $this->process_next_address_batch( $this->billing_address_table_migrator, $order_post_ids, $order_by ); + $this->process_next_address_batch( $this->shipping_address_table_migrator, $order_post_ids, $order_by ); + + $last_post_migrated = max( array_keys( $order_data['data'] ) ); + $this->update_checkpoint( $last_post_migrated ); + + return false; + } + + /** + * Process next batch for a given address type. + * + * @param MetaToCustomTableMigrator $migrator Migrator instance for address type. + * @param array $order_post_ids Array of post IDs for orders. + * @param string $order_by Order by clause. + */ + private function process_next_address_batch( $migrator, $order_post_ids, $order_by ) { + global $wpdb; $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); - $address_data = $this->address_table_migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); + $batch_size = count( $order_post_ids ); + $address_data = $migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); foreach ( $address_data['errors'] as $order_id => $error ) { $this->error_logger->log( 'info', "Error in importing address data for Order ID $order_id: " . print_r( $error, true ) ); } - $address_queries = $this->address_table_migrator->generate_insert_sql_for_batch( $address_data['data'], 'insert' ); + $address_queries = $migrator->generate_insert_sql_for_batch( $address_data['data'], 'insert' ); $result = $wpdb->query( $address_queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. if ( count( $address_data['data'] ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. echo 'error'; } - - $last_post_migrated = max( array_keys( $order_data['data'] ) ); - $this->update_checkpoint( $last_post_migrated ); - - return false; } /** From 786561d6fd14e63b27e1014619b640eb9aa2d19d Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 10 Mar 2022 19:50:01 +0530 Subject: [PATCH 107/206] Add migrations for operational data table. --- .../MetaToCustomTableMigrator.php | 52 +++++-- .../CustomOrderTable/WPPostToCOTMigrator.php | 147 ++++++++++++++++-- .../Orders/OrdersTableDataStore.php | 4 +- 3 files changed, 170 insertions(+), 33 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 0f0833e6bb9..81d4bdcf233 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -39,10 +39,12 @@ class MetaToCustomTableMigrator { * @var string[] */ private $wpdb_placeholder_for_type = array( - 'int' => '%d', - 'decimal' => '%f', - 'string' => '%s', - 'date' => '%s', + 'int' => '%d', + 'decimal' => '%f', + 'string' => '%s', + 'date' => '%s', + 'date_epoch' => '%s', + 'bool' => '%d', ); /** @@ -181,18 +183,18 @@ class MetaToCustomTableMigrator { // TODO: Add code to validate params. $entity_table_query = $this->build_entity_table_query( $where_clause, $batch_size, $order_by ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared. - $entity_data = $wpdb->get_results( $entity_table_query ); + $entity_data = $wpdb->get_results( $entity_table_query ); if ( empty( $entity_data ) ) { return array( - 'data' => array(), + 'data' => array(), 'errors' => array(), ); } - $entity_ids = array_column( $entity_data, 'entity_rel_column' ); + $entity_ids = array_column( $entity_data, 'entity_rel_column' ); $meta_table_query = $this->build_meta_data_query( $entity_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared. - $meta_data = $wpdb->get_results( $meta_table_query ); + $meta_data = $wpdb->get_results( $meta_table_query ); return $this->process_and_sanitize_data( $entity_data, $meta_data ); } @@ -208,10 +210,10 @@ class MetaToCustomTableMigrator { */ private function build_entity_table_query( $where_clause, $batch_size, $order_by ) { global $wpdb; - $entity_table = $this->escape_backtick( $this->schema_config['entity_schema']['table_name'] ); - $primary_id_column = $this->escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); + $entity_table = $this->escape_backtick( $this->schema_config['entity_schema']['table_name'] ); + $primary_id_column = $this->escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); $entity_rel_column = $this->escape_backtick( $this->schema_config['entity_meta_relation']['entity_rel_column'] ); - $entity_keys = array(); + $entity_keys = array(); foreach ( $this->core_column_mapping as $column_name => $column_schema ) { if ( isset( $column_schema['select_clause'] ) ) { $select_clause = $column_schema['select_clause']; @@ -222,7 +224,7 @@ class MetaToCustomTableMigrator { } $entity_column_string = implode( ', ', $entity_keys ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $entity_table, $primary_id_column and $entity_column_string is escaped for backticks. $where clause and $order_by should already be escaped. - $query = $wpdb->prepare( + $query = $wpdb->prepare( " SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_rel_column, $entity_column_string FROM $entity_table WHERE $where_clause ORDER BY $order_by LIMIT %d; ", @@ -230,6 +232,7 @@ SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_re $batch_size, ) ); + // phpcs:enable return $query; @@ -279,6 +282,7 @@ WHERE $meta_keys ) ); + // phpcs:enable return $query; @@ -304,7 +308,7 @@ WHERE $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data ); return array( - 'data' => $sanitized_entity_data, + 'data' => $sanitized_entity_data, 'errors' => $error_records, ); } @@ -345,7 +349,7 @@ WHERE $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); if ( is_wp_error( $value ) ) { - $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = $value->get_error_message(); + $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}"; } else { $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value; } @@ -368,10 +372,28 @@ WHERE case 'int': $value = (int) $value; break; + case 'bool': + $value = wc_string_to_bool( $value ); + break; case 'date': // TODO: Test this validation in unit tests. try { - $value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' ); + if ( '' === $value ) { + $value = null; + } else { + $value = ( new \DateTime( $value ) )->format( 'Y-m-d H:i:s' ); + } + } catch ( \Exception $e ) { + return new \WP_Error( $e->getMessage() ); + } + break; + case 'date_epoch': + try { + if ( '' === $value ) { + $value = null; + } else { + $value = ( new \DateTime( "@$value" ) )->format( 'Y-m-d H:i:s' ); + } } catch ( \Exception $e ) { return new \WP_Error( $e->getMessage() ); } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index 5eda1f77a0d..0a1e9fbf117 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -42,6 +42,13 @@ class WPPostToCOTMigrator { */ private $shipping_address_table_migrator; + /** + * Migrator instance to migrate operational data. + * + * @var MetaToCustomTableMigrator + */ + private $operation_data_table_migrator; + /** * Names of different order tables. * @@ -62,13 +69,17 @@ class WPPostToCOTMigrator { 'op_data' => $wpdb->prefix . 'wc_order_operational_data', ); - $order_config = $this->get_config_for_order_table(); - $billing_address_config = $this->get_config_for_address_table_billing(); - $shipping_address_config = $this->get_config_for_address_table_shipping(); + $order_config = $this->get_config_for_order_table(); + $billing_address_config = $this->get_config_for_address_table_billing(); + $shipping_address_config = $this->get_config_for_address_table_shipping(); + $operation_data_config = $this->get_config_for_operational_data_table(); + $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); $this->billing_address_table_migrator = new MetaToCustomTableMigrator( $billing_address_config['schema'], $billing_address_config['meta'], $billing_address_config['core'] ); $this->shipping_address_table_migrator = new MetaToCustomTableMigrator( $shipping_address_config['schema'], $shipping_address_config['meta'], $shipping_address_config['core'] ); - $this->error_logger = new MigrationErrorLogger(); + $this->operation_data_table_migrator = new MetaToCustomTableMigrator( $operation_data_config['schema'], $operation_data_config['meta'], $operation_data_config['core'] ); + + $this->error_logger = new MigrationErrorLogger(); } /** @@ -76,7 +87,7 @@ class WPPostToCOTMigrator { * * @return array Config for order table. */ - public function get_config_for_order_table() { + private function get_config_for_order_table() { global $wpdb; $order_table_schema_config = array( 'entity_schema' => array( @@ -170,7 +181,7 @@ class WPPostToCOTMigrator { * * @return array Billing address migration config. */ - public function get_config_for_address_table_billing() { + private function get_config_for_address_table_billing() { return $this->get_config_for_address_table( 'billing' ); } @@ -179,7 +190,7 @@ class WPPostToCOTMigrator { * * @return array Shipping address migration config. */ - public function get_config_for_address_table_shipping() { + private function get_config_for_address_table_shipping() { return $this->get_config_for_address_table( 'shipping' ); } @@ -277,6 +288,108 @@ class WPPostToCOTMigrator { ); } + /** + * Generate config for operational data. + * + * @return array Config for operational data table. + */ + private function get_config_for_operational_data_table() { + global $wpdb; + + $schema_config = array( + 'entity_schema' => array( + 'primary_id' => 'post_id', + 'table_name' => $this->table_names['orders'], + ), + 'entity_meta_schema' => array( + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'table_name' => $wpdb->postmeta, + ), + 'destination_table' => $this->table_names['op_data'], + 'entity_meta_relation' => array( + 'entity_rel_column' => 'post_id', + 'meta_rel_column' => 'post_id', + ), + ); + + $core_config = array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + ); + + $meta_config = array( + '_created_via' => array( + 'type' => 'string', + 'destination' => 'created_via', + ), + '_order_version' => array( + 'type' => 'string', + 'destination' => 'woocommerce_version', + ), + '_prices_include_tax' => array( + 'type' => 'bool', + 'destination' => 'prices_include_tax', + ), + '_recorded_coupon_usage_counts' => array( + 'type' => 'bool', + 'destination' => 'coupon_usages_are_counted', + ), + '_download_permissions_granted' => array( + 'type' => 'bool', + 'destination' => 'download_permissions_granted', + ), + '_cart_hash' => array( + 'type' => 'string', + 'destination' => 'cart_hash', + ), + '_new_order_email_sent' => array( + 'type' => 'bool', + 'destination' => 'new_order_email_sent', + ), + '_order_key' => array( + 'type' => 'string', + 'destination' => 'order_key', + ), + '_order_stock_reduced' => array( + 'type' => 'bool', + 'destination' => 'order_stock_reduced', + ), + '_date_paid' => array( + 'type' => 'date_epoch', + 'destination' => 'date_paid_gmt', + ), + '_date_completed' => array( + 'type' => 'date_epoch', + 'destination' => 'date_completed_gmt', + ), + '_order_shipping_tax' => array( + 'type' => 'decimal', + 'destination' => 'shipping_tax_amount', + ), + '_order_shipping' => array( + 'type' => 'decimal', + 'destination' => 'shipping_total_amount', + ), + '_cart_discount_tax' => array( + 'type' => 'decimal', + 'destination' => 'discount_tax_amount', + ), + '_cart_discount' => array( + 'type' => 'decimal', + 'destination' => 'discount_total_amount', + ), + ); + + return array( + 'schema' => $schema_config, + 'core' => $core_config, + 'meta' => $meta_config, + ); + } + /** * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. * @@ -307,8 +420,9 @@ class WPPostToCOTMigrator { } $order_post_ids = array_column( $order_data['data'], 'post_id' ); - $this->process_next_address_batch( $this->billing_address_table_migrator, $order_post_ids, $order_by ); - $this->process_next_address_batch( $this->shipping_address_table_migrator, $order_post_ids, $order_by ); + $this->process_next_migrator_batch( $this->billing_address_table_migrator, $order_post_ids, $order_by ); + $this->process_next_migrator_batch( $this->shipping_address_table_migrator, $order_post_ids, $order_by ); + $this->process_next_migrator_batch( $this->operation_data_table_migrator, $order_post_ids, $order_by ); $last_post_migrated = max( array_keys( $order_data['data'] ) ); $this->update_checkpoint( $last_post_migrated ); @@ -323,17 +437,18 @@ class WPPostToCOTMigrator { * @param array $order_post_ids Array of post IDs for orders. * @param string $order_by Order by clause. */ - private function process_next_address_batch( $migrator, $order_post_ids, $order_by ) { + private function process_next_migrator_batch( $migrator, $order_post_ids, $order_by ) { global $wpdb; $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); $batch_size = count( $order_post_ids ); - $address_data = $migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); - foreach ( $address_data['errors'] as $order_id => $error ) { - $this->error_logger->log( 'info', "Error in importing address data for Order ID $order_id: " . print_r( $error, true ) ); + $data = $migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); + foreach ( $data['errors'] as $order_id => $error ) { + // TODO: Add name of the migrator in error message. + $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); } - $address_queries = $migrator->generate_insert_sql_for_batch( $address_data['data'], 'insert' ); - $result = $wpdb->query( $address_queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. - if ( count( $address_data['data'] ) !== $result ) { + $queries = $migrator->generate_insert_sql_for_batch( $data['data'], 'insert' ); + $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. + if ( count( $data['data'] ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. echo 'error'; diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index db22608e63b..8647ab413ca 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -234,7 +234,7 @@ CREATE TABLE $operational_data_table_name ( woocommerce_version varchar(20) NULL, prices_include_tax tinyint(1) NULL, coupon_usages_are_counted tinyint(1) NULL, - download_permissionis_granted tinyint(1) NULL, + download_permission_granted tinyint(1) NULL, cart_hash varchar(100) NULL, new_order_email_sent tinyint(1) NULL, order_key varchar(100) NULL, @@ -242,7 +242,7 @@ CREATE TABLE $operational_data_table_name ( date_paid_gmt datetime NULL, date_completed_gmt datetime NULL, shipping_tax_amount decimal(26, 8) NULL, - shopping_total_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, KEY order_id (order_id), From cb280829cc8d91f47247d0b21c7ab1a2ac0f35e7 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 12:52:54 +0530 Subject: [PATCH 108/206] Add a migration helper class to refactor common utility methods. --- .../Database/Migrations/MigrationHelper.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 plugins/woocommerce/src/Database/Migrations/MigrationHelper.php diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php new file mode 100644 index 00000000000..8d7e7da30a9 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php @@ -0,0 +1,75 @@ + '%d', + 'decimal' => '%f', + 'string' => '%s', + 'date' => '%s', + 'date_epoch' => '%s', + 'bool' => '%d', + ); + + /** + * Get insert clause for appropriate switch. + * + * @param string $switch Name of the switch to use. + * + * @return string Insert clause. + */ + public static function get_insert_switch( $switch ) { + switch ( $switch ) { + case 'insert_ignore': + $insert_query = 'INSERT IGNORE'; + break; + case 'replace': + $insert_query = 'REPLACE'; + break; + case 'insert': + default: + $insert_query = 'INSERT'; + } + + return $insert_query; + } + + /** + * Helper method to escape backtick in column and table names. + * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506 + * + * @param string|array $identifier Column or table name. + * + * @return array|string|string[] Escaped identifier. + */ + public static function escape_backtick( $identifier ) { + return str_replace( '`', '``', $identifier ); + } + + /** + * Return $wpdb->prepare placeholder for data type. + * + * @param string $type Data type. + * + * @return string $wpdb placeholder. + */ + public static function get_wpdb_placeholder_for_type( $type ) { + return self::$wpdb_placeholder_for_type[ $type ]; + } + +} From c44cc40d2143f306596a4af77dd7e7784a448ea3 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:40:55 +0530 Subject: [PATCH 109/206] Add utility class for migrating metadata. --- .../MetaToMetaTableMigrator.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php new file mode 100644 index 00000000000..400674b7bd0 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -0,0 +1,121 @@ + + * array ( + * 'primary_id' => 'primary_id column name of source table', + * 'table_name' => 'name of the source table'. + * ), + * 'entity_meta_schema' => + * array ( + * 'meta_key_column' => 'name of meta_key column in source meta table', + * 'meta_value_column' => 'name of meta_value column in source meta table', + * 'table_name' => 'name of source meta table', + * ), + * 'destination_table' => 'name of destination custom table', + * 'entity_meta_relation' => + * array ( + * 'entity' => 'name of column in source table which is used in source meta table', + * 'meta' => 'name of column in source meta table which contains key of records in source table', + * ) + * ) + * ). + * @param array $exclude_columns List of columns to exclude. + */ + public function __construct( $schema_config ) { + $this->schema_config = $schema_config; + } + + public function generate_insert_sql_for_batch( $batch, $insert_switch ) { + global $wpdb; + + $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); + + $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['meta_key_column'] ); + $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['meta_value_column'] ); + $entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['entity_id_column'] ); + $column_sql = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)"; + $table = $this->schema_config['destination']['meta_table_name']; + + $entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] ); + $placeholder_string = "( $entity_id_column_placeholder, %s, %s )"; + $values = array(); + foreach ( array_values( $batch ) as $row ) { + $query_params = array( + $row['destination_entity_id'], + $row['meta_key'], + $row['meta_value'] + ); + $value_sql = $wpdb->prepare( "( $placeholder_string )", $query_params ); + $values[] = $value_sql; + } + + $values_sql = implode( ',', $values ); + + return "$insert_query INTO $table $column_sql VALUES $values_sql"; + } + + public function fetch_data_for_migration( $where_clause ) { + global $wpdb; + + $meta_query = $this->build_meta_table_query( $where_clause ); + + $meta_data_rows = $wpdb->get_results( $meta_query ); + if ( empty ( $meta_data_rows ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + + return $meta_data_rows; + } + + private function build_meta_table_query( $where_clause ) { + global $wpdb; + $source_meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_key_column'] ); + $source_meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_value_column'] ); + $source_entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_entity_id_column'] ); + $source_meta_table_name = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_table_name'] ); + $order_by = "`$source_meta_table_name`.`$source_entity_id_column` ASC"; + + $destination_entity_table = $this->schema_config['destination']['entity_table_name']; + $destination_entity_id_column = $this->schema_config['destination']['entity_id_column']; + $destination_source_id_mapping_column = $this->schema_config['destination']['source_id_column']; + + if ( $this->schema_config['source']['excluded_keys'] ) { + $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); + $exclude_clause = $wpdb->prepare( "$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + $where_clause = "$where_clause AND $exclude_clause"; + } + + $meta_data_sql = " +SELECT + `$source_meta_table_name`.`$source_entity_id_column` as source_entity_id, + `$destination_entity_table`.`$destination_entity_id_column` as destination_entity_id, + `$source_meta_table_name`.`$source_meta_key_column` as meta_key, + `$source_meta_table_name`.`$source_meta_value_column` as meta_value +FROM `$source_meta_table_name` +JOIN `$destination_entity_table` ON `$destination_entity_table`.`$destination_source_id_mapping_column` = `$source_meta_table_name`.`$source_entity_id_column` +WHERE $where_clause ORDER BY $order_by +"; + + return $meta_data_sql; + } + +} From ee834e2cf1a9da04affcfe245b881b3e20bb3665 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:43:47 +0530 Subject: [PATCH 110/206] Applied coding standards. --- .../MetaToMetaTableMigrator.php | 87 ++++++++++++------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index 400674b7bd0..2335997d607 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -5,42 +5,43 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; -use MigrationHelper; +use Automattic\WooCommerce\DataBase\Migrations\MigrationHelper; +/** + * Class MetaToMetaTableMigrator. + * + * Generic class for powering migrations from one meta table to another table. + * + * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + */ class MetaToMetaTableMigrator { + /** + * Schema config, see __construct for more details. + * + * @var array + */ private $schema_config; /** * MetaToMetaTableMigrator constructor. * * @param array $schema_config This parameters provides general but essential information about tables under migrations. Must be of the form- - * array( - * 'entity_schema' => - * array ( - * 'primary_id' => 'primary_id column name of source table', - * 'table_name' => 'name of the source table'. - * ), - * 'entity_meta_schema' => - * array ( - * 'meta_key_column' => 'name of meta_key column in source meta table', - * 'meta_value_column' => 'name of meta_value column in source meta table', - * 'table_name' => 'name of source meta table', - * ), - * 'destination_table' => 'name of destination custom table', - * 'entity_meta_relation' => - * array ( - * 'entity' => 'name of column in source table which is used in source meta table', - * 'meta' => 'name of column in source meta table which contains key of records in source table', - * ) - * ) - * ). - * @param array $exclude_columns List of columns to exclude. + * TODO: Add structure. */ public function __construct( $schema_config ) { + // TODO: Validate params. $this->schema_config = $schema_config; } + /** + * Generate insert sql queries for batches. + * + * @param array $batch Data to generate queries for. + * @param string $insert_switch Insert switch to use. + * + * @return string + */ public function generate_insert_sql_for_batch( $batch, $insert_switch ) { global $wpdb; @@ -59,10 +60,11 @@ class MetaToMetaTableMigrator { $query_params = array( $row['destination_entity_id'], $row['meta_key'], - $row['meta_value'] + $row['meta_value'], ); - $value_sql = $wpdb->prepare( "( $placeholder_string )", $query_params ); - $values[] = $value_sql; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholder_string is hardcoded. + $value_sql = $wpdb->prepare( "( $placeholder_string )", $query_params ); + $values[] = $value_sql; } $values_sql = implode( ',', $values ); @@ -70,13 +72,30 @@ class MetaToMetaTableMigrator { return "$insert_query INTO $table $column_sql VALUES $values_sql"; } + /** + * Fetch data for migration. + * + * @param string $where_clause Where conditions to use while selecting data from source table. + * + * @return array[] Data along with errors (if any), will of the form: + * array( + * 'data' => array( + * 'id_1' => array( 'column1' => value1, 'column2' => value2, ...), + * ..., + * ), + * 'errors' => array( + * 'id_1' => array( 'column1' => error1, 'column2' => value2, ...), + * ..., + * ) + */ public function fetch_data_for_migration( $where_clause ) { global $wpdb; $meta_query = $this->build_meta_table_query( $where_clause ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Meta query has interpolated variables, but they should all be escaped for backticks. $meta_data_rows = $wpdb->get_results( $meta_query ); - if ( empty ( $meta_data_rows ) ) { + if ( empty( $meta_data_rows ) ) { return array( 'data' => array(), 'errors' => array(), @@ -86,6 +105,13 @@ class MetaToMetaTableMigrator { return $meta_data_rows; } + /** + * Helper method to build query used to fetch data from source meta table. + * + * @param string $where_clause Where conditions to use while selecting data from source table. + * + * @return string Query that can be used to fetch data. + */ private function build_meta_table_query( $where_clause ) { global $wpdb; $source_meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_key_column'] ); @@ -94,13 +120,14 @@ class MetaToMetaTableMigrator { $source_meta_table_name = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_table_name'] ); $order_by = "`$source_meta_table_name`.`$source_entity_id_column` ASC"; - $destination_entity_table = $this->schema_config['destination']['entity_table_name']; - $destination_entity_id_column = $this->schema_config['destination']['entity_id_column']; - $destination_source_id_mapping_column = $this->schema_config['destination']['source_id_column']; + $destination_entity_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity_table_name'] ); + $destination_entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity_id_column'] ); + $destination_source_id_mapping_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['source_id_column'] ); if ( $this->schema_config['source']['excluded_keys'] ) { $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); - $exclude_clause = $wpdb->prepare( "$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. + $exclude_clause = $wpdb->prepare( "`$source_meta_key_column` NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); $where_clause = "$where_clause AND $exclude_clause"; } From aa00d7af684a160917245bf96ba4102e26ba6700 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:45:31 +0530 Subject: [PATCH 111/206] Add defination of order meta data table. Made table name methods static so that they can be reused. --- .../Orders/OrdersTableDataStore.php | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 8647ab413ca..cb0218d1f1c 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -17,7 +17,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The custom orders table name. */ - public function get_orders_table_name() { + public static function get_orders_table_name() { global $wpdb; return $wpdb->prefix . 'wc_orders'; } @@ -27,7 +27,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The order addresses table name. */ - public function get_addresses_table_name() { + public static function get_addresses_table_name() { global $wpdb; return $wpdb->prefix . 'wc_order_addresses'; } @@ -37,11 +37,21 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * * @return string The orders operational data table name. */ - public function get_operational_data_table_name() { + public static function get_operational_data_table_name() { global $wpdb; return $wpdb->prefix . 'wc_order_operational_data'; } + /** + * Get the orders meta data table name. + * + * @return string Name of order meta data table. + */ + public static function get_meta_table_name() { + global $wpdb; + return $wpdb->prefix . 'wc_ordes_meta'; + } + /** * Get the names of all the tables involved in the custom orders table feature. * @@ -185,6 +195,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements $orders_table_name = $this->get_orders_table_name(); $addresses_table_name = $this->get_addresses_table_name(); $operational_data_table_name = $this->get_operational_data_table_name(); + $meta_table = $this->get_meta_table_name(); $sql = " CREATE TABLE $orders_table_name ( @@ -247,7 +258,16 @@ CREATE TABLE $operational_data_table_name ( discount_total_amount decimal(26, 8) NULL, KEY order_id (order_id), KEY order_key (order_key) -);"; +); +CREATE TABLE $meta_table ( + id bigint(20) unsigned auto_increment primary key, + order_id bigint(20) unsigned null, + meta_key varchar(255), + meta_value text null, + KEY meta_key_value (meta_key, meta_value(100)) +); +"; + return $sql; } } From 6b70afc7314ce58f208cbf9bb7ae60ede6079bdf Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:46:38 +0530 Subject: [PATCH 112/206] Refactor to use migration helper for better re-usability. --- .../MetaToCustomTableMigrator.php | 63 +++++-------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 81d4bdcf233..dbe66cc10bf 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +use MigrationHelper; + /** * Class MetaToCustomTableMigrator. * @@ -33,20 +35,6 @@ class MetaToCustomTableMigrator { */ private $core_column_mapping; - /** - * Placeholders that we will use in building $wpdb queries. - * - * @var string[] - */ - private $wpdb_placeholder_for_type = array( - 'int' => '%d', - 'decimal' => '%f', - 'string' => '%s', - 'date' => '%s', - 'date_epoch' => '%s', - 'bool' => '%d', - ); - /** * MetaToCustomTableMigrator constructor. * @@ -120,24 +108,13 @@ class MetaToCustomTableMigrator { // TODO: Add code to validate params. $table = $this->schema_config['destination_table']; - - switch ( $insert_switch ) { - case 'insert_ignore': - $insert_query = 'INSERT IGNORE'; - break; - case 'replace': - $insert_query = 'REPLACE'; - break; - case 'insert': - default: - $insert_query = 'INSERT'; - } + $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); $columns = array(); $placeholders = array(); foreach ( array_merge( $this->core_column_mapping, $this->meta_column_mapping ) as $prev_column => $schema ) { $columns[] = $schema['destination']; - $placeholders[] = $this->wpdb_placeholder_for_type[ $schema['type'] ]; + $placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] ); } $placeholders = "'" . implode( "', '", $placeholders ) . "'"; @@ -147,14 +124,14 @@ class MetaToCustomTableMigrator { foreach ( $columns as $column ) { $query_params[] = $row[ $column ] ?? null; } - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in $this->>wpdb_placeholder_for_type. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $placeholders can only contain combination of placeholders described in MigrationHelper::get_wpdb_placeholder_for_type $value_string = '(' . $wpdb->prepare( $placeholders, $query_params ) . ')'; $values[] = $value_string; } $value_sql = implode( ',', $values ); - $column_sql = implode( '`, `', $this->escape_backtick( $columns ) ); + $column_sql = implode( '`, `', MigrationHelper::escape_backtick( $columns ) ); return "$insert_query INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. } @@ -210,16 +187,16 @@ class MetaToCustomTableMigrator { */ private function build_entity_table_query( $where_clause, $batch_size, $order_by ) { global $wpdb; - $entity_table = $this->escape_backtick( $this->schema_config['entity_schema']['table_name'] ); - $primary_id_column = $this->escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); - $entity_rel_column = $this->escape_backtick( $this->schema_config['entity_meta_relation']['entity_rel_column'] ); + $entity_table = MigrationHelper::escape_backtick( $this->schema_config['entity_schema']['table_name'] ); + $primary_id_column = MigrationHelper::escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); + $entity_rel_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_relation']['entity_rel_column'] ); $entity_keys = array(); foreach ( $this->core_column_mapping as $column_name => $column_schema ) { if ( isset( $column_schema['select_clause'] ) ) { $select_clause = $column_schema['select_clause']; $entity_keys[] = "$select_clause AS $column_name"; } else { - $entity_keys[] = '`' . $this->escape_backtick( $column_name ) . '`'; + $entity_keys[] = '`' . MigrationHelper::escape_backtick( $column_name ) . '`'; } } $entity_column_string = implode( ', ', $entity_keys ); @@ -238,18 +215,6 @@ SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_re return $query; } - /** - * Helper method to escape backtick in column and table names. - * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506 - * - * @param string|array $identifier Column or table name. - * - * @return array|string|string[] Escaped identifier. - */ - private function escape_backtick( $identifier ) { - return str_replace( '`', '``', $identifier ); - } - /** * Helper method to build query that will be used to fetch data from source meta table. * @@ -259,11 +224,11 @@ SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_re */ private function build_meta_data_query( $entity_ids ) { global $wpdb; - $meta_table = $this->escape_backtick( $this->schema_config['entity_meta_schema']['table_name'] ); + $meta_table = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['table_name'] ); $meta_keys = array_keys( $this->meta_column_mapping ); - $meta_key_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_key_column'] ); - $meta_value_column = $this->escape_backtick( $this->schema_config['entity_meta_schema']['meta_value_column'] ); - $meta_table_relational_key = $this->escape_backtick( $this->schema_config['entity_meta_relation']['meta_rel_column'] ); + $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['meta_key_column'] ); + $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['meta_value_column'] ); + $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_relation']['meta_rel_column'] ); $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); From 7bb25779aaae28efc7107aa5488a9efe2c2e6463 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:47:15 +0530 Subject: [PATCH 113/206] Add config for migration meta data table as well. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index 0a1e9fbf117..fc2e30b809b 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -49,6 +49,8 @@ class WPPostToCOTMigrator { */ private $operation_data_table_migrator; + private $meta_table_migrator; + /** * Names of different order tables. * @@ -67,18 +69,22 @@ class WPPostToCOTMigrator { 'orders' => $wpdb->prefix . 'wc_orders', 'addresses' => $wpdb->prefix . 'wc_order_addresses', 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', ); $order_config = $this->get_config_for_order_table(); $billing_address_config = $this->get_config_for_address_table_billing(); $shipping_address_config = $this->get_config_for_address_table_shipping(); $operation_data_config = $this->get_config_for_operational_data_table(); + $meta_data_config = $this->get_config_for_meta_table(); $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); $this->billing_address_table_migrator = new MetaToCustomTableMigrator( $billing_address_config['schema'], $billing_address_config['meta'], $billing_address_config['core'] ); $this->shipping_address_table_migrator = new MetaToCustomTableMigrator( $shipping_address_config['schema'], $shipping_address_config['meta'], $shipping_address_config['core'] ); $this->operation_data_table_migrator = new MetaToCustomTableMigrator( $operation_data_config['schema'], $operation_data_config['meta'], $operation_data_config['core'] ); + $this->meta_table_migrator = new MetaToMetaTableMigrator( $meta_data_config ); + $this->error_logger = new MigrationErrorLogger(); } @@ -390,6 +396,41 @@ class WPPostToCOTMigrator { ); } + private function get_config_for_meta_table() { + global $wpdb; + + $excluded_columns = array_keys( $this->get_config_for_order_table()['meta'] ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_operational_data_table()['meta'] ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_address_table_billing()['meta'] ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_address_table_shipping()['meta'] ) ); + + return array( + 'source' => array( + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'entity_id_column' => 'post_id', + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + ), + 'excluded_keys' => $excluded_columns, + ), + 'destination' => array( + 'meta' => array( + 'table_name' => $this->table_names['meta'], + 'entity_id_column' => 'order_id', + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_type' => 'int', + ), + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'source_id_column' => 'post_id', + 'id_column' => 'id', + ), + ) + ); + } + /** * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. * @@ -423,6 +464,7 @@ class WPPostToCOTMigrator { $this->process_next_migrator_batch( $this->billing_address_table_migrator, $order_post_ids, $order_by ); $this->process_next_migrator_batch( $this->shipping_address_table_migrator, $order_post_ids, $order_by ); $this->process_next_migrator_batch( $this->operation_data_table_migrator, $order_post_ids, $order_by ); + $this->process_meta_migration( $order_post_ids ); $last_post_migrated = max( array_keys( $order_data['data'] ) ); $this->update_checkpoint( $last_post_migrated ); @@ -434,8 +476,8 @@ class WPPostToCOTMigrator { * Process next batch for a given address type. * * @param MetaToCustomTableMigrator $migrator Migrator instance for address type. - * @param array $order_post_ids Array of post IDs for orders. - * @param string $order_by Order by clause. + * @param array $order_post_ids Array of post IDs for orders. + * @param string $order_by Order by clause. */ private function process_next_migrator_batch( $migrator, $order_post_ids, $order_by ) { global $wpdb; @@ -455,6 +497,18 @@ class WPPostToCOTMigrator { } } + private function process_meta_migration( $order_post_ids ) { + global $wpdb; + $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); + $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration( $post_ids_where_clause ); + $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); + $result = $wpdb->query( $insert_queries ); + if ( count( $data_to_migrate['data'] ) !== $result ) { + // TODO: Find and log entity ids that were not inserted. + echo 'error'; + } + } + /** * Method to migrate single record. * @@ -503,7 +557,7 @@ class WPPostToCOTMigrator { /** * Helper method to create `ID in (.., .., ...)` clauses. * - * @param array $ids List of IDs. + * @param array $ids List of IDs. * @param string $column_name Name of the ID column. * * @return string Prepared clause for where. From 5d6eaff099d8d4f30b7bad98f26d70a8378c538f Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 13:56:46 +0530 Subject: [PATCH 114/206] Applied coding standards. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index fc2e30b809b..4a9cf58b5c6 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -49,6 +49,11 @@ class WPPostToCOTMigrator { */ private $operation_data_table_migrator; + /** + * Migrator instance to migrate meta data. + * + * @var MetaToMetaTableMigrator + */ private $meta_table_migrator; /** @@ -396,6 +401,11 @@ class WPPostToCOTMigrator { ); } + /** + * Generate config for meta data migration. + * + * @return array Meta data migration config. + */ private function get_config_for_meta_table() { global $wpdb; @@ -427,7 +437,7 @@ class WPPostToCOTMigrator { 'source_id_column' => 'post_id', 'id_column' => 'id', ), - ) + ), ); } @@ -476,8 +486,8 @@ class WPPostToCOTMigrator { * Process next batch for a given address type. * * @param MetaToCustomTableMigrator $migrator Migrator instance for address type. - * @param array $order_post_ids Array of post IDs for orders. - * @param string $order_by Order by clause. + * @param array $order_post_ids Array of post IDs for orders. + * @param string $order_by Order by clause. */ private function process_next_migrator_batch( $migrator, $order_post_ids, $order_by ) { global $wpdb; @@ -497,13 +507,19 @@ class WPPostToCOTMigrator { } } + /** + * Process migration for metadata for given post ids.\ + * + * @param array $order_post_ids Post IDs. + */ private function process_meta_migration( $order_post_ids ) { global $wpdb; $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration( $post_ids_where_clause ); $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_queries should already be escaped in the generating function. $result = $wpdb->query( $insert_queries ); - if ( count( $data_to_migrate['data'] ) !== $result ) { + if ( count( $data_to_migrate['data'] ) !== $result ) { // TODO: Find and log entity ids that were not inserted. echo 'error'; } @@ -557,7 +573,7 @@ class WPPostToCOTMigrator { /** * Helper method to create `ID in (.., .., ...)` clauses. * - * @param array $ids List of IDs. + * @param array $ids List of IDs. * @param string $column_name Name of the ID column. * * @return string Prepared clause for where. From 48356c213f3e7d5bca2248345a60588a70582690 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 15:38:16 +0530 Subject: [PATCH 115/206] Refactor config format to be more clear. --- .../MetaToCustomTableMigrator.php | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index dbe66cc10bf..0c7126edc62 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; -use MigrationHelper; +use Automattic\WooCommerce\DataBase\Migrations\MigrationHelper; /** * Class MetaToCustomTableMigrator. @@ -94,7 +94,7 @@ class MetaToCustomTableMigrator { /** * Generate SQL for data insertion. * - * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. * @param string $insert_switch Insert command to use in generating queries, could be insert, insert_ignore, or replace. * * @return string Generated queries for insertion for this batch, would be of the form: @@ -107,7 +107,7 @@ class MetaToCustomTableMigrator { global $wpdb; // TODO: Add code to validate params. - $table = $this->schema_config['destination_table']; + $table = $this->schema_config['destination']['table_name']; $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); $columns = array(); @@ -187,10 +187,14 @@ class MetaToCustomTableMigrator { */ private function build_entity_table_query( $where_clause, $batch_size, $order_by ) { global $wpdb; - $entity_table = MigrationHelper::escape_backtick( $this->schema_config['entity_schema']['table_name'] ); - $primary_id_column = MigrationHelper::escape_backtick( $this->schema_config['entity_schema']['primary_id'] ); - $entity_rel_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_relation']['entity_rel_column'] ); - $entity_keys = array(); + $source_entity_table = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['table_name'] ); + $source_meta_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['meta_rel_column'] ); + $source_destination_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['destination_rel_column'] ); + + $destination_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['table_name'] ); + $destination_source_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['source_rel_column'] ); + + $entity_keys = array(); foreach ( $this->core_column_mapping as $column_name => $column_schema ) { if ( isset( $column_schema['select_clause'] ) ) { $select_clause = $column_schema['select_clause']; @@ -200,10 +204,20 @@ class MetaToCustomTableMigrator { } } $entity_column_string = implode( ', ', $entity_keys ); - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $entity_table, $primary_id_column and $entity_column_string is escaped for backticks. $where clause and $order_by should already be escaped. + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped. $query = $wpdb->prepare( " -SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_rel_column, $entity_column_string FROM $entity_table WHERE $where_clause ORDER BY $order_by LIMIT %d; +SELECT + `$source_entity_table`.`$source_meta_rel_id_column` as entity_meta_rel_id, + `$source_entity_table`.`$source_destination_rel_id_column` AS destination_rel_id, + CASE + WHEN EXISTS( `$destination_table`.`$destination_source_rel_id_column` ) THEN 'replace' + ELSE 'insert' + END as insert_switch, + $entity_column_string +FROM `$source_entity_table` +LEFT JOIN `$destination_table` ON `$destination_table`.`$destination_source_rel_id_column` = `$source_entity_table`.`$source_destination_rel_id_column` +WHERE $where_clause ORDER BY $order_by LIMIT %d; ", array( $batch_size, @@ -224,11 +238,11 @@ SELECT `$primary_id_column` as primary_key_id, `$entity_rel_column` AS entity_re */ private function build_meta_data_query( $entity_ids ) { global $wpdb; - $meta_table = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['table_name'] ); + $meta_table = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['table_name'] ); $meta_keys = array_keys( $this->meta_column_mapping ); - $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['meta_key_column'] ); - $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_schema']['meta_value_column'] ); - $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['entity_meta_relation']['meta_rel_column'] ); + $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_key_column'] ); + $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_value_column'] ); + $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['meta_rel_column'] ); $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); @@ -324,7 +338,7 @@ WHERE /** * Validate and transform data so that we catch as many errors as possible before inserting. * - * @param mixed $value Actual data value. + * @param mixed $value Actual data value. * @param string $type Type of data, could be decimal, int, date, string. * * @return float|int|mixed|string|\WP_Error From 78360660086177fa27e866d41a371c9bc2145970 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 15:39:07 +0530 Subject: [PATCH 116/206] Refactor config format to be more clear. --- .../Migrations/CustomOrderTable/MetaToCustomTableMigrator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 0c7126edc62..5f8b1438740 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -94,7 +94,7 @@ class MetaToCustomTableMigrator { /** * Generate SQL for data insertion. * - * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. * @param string $insert_switch Insert command to use in generating queries, could be insert, insert_ignore, or replace. * * @return string Generated queries for insertion for this batch, would be of the form: @@ -338,7 +338,7 @@ WHERE /** * Validate and transform data so that we catch as many errors as possible before inserting. * - * @param mixed $value Actual data value. + * @param mixed $value Actual data value. * @param string $type Type of data, could be decimal, int, date, string. * * @return float|int|mixed|string|\WP_Error From 8724429382d0331293423208c2512710d3547b0c Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 24 Mar 2022 15:39:36 +0530 Subject: [PATCH 117/206] Refactor to use new config format. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 84 +++++++++++-------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index 4a9cf58b5c6..aa92014bf6a 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -101,21 +101,25 @@ class WPPostToCOTMigrator { private function get_config_for_order_table() { global $wpdb; $order_table_schema_config = array( - 'entity_schema' => array( - 'primary_id' => 'ID', - 'table_name' => $wpdb->posts, + 'source' => array( + 'entity' => array( + 'table_name' => $wpdb->posts, + 'meta_rel_column' => 'ID', + 'destination_rel_column' => 'ID', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), ), - 'entity_meta_schema' => array( - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'table_name' => $wpdb->postmeta, + 'destination' => array( + array( + 'table_name' => $this->table_names['orders'], + 'source_rel_column' => 'post_id', + ), ), - 'destination_table' => $wpdb->prefix . 'wc_orders', - 'entity_meta_relation' => array( - 'entity_rel_column' => 'ID', - 'meta_rel_column' => 'post_id', - ), - ); $order_table_core_config = array( @@ -217,19 +221,22 @@ class WPPostToCOTMigrator { // We join order core table and post meta table to get data for address, since we need order ID. // So order core record needs to be already present. $schema_config = array( - 'entity_schema' => array( - 'primary_id' => 'post_id', - 'table_name' => $this->table_names['orders'], + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), ), - 'entity_meta_schema' => array( - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'table_name' => $wpdb->postmeta, - ), - 'destination_table' => $this->table_names['addresses'], - 'entity_meta_relation' => array( - 'entity_rel_column' => 'post_id', - 'meta_rel_column' => 'post_id', + 'destination' => array( + 'table_name' => $this->table_names['addresses'], + 'source_rel_column' => 'order_id', ), ); @@ -308,19 +315,22 @@ class WPPostToCOTMigrator { global $wpdb; $schema_config = array( - 'entity_schema' => array( - 'primary_id' => 'post_id', - 'table_name' => $this->table_names['orders'], + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), ), - 'entity_meta_schema' => array( - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'table_name' => $wpdb->postmeta, - ), - 'destination_table' => $this->table_names['op_data'], - 'entity_meta_relation' => array( - 'entity_rel_column' => 'post_id', - 'meta_rel_column' => 'post_id', + 'destination' => array( + 'table_name' => $this->table_names['op_data'], + 'source_rel_column' => 'order_id', ), ); From ee76fbc17cf387928a63f2bfc510bfb6d4572fb3 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 25 Mar 2022 16:32:33 +0530 Subject: [PATCH 118/206] Modify to migrate by IDs to reduce responsibilities of generate migrator classes. --- .../MetaToCustomTableMigrator.php | 45 ++++---- .../MetaToMetaTableMigrator.php | 49 ++++---- .../CustomOrderTable/WPPostToCOTMigrator.php | 105 +++++++++--------- .../Database/Migrations/MigrationHelper.php | 8 ++ 4 files changed, 112 insertions(+), 95 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 5f8b1438740..d7b397c7727 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -94,7 +94,7 @@ class MetaToCustomTableMigrator { /** * Generate SQL for data insertion. * - * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. * @param string $insert_switch Insert command to use in generating queries, could be insert, insert_ignore, or replace. * * @return string Generated queries for insertion for this batch, would be of the form: @@ -154,11 +154,11 @@ class MetaToCustomTableMigrator { * ..., * ) */ - public function fetch_data_for_migration( $where_clause, $batch_size, $order_by ) { + public function fetch_data_for_migration_for_ids( $entity_ids ) { global $wpdb; // TODO: Add code to validate params. - $entity_table_query = $this->build_entity_table_query( $where_clause, $batch_size, $order_by ); + $entity_table_query = $this->build_entity_table_query( $entity_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared. $entity_data = $wpdb->get_results( $entity_table_query ); if ( empty( $entity_data ) ) { @@ -167,15 +167,16 @@ class MetaToCustomTableMigrator { 'errors' => array(), ); } - $entity_ids = array_column( $entity_data, 'entity_rel_column' ); + $entity_meta_rel_ids = array_column( $entity_data, 'entity_meta_rel_id' ); - $meta_table_query = $this->build_meta_data_query( $entity_ids ); + $meta_table_query = $this->build_meta_data_query( $entity_meta_rel_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_meta_data_query is already prepared. $meta_data = $wpdb->get_results( $meta_table_query ); return $this->process_and_sanitize_data( $entity_data, $meta_data ); } + /** * Helper method to build query used to fetch data from core source table. * @@ -185,22 +186,24 @@ class MetaToCustomTableMigrator { * * @return string Query that can be used to fetch data. */ - private function build_entity_table_query( $where_clause, $batch_size, $order_by ) { + private function build_entity_table_query( $entity_ids ) { global $wpdb; - $source_entity_table = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['table_name'] ); - $source_meta_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['meta_rel_column'] ); - $source_destination_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['destination_rel_column'] ); + $source_entity_table = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['table_name'] ); + $source_meta_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['meta_rel_column'] ); + $source_destination_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['destination_rel_column'] ); + $source_primary_key_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['primary_key'] ); $destination_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['table_name'] ); - $destination_source_rel_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['source_rel_column'] ); + $destination_source_rel_id_column = MigrationHelper::add_table_name_to_column( $destination_table, $this->schema_config['destination']['source_rel_column'] ); + $where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; $entity_keys = array(); foreach ( $this->core_column_mapping as $column_name => $column_schema ) { if ( isset( $column_schema['select_clause'] ) ) { $select_clause = $column_schema['select_clause']; $entity_keys[] = "$select_clause AS $column_name"; } else { - $entity_keys[] = '`' . MigrationHelper::escape_backtick( $column_name ) . '`'; + $entity_keys[] = MigrationHelper::add_table_name_to_column( $source_entity_table, $column_name ); } } $entity_column_string = implode( ', ', $entity_keys ); @@ -208,20 +211,18 @@ class MetaToCustomTableMigrator { $query = $wpdb->prepare( " SELECT - `$source_entity_table`.`$source_meta_rel_id_column` as entity_meta_rel_id, - `$source_entity_table`.`$source_destination_rel_id_column` AS destination_rel_id, + $source_meta_rel_id_column as entity_meta_rel_id, + $source_destination_rel_id_column AS destination_rel_id, CASE - WHEN EXISTS( `$destination_table`.`$destination_source_rel_id_column` ) THEN 'replace' + WHEN $destination_source_rel_id_column IS NOT NULL THEN 'replace' ELSE 'insert' END as insert_switch, $entity_column_string FROM `$source_entity_table` -LEFT JOIN `$destination_table` ON `$destination_table`.`$destination_source_rel_id_column` = `$source_entity_table`.`$source_destination_rel_id_column` -WHERE $where_clause ORDER BY $order_by LIMIT %d; +LEFT JOIN `$destination_table` ON $destination_source_rel_id_column = $source_destination_rel_id_column +WHERE $where_clause; ", - array( - $batch_size, - ) + $entity_ids ); // phpcs:enable @@ -242,7 +243,7 @@ WHERE $where_clause ORDER BY $order_by LIMIT %d; $meta_keys = array_keys( $this->meta_column_mapping ); $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_key_column'] ); $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_value_column'] ); - $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['meta_rel_column'] ); + $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['entity_id_column'] ); $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); @@ -312,7 +313,7 @@ WHERE $row_data[ $custom_table_column_name ] = $value; } } - $sanitized_entity_data[ $entity->primary_key_id ] = $row_data; + $sanitized_entity_data[ $entity->destination_rel_id ] = $row_data; } } @@ -338,7 +339,7 @@ WHERE /** * Validate and transform data so that we catch as many errors as possible before inserting. * - * @param mixed $value Actual data value. + * @param mixed $value Actual data value. * @param string $type Type of data, could be decimal, int, date, string. * * @return float|int|mixed|string|\WP_Error diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index 2335997d607..d3db92b05ec 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -51,7 +51,7 @@ class MetaToMetaTableMigrator { $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['meta_value_column'] ); $entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['entity_id_column'] ); $column_sql = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)"; - $table = $this->schema_config['destination']['meta_table_name']; + $table = $this->schema_config['destination']['meta']['table_name']; $entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] ); $placeholder_string = "( $entity_id_column_placeholder, %s, %s )"; @@ -88,10 +88,10 @@ class MetaToMetaTableMigrator { * ..., * ) */ - public function fetch_data_for_migration( $where_clause ) { + public function fetch_data_for_migration_for_ids( $order_post_ids ) { global $wpdb; - $meta_query = $this->build_meta_table_query( $where_clause ); + $meta_query = $this->build_meta_table_query( $order_post_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Meta query has interpolated variables, but they should all be escaped for backticks. $meta_data_rows = $wpdb->get_results( $meta_query ); @@ -112,37 +112,40 @@ class MetaToMetaTableMigrator { * * @return string Query that can be used to fetch data. */ - private function build_meta_table_query( $where_clause ) { + private function build_meta_table_query( $entity_ids ) { global $wpdb; - $source_meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_key_column'] ); - $source_meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_value_column'] ); - $source_entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_entity_id_column'] ); - $source_meta_table_name = MigrationHelper::escape_backtick( $this->schema_config['source']['meta_table_name'] ); - $order_by = "`$source_meta_table_name`.`$source_entity_id_column` ASC"; + $source_meta_table = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['table_name'] ); + $source_meta_key_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['meta_key_column'] ); + $source_meta_value_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['meta_value_column'] ); + $source_entity_id_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['entity_id_column'] ); + $order_by = "$source_entity_id_column ASC"; - $destination_entity_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity_table_name'] ); - $destination_entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity_id_column'] ); - $destination_source_id_mapping_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['source_id_column'] ); + $where_clause = "$source_entity_id_column IN (" . implode( array_fill( 0, count( $entity_ids ), '%d') ) . ')'; + + $destination_entity_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity']['table_name'] ); + $destination_entity_id_column = MigrationHelper::add_table_name_to_column( $destination_entity_table, $this->schema_config['destination']['entity']['id_column'] ); + $destination_source_id_mapping_column = MigrationHelper::add_table_name_to_column( $destination_entity_table, $this->schema_config['destination']['entity']['source_id_column'] ); if ( $this->schema_config['source']['excluded_keys'] ) { $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. - $exclude_clause = $wpdb->prepare( "`$source_meta_key_column` NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + $exclude_clause = $wpdb->prepare( "$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); $where_clause = "$where_clause AND $exclude_clause"; } - $meta_data_sql = " + return $wpdb->prepare( + " SELECT - `$source_meta_table_name`.`$source_entity_id_column` as source_entity_id, - `$destination_entity_table`.`$destination_entity_id_column` as destination_entity_id, - `$source_meta_table_name`.`$source_meta_key_column` as meta_key, - `$source_meta_table_name`.`$source_meta_value_column` as meta_value -FROM `$source_meta_table_name` -JOIN `$destination_entity_table` ON `$destination_entity_table`.`$destination_source_id_mapping_column` = `$source_meta_table_name`.`$source_entity_id_column` + $source_entity_id_column as source_entity_id, + $destination_entity_id_column as destination_entity_id, + $source_meta_key_column as meta_key, + $source_meta_value_column as meta_value +FROM `$source_meta_table` +JOIN `$destination_entity_table` ON $destination_source_id_mapping_column = $source_entity_id_column WHERE $where_clause ORDER BY $order_by -"; - - return $meta_data_sql; +", + $entity_ids + ); } } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index aa92014bf6a..d6041f0863b 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -101,24 +101,23 @@ class WPPostToCOTMigrator { private function get_config_for_order_table() { global $wpdb; $order_table_schema_config = array( - 'source' => array( + 'source' => array( 'entity' => array( - 'table_name' => $wpdb->posts, - 'meta_rel_column' => 'ID', + 'table_name' => $wpdb->posts, + 'meta_rel_column' => 'ID', 'destination_rel_column' => 'ID', + 'primary_key' => 'ID', ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', + 'entity_id_column' => 'post_id', ), ), 'destination' => array( - array( - 'table_name' => $this->table_names['orders'], - 'source_rel_column' => 'post_id', - ), + 'table_name' => $this->table_names['orders'], + 'source_rel_column' => 'post_id', ), ); @@ -221,21 +220,22 @@ class WPPostToCOTMigrator { // We join order core table and post meta table to get data for address, since we need order ID. // So order core record needs to be already present. $schema_config = array( - 'source' => array( + 'source' => array( 'entity' => array( - 'table_name' => $this->table_names['orders'], - 'meta_rel_column' => 'post_id', + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', 'destination_rel_column' => 'id', + 'primary_key' => 'id', ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', + 'entity_id_column' => 'post_id', ), ), 'destination' => array( - 'table_name' => $this->table_names['addresses'], + 'table_name' => $this->table_names['addresses'], 'source_rel_column' => 'order_id', ), ); @@ -315,21 +315,22 @@ class WPPostToCOTMigrator { global $wpdb; $schema_config = array( - 'source' => array( + 'source' => array( 'entity' => array( - 'table_name' => $this->table_names['orders'], - 'meta_rel_column' => 'post_id', + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', 'destination_rel_column' => 'id', + 'primary_key' => 'id', ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', + 'entity_id_column' => 'post_id', ), ), 'destination' => array( - 'table_name' => $this->table_names['op_data'], + 'table_name' => $this->table_names['op_data'], 'source_rel_column' => 'order_id', ), ); @@ -460,9 +461,8 @@ class WPPostToCOTMigrator { */ public function process_next_migration_batch( $batch_size = 100 ) { global $wpdb; - $order_by = 'ID ASC'; - $order_data = $this->order_table_migrator->fetch_data_for_migration( $this->get_where_clause(), $batch_size, $order_by ); + $order_data = $this->order_table_migrator->fetch_data_for_migration_for_ids( $this->get_next_batch_ids( $batch_size ) ); foreach ( $order_data['errors'] as $post_id => $error ) { $this->error_logger->log( 'info', "Error in importing post id $post_id: " . print_r( $error, true ) ); @@ -481,9 +481,9 @@ class WPPostToCOTMigrator { } $order_post_ids = array_column( $order_data['data'], 'post_id' ); - $this->process_next_migrator_batch( $this->billing_address_table_migrator, $order_post_ids, $order_by ); - $this->process_next_migrator_batch( $this->shipping_address_table_migrator, $order_post_ids, $order_by ); - $this->process_next_migrator_batch( $this->operation_data_table_migrator, $order_post_ids, $order_by ); + $this->process_next_migrator_batch( $this->billing_address_table_migrator, $order_post_ids ); + $this->process_next_migrator_batch( $this->shipping_address_table_migrator, $order_post_ids ); + $this->process_next_migrator_batch( $this->operation_data_table_migrator, $order_post_ids ); $this->process_meta_migration( $order_post_ids ); $last_post_migrated = max( array_keys( $order_data['data'] ) ); @@ -496,14 +496,12 @@ class WPPostToCOTMigrator { * Process next batch for a given address type. * * @param MetaToCustomTableMigrator $migrator Migrator instance for address type. - * @param array $order_post_ids Array of post IDs for orders. - * @param string $order_by Order by clause. + * @param array $order_post_ids Array of post IDs for orders. + * @param string $order_by Order by clause. */ - private function process_next_migrator_batch( $migrator, $order_post_ids, $order_by ) { + private function process_next_migrator_batch( $migrator, $order_post_ids ) { global $wpdb; - $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); - $batch_size = count( $order_post_ids ); - $data = $migrator->fetch_data_for_migration( $post_ids_where_clause, $batch_size, $order_by ); + $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); foreach ( $data['errors'] as $order_id => $error ) { // TODO: Add name of the migrator in error message. $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); @@ -524,9 +522,8 @@ class WPPostToCOTMigrator { */ private function process_meta_migration( $order_post_ids ) { global $wpdb; - $post_ids_where_clause = $this->get_where_id_clause( $order_post_ids, 'post_id' ); - $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration( $post_ids_where_clause ); - $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); + $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_queries should already be escaped in the generating function. $result = $wpdb->query( $insert_queries ); if ( count( $data_to_migrate['data'] ) !== $result ) { @@ -546,7 +543,7 @@ class WPPostToCOTMigrator { global $wpdb; $where_clause = $wpdb->prepare( 'ID = %d', $post_id ); - $data = $this->order_table_migrator->fetch_data_for_migration( $where_clause, 1, 'ID ASC' ); + $data = $this->order_table_migrator->fetch_data_for_migration_for_ids( $where_clause, 1, 'ID ASC' ); if ( isset( $data['errors'][ $post_id ] ) ) { return new \WP_Error( $data['errors'][ $post_id ] ); } @@ -566,24 +563,28 @@ class WPPostToCOTMigrator { /** * Helper function to get where clause to send to MetaToCustomTableMigrator instance. * - * @return string|void Where clause. + * @return array Where clause. */ - private function get_where_clause() { + private function get_next_batch_ids( $batch_size ) { global $wpdb; $checkpoint = $this->get_checkpoint(); - $where_clause = $wpdb->prepare( - 'post_type = "shop_order" AND ID > %d', - $checkpoint['id'] + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = '%s' LIMIT %d", + $checkpoint['id'], + 'shop_order', + $batch_size + ) ); - return $where_clause; + return $post_ids; } /** * Helper method to create `ID in (.., .., ...)` clauses. * - * @param array $ids List of IDs. + * @param array $ids List of IDs. * @param string $column_name Name of the ID column. * * @return string Prepared clause for where. @@ -615,6 +616,10 @@ class WPPostToCOTMigrator { * @param int $id Order ID. */ public function update_checkpoint( $id ) { - update_option( 'wc_cot_migration', array( 'id' => $id ) ); + return update_option( 'wc_cot_migration', array( 'id' => $id ) ); + } + + public function delete_checkpoint() { + return delete_option( 'wp_cot_migration' ); } } diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php index 8d7e7da30a9..72e1c3ad4fd 100644 --- a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php +++ b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php @@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\DataBase\Migrations; +use function DeliciousBrains\WP_Offload_Media\Aws3\JmesPath\search; + /** * Class MigrationHelper. * @@ -72,4 +74,10 @@ class MigrationHelper { return self::$wpdb_placeholder_for_type[ $type ]; } + public static function add_table_name_to_column( $table_name, $column_name ) { + $table_name = self::escape_backtick( $table_name ); + $column_name = self::escape_backtick( $column_name ); + return "`$table_name`.`$column_name`"; + } + } From 169bd4fd4f2c1edb98f3802cff8c39cbd7645f03 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 25 Mar 2022 18:21:40 +0530 Subject: [PATCH 119/206] Fix incorrect column mapping bugs. --- .../MetaToCustomTableMigrator.php | 2 +- .../MetaToMetaTableMigrator.php | 17 ++++++++--------- .../CustomOrderTable/WPPostToCOTMigrator.php | 7 ++++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index d7b397c7727..b96fb8f74db 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -313,7 +313,7 @@ WHERE $row_data[ $custom_table_column_name ] = $value; } } - $sanitized_entity_data[ $entity->destination_rel_id ] = $row_data; + $sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data; } } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index d3db92b05ec..bef0c8434cc 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -54,22 +54,22 @@ class MetaToMetaTableMigrator { $table = $this->schema_config['destination']['meta']['table_name']; $entity_id_column_placeholder = MigrationHelper::get_wpdb_placeholder_for_type( $this->schema_config['destination']['meta']['entity_id_type'] ); - $placeholder_string = "( $entity_id_column_placeholder, %s, %s )"; + $placeholder_string = "$entity_id_column_placeholder, %s, %s"; $values = array(); foreach ( array_values( $batch ) as $row ) { $query_params = array( - $row['destination_entity_id'], - $row['meta_key'], - $row['meta_value'], + $row->destination_entity_id, + $row->meta_key, + $row->meta_value, ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholder_string is hardcoded. - $value_sql = $wpdb->prepare( "( $placeholder_string )", $query_params ); + $value_sql = $wpdb->prepare( "$placeholder_string", $query_params ); $values[] = $value_sql; } - $values_sql = implode( ',', $values ); + $values_sql = implode( '), (', $values ); - return "$insert_query INTO $table $column_sql VALUES $values_sql"; + return "$insert_query INTO $table $column_sql VALUES ($values_sql)"; } /** @@ -102,7 +102,7 @@ class MetaToMetaTableMigrator { ); } - return $meta_data_rows; + return array( 'data' => $meta_data_rows, 'errors' => array() ); } /** @@ -147,5 +147,4 @@ WHERE $where_clause ORDER BY $order_by $entity_ids ); } - } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index d6041f0863b..a8963bd381e 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -225,7 +225,7 @@ class WPPostToCOTMigrator { 'table_name' => $this->table_names['orders'], 'meta_rel_column' => 'post_id', 'destination_rel_column' => 'id', - 'primary_key' => 'id', + 'primary_key' => 'post_id', ), 'meta' => array( 'table_name' => $wpdb->postmeta, @@ -320,7 +320,7 @@ class WPPostToCOTMigrator { 'table_name' => $this->table_names['orders'], 'meta_rel_column' => 'post_id', 'destination_rel_column' => 'id', - 'primary_key' => 'id', + 'primary_key' => 'post_id', ), 'meta' => array( 'table_name' => $wpdb->postmeta, @@ -361,7 +361,7 @@ class WPPostToCOTMigrator { ), '_download_permissions_granted' => array( 'type' => 'bool', - 'destination' => 'download_permissions_granted', + 'destination' => 'download_permission_granted', ), '_cart_hash' => array( 'type' => 'string', @@ -474,6 +474,7 @@ class WPPostToCOTMigrator { $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $order_data['data'], 'insert' ); $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. + $wpdb->query("COMMIT;"); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $order_data['data'] ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. From 2802162be52d8ce1be2b3a85efd6463e479251ee Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 25 Mar 2022 18:22:00 +0530 Subject: [PATCH 120/206] Add meta table and typo fix. --- .../src/Internal/DataStores/Orders/OrdersTableDataStore.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index cb0218d1f1c..ed6b2b4a957 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -49,7 +49,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements */ public static function get_meta_table_name() { global $wpdb; - return $wpdb->prefix . 'wc_ordes_meta'; + return $wpdb->prefix . 'wc_orders_meta'; } /** @@ -62,6 +62,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements $this->get_orders_table_name(), $this->get_addresses_table_name(), $this->get_operational_data_table_name(), + $this->get_meta_table_name(), ); } From a4c83de142bf74830ae3926702ddc1a34e49d0e7 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 25 Mar 2022 18:23:09 +0530 Subject: [PATCH 121/206] Add dependency injection class. --- plugins/woocommerce/src/Container.php | 2 ++ .../COTMigrationServiceProvider.php | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 711a6e16408..300e72d4aa4 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -6,6 +6,7 @@ namespace Automattic\WooCommerce; use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMigrationServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersDataStoreServiceProvider; @@ -47,6 +48,7 @@ final class Container implements \Psr\Container\ContainerInterface { ProxiesServiceProvider::class, RestockRefundedItemsAdjusterServiceProvider::class, UtilsClassesServiceProvider::class, + COTMigrationServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php new file mode 100644 index 00000000000..cbe4221f3fb --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php @@ -0,0 +1,27 @@ +leagueContainer property or the `getLeagueContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register() { + $this->share( WPPostToCOTMigrator::class ); + } +} From 7186c80c5eb4518e1ddc774bd15999ce70bd2180 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Tue, 29 Mar 2022 15:44:59 +0530 Subject: [PATCH 122/206] Reuse function instead of hardcoding for consistency. --- plugins/woocommerce/includes/abstracts/abstract-wc-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php index eda6c546b7f..a9642de97c4 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php @@ -640,7 +640,7 @@ abstract class WC_Data { } } if ( ! empty( $this->cache_group ) ) { - $cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id(); + $cache_key = self::generate_meta_cache_key( $this->get_id(), $this->cache_group ); wp_cache_delete( $cache_key, $this->cache_group ); } } From 3ed99434742c328d19cc3325134c4738afdf672c Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Tue, 29 Mar 2022 15:45:42 +0530 Subject: [PATCH 123/206] Also add migration config for transaction_id column. --- .../Migrations/CustomOrderTable/WPPostToCOTMigrator.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index a8963bd381e..cbc0e43da17 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -181,6 +181,10 @@ class WPPostToCOTMigrator { 'type' => 'string', 'destination' => 'user_agent', ), + '_transaction_id' => array( + 'type' => 'string', + 'destination' => 'transaction_id', + ), ); return array( @@ -509,6 +513,7 @@ class WPPostToCOTMigrator { } $queries = $migrator->generate_insert_sql_for_batch( $data['data'], 'insert' ); $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. + $wpdb->query('COMMIT;'); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $data['data'] ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. From 9a178b7704e91928066b678855ec703a49865225 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Tue, 29 Mar 2022 15:49:36 +0530 Subject: [PATCH 124/206] Add unit test for order data migration. --- .../WPPostToCOTMigratorTest.php | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php new file mode 100644 index 00000000000..57d7868b06d --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -0,0 +1,250 @@ +create_order_custom_table_if_not_exist(); + $this->data_store = wc_get_container()->get( OrdersTableDataStore::class ); + $this->sut = wc_get_container()->get( WPPostToCOTMigrator::class ); + } + + public function test_process_next_migration_batch_normal_order() { + + $order = wc_get_order( $this->create_complex_wp_post_order() ); + $this->clear_all_orders_and_reset_checkpoint(); + $this->sut->process_next_migration_batch( 100 ); + $db_order = $this->get_order_from_cot( $order ); + + // Verify core data. + $this->assertEquals( $order->get_id(), $db_order->post_id ); + $this->assertEquals( 'wc-' . $order->get_status(), $db_order->status ); + $this->assertEquals( 'INR', $db_order->currency ); + $this->assertEquals( $order->get_customer_id(), $db_order->customer_id ); + $this->assertEquals( $order->get_billing_email(), $db_order->billing_email ); + $this->assertEquals( $order->get_payment_method(), $db_order->payment_method ); + $this->assertEquals( + $order->get_date_created()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order->date_created_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( $order->get_date_modified()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order->date_updated_gmt ) )->date( DATE_ISO8601 ) ); + $this->assertEquals( $order->get_parent_id(), $db_order->parent_order_id ); + $this->assertEquals( $order->get_payment_method_title(), $db_order->payment_method_title ); + $this->assertEquals( $order->get_transaction_id(), $db_order->transaction_id ); + $this->assertEquals( $order->get_customer_ip_address(), $db_order->ip_address ); + $this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent ); + + // Verify order billing address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'billing' ); + $this->assertEquals( $order->get_billing_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_billing_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_billing_company(), $db_order_address->company ); + $this->assertEquals( $order->get_billing_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_billing_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_billing_city(), $db_order_address->city ); + $this->assertEquals( $order->get_billing_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_billing_country(), $db_order_address->country ); + $this->assertEquals( $order->get_billing_email(), $db_order_address->email ); + $this->assertEquals( $order->get_billing_phone(), $db_order_address->phone ); + + // Verify order shipping address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'shipping' ); + $this->assertEquals( $order->get_shipping_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_shipping_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_shipping_company(), $db_order_address->company ); + $this->assertEquals( $order->get_shipping_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_shipping_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_shipping_city(), $db_order_address->city ); + $this->assertEquals( $order->get_shipping_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_shipping_country(), $db_order_address->country ); + $this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone ); + + // Verify order operational data. + $db_order_op_data = $this->get_order_operational_data_from_cot( $db_order->id ); + $this->assertEquals( $order->get_created_via(), $db_order_op_data->created_via ); + $this->assertEquals( $order->get_version(), $db_order_op_data->woocommerce_version ); + $this->assertEquals( $order->get_prices_include_tax(), $db_order_op_data->prices_include_tax ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ), + $db_order_op_data->coupon_usages_are_counted + ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_download_permissions_granted( $order ) ), + $db_order_op_data->download_permission_granted + ); + $this->assertEquals( $order->get_cart_hash(), $db_order_op_data->cart_hash ); + $this->assertEquals( + wc_string_to_bool( $order->get_meta( '_new_order_email_sent' ) ), + $db_order_op_data->new_order_email_sent + ); + $this->assertEquals( $order->get_order_key(), $db_order_op_data->order_key ); + $this->assertEquals( $order->get_data_store()->get_stock_reduced( $order ), $db_order_op_data->order_stock_reduced ); + $this->assertEquals( + $order->get_date_paid()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_paid_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( + $order->get_date_completed()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( $order->get_shipping_tax(), $db_order_op_data->shipping_tax_amount ); + $this->assertEquals( $order->get_shipping_total(), $db_order_op_data->shipping_total_amount ); + $this->assertEquals( $order->get_discount_tax(), $db_order_op_data->discount_tax_amount ); + $this->assertEquals( $order->get_discount_total(), $db_order_op_data->discount_total_amount ); + } + + public function test_process_next_migration_batch_interrupted_migrating_order() { + + } + + public function test_process_next_migration_batch_already_migrated_order() { + + } + + public function test_process_next_migration_batch_invalid_order_data() { + + } + + public function test_process_next_migration_batch_invalid_valid_order_combo() { + + } + + public function test_process_next_migration_batch_stale_order() { + + } + + private function get_order_from_cot( $post_order ) { + global $wpdb; + $order_table = $this->data_store::get_orders_table_name(); + $query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};"; + return $wpdb->get_row( $query ); + } + + private function get_address_details_from_cot( $order_id, $address_type ) { + global $wpdb; + $address_table = $this->data_store::get_addresses_table_name(); + return $wpdb->get_row( "SELECT * FROM $address_table WHERE order_id = $order_id AND address_type = '$address_type';" ); + } + + private function get_order_operational_data_from_cot( $order_id ) { + global $wpdb; + $operational_data_table = $this->data_store::get_operational_data_table_name(); + return $wpdb->get_row( "SELECT * FROM $operational_data_table WHERE order_id = $order_id;" ); + } + + private function create_complex_wp_post_order() { + update_option( 'woocommerce_prices_include_tax', 'yes' ); + update_option( 'woocommerce_calc_taxes', 'yes' ); + $customer = CustomerHelper::create_customer(); + $tax_rate = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '15.0000', + 'tax_rate_name' => 'tax', + 'tax_rate_priority' => '1', + 'tax_rate_order' => '1', + 'tax_rate_shipping' => '1', + ); + WC_Tax::_insert_tax_rate( $tax_rate ); + + ShippingHelper::create_simple_flat_rate(); + + $order = OrderHelper::create_order(); + // Make sure this is a wp_post order. + $post = get_post( $order->get_id() ); + $this->assertNotNull( $post, 'Order is not created in wp_post table.' ); + $this->assertEquals( 'shop_order', $post->post_type, 'Order is not created in wp_post table.' ); + + $order->save(); + + $order->set_status( 'completed' ); + $order->set_currency( 'INR' ); + $order->set_customer_id( $customer->get_id() ); + $order->set_billing_email( $customer->get_billing_email() ); + + $payment_gateway = new WC_Mock_Payment_Gateway(); + $order->set_payment_method( 'mock' ); + $order->set_transaction_id( 'mock1' ); + + $order->set_customer_ip_address( '1.1.1.1' ); + $order->set_customer_user_agent( 'wc_unit_tests' ); + + $order->save(); + + $order->set_shipping_first_name( 'Albert' ); + $order->set_shipping_last_name( 'Einstein' ); + $order->set_shipping_company( 'The Olympia Academy' ); + $order->set_shipping_address_1( '112 Mercer Street' ); + $order->set_shipping_address_2( 'Princeton' ); + $order->set_shipping_city( 'New Jersey' ); + $order->set_shipping_postcode( '08544' ); + $order->set_shipping_phone( '299792458' ); + $order->set_shipping_country( 'US' ); + + $order->set_created_via( 'unit_tests' ); + $order->set_version( '0.0.2' ); + $order->set_prices_include_tax( true ); + wc_update_coupon_usage_counts( $order->get_id() ); + $order->get_data_store()->set_download_permissions_granted( $order, true ); + $order->set_cart_hash( '1234' ); + $order->update_meta_data( '_new_order_email_sent', 'true' ); + $order->update_meta_data( '_order_stock_reduced', 'true' ); + $order->set_date_paid( time() ); + $order->set_date_completed( time() ); + $order->calculate_shipping(); + + $order->save(); + $order->save_meta_data(); + + return $order->get_id(); + } + + private function clear_all_orders_and_reset_checkpoint() { + global $wpdb; + $order_tables = $this->data_store->get_all_table_names(); + foreach ( $order_tables as $table ) { + $wpdb->query( "TRUNCATE table $table;" ); + } + $this->sut->delete_checkpoint(); + } + + private function create_order_custom_table_if_not_exist() { + $order_table_controller = wc_get_container() + ->get( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ); + $order_table_controller->show_feature(); + $this->synchronizer = wc_get_container() + ->get( DataSynchronizer::class ); + if( ! $this->synchronizer->check_orders_table_exists() ) { + $this->synchronizer->create_database_tables(); + } + } +} From 56ee0b2f4a555b191b61844d0e59bbcc53974cba Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 14:30:08 +0530 Subject: [PATCH 125/206] Add update support along with inser. --- plugins/woocommerce.php | 0 .../MetaToCustomTableMigrator.php | 88 +++++++++++++++---- .../WPPostToOrderAddressTableMigrator.php | 0 3 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 plugins/woocommerce.php create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php diff --git a/plugins/woocommerce.php b/plugins/woocommerce.php new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index b96fb8f74db..0bc08e73136 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -12,7 +12,7 @@ use Automattic\WooCommerce\DataBase\Migrations\MigrationHelper; * * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable */ -class MetaToCustomTableMigrator { +abstract class MetaToCustomTableMigrator { /** * Config for tables being migrated and migrated from. See __construct() for detailed config. @@ -103,16 +103,45 @@ class MetaToCustomTableMigrator { * ($value for row 2) * ... */ - public function generate_insert_sql_for_batch( $batch, $insert_switch ) { - global $wpdb; + public function generate_insert_sql_for_batch( $batch ) { + $table = $this->schema_config['destination']['table_name']; - // TODO: Add code to validate params. - $table = $this->schema_config['destination']['table_name']; - $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); + list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch ); + + + return "INSERT IGNORE INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. + } + + public function generate_update_sql_for_batch( $batch ) { + $table = $this->schema_config['destination']['table_name']; + + $destination_primary_id_schema = $this->get_destination_table_primary_id_schema(); + + list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses( + array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ), + $batch + ); + + $duplicate_update_key_statement = $this->generate_on_duplicate_statement_clause( $columns ); + + return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;"; + } + + private function get_destination_table_primary_id_schema() { + return array( + 'destination_primary_key' => array( + 'destination' => $this->schema_config['destination']['primary_key'], + 'type' => $this->schema_config['destination']['primary_key_type'], + ), + ); + } + + private function generate_column_clauses( $columns_schema, $batch ) { + global $wpdb; $columns = array(); $placeholders = array(); - foreach ( array_merge( $this->core_column_mapping, $this->meta_column_mapping ) as $prev_column => $schema ) { + foreach ( $columns_schema as $prev_column => $schema ) { $columns[] = $schema['destination']; $placeholders[] = MigrationHelper::get_wpdb_placeholder_for_type( $schema['type'] ); } @@ -133,7 +162,17 @@ class MetaToCustomTableMigrator { $column_sql = implode( '`, `', MigrationHelper::escape_backtick( $columns ) ); - return "$insert_query INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. + return array( $value_sql, $column_sql, $columns ); + } + + private function generate_on_duplicate_statement_clause( $columns ) { + $update_value_statements = []; + foreach ( $columns as $column ) { + $update_value_statements[] = "$column = VALUES( $column )"; + } + $update_value_clause = implode( ', ', $update_value_statements ); + + return "ON DUPLICATE KEY UPDATE $update_value_clause"; } /** @@ -157,7 +196,13 @@ class MetaToCustomTableMigrator { public function fetch_data_for_migration_for_ids( $entity_ids ) { global $wpdb; - // TODO: Add code to validate params. + if ( empty( $entity_ids ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } + $entity_table_query = $this->build_entity_table_query( $entity_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Output of $this->build_entity_table_query is already prepared. $entity_data = $wpdb->get_results( $entity_table_query ); @@ -191,13 +236,14 @@ class MetaToCustomTableMigrator { $source_entity_table = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['table_name'] ); $source_meta_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['meta_rel_column'] ); $source_destination_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['destination_rel_column'] ); - $source_primary_key_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['primary_key'] ); + $source_primary_key_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['primary_key'] ); $destination_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['table_name'] ); $destination_source_rel_id_column = MigrationHelper::add_table_name_to_column( $destination_table, $this->schema_config['destination']['source_rel_column'] ); + $destination_primary_key = MigrationHelper::add_table_name_to_column( $destination_table, $this->schema_config['destination']['primary_key'] ); $where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; - $entity_keys = array(); + $entity_keys = array(); foreach ( $this->core_column_mapping as $column_name => $column_schema ) { if ( isset( $column_schema['select_clause'] ) ) { $select_clause = $column_schema['select_clause']; @@ -213,8 +259,9 @@ class MetaToCustomTableMigrator { SELECT $source_meta_rel_id_column as entity_meta_rel_id, $source_destination_rel_id_column AS destination_rel_id, + $destination_primary_key AS destination_primary_key, CASE - WHEN $destination_source_rel_id_column IS NOT NULL THEN 'replace' + WHEN $destination_source_rel_id_column IS NOT NULL THEN 'update' ELSE 'insert' END as insert_switch, $entity_column_string @@ -284,8 +331,9 @@ WHERE */ $sanitized_entity_data = array(); $error_records = array(); - $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data ); - $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data ); + $insert_switch = array(); + $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $insert_switch, $entity_data ); + $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $insert_switch, $meta_data ); return array( 'data' => $sanitized_entity_data, @@ -300,10 +348,10 @@ WHERE * @param array $error_records Error records. * @param array $entity_data Original source data. */ - private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, $entity_data ) { + private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, &$insert_switch, $entity_data ) { foreach ( $entity_data as $entity ) { $row_data = array(); - foreach ( $this->core_column_mapping as $column_name => $schema ) { + foreach ( array_merge( $this->get_destination_table_primary_id_schema(), $this->core_column_mapping ) as $column_name => $schema ) { $custom_table_column_name = $schema['destination'] ?? $column_name; $value = $entity->$column_name; $value = $this->validate_data( $value, $schema['type'] ); @@ -313,6 +361,8 @@ WHERE $row_data[ $custom_table_column_name ] = $value; } } + $insert_switch[ $entity->entity_meta_rel_id ] = $entity->insert_switch; // Save the insert_switch in row data so that we can use it later. + $sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data; } } @@ -324,14 +374,16 @@ WHERE * @param array $error_records Error records. * @param array $meta_data Original source data. */ - private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, $meta_data ) { + private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, &$insert_switch, $meta_data ) { foreach ( $meta_data as $datum ) { $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); if ( is_wp_error( $value ) ) { $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}"; } else { - $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value; + $insert_switch_string = $insert_switch[ $datum->entity_id ]; + + $sanitized_entity_data[ $insert_switch_string ][ $datum->entity_id ][ $column_schema['destination'] ] = $value; } } } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php new file mode 100644 index 00000000000..e69de29bb2d From 78e0e6cb780fff9813a5371069975de24fec1fe2 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 18:15:49 +0530 Subject: [PATCH 126/206] Refactor migrator config into dedicated files. --- .../MetaToCustomTableMigrator.php | 172 ++++-- .../MetaToMetaTableMigrator.php | 36 +- .../CustomOrderTable/WPPostToCOTMigrator.php | 490 ++++-------------- .../WPPostToOrderAddressTableMigrator.php | 141 +++++ .../WPPostToOrderOpTableMigrator.php | 114 ++++ .../WPPostToOrderTableMigrator.php | 117 +++++ .../Database/Migrations/MigrationHelper.php | 22 +- 7 files changed, 633 insertions(+), 459 deletions(-) create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php create mode 100644 plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 0bc08e73136..ad8d6e2e05b 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -19,21 +19,23 @@ abstract class MetaToCustomTableMigrator { * * @var array */ - private $schema_config; + protected $schema_config; /** * Meta config, see __construct for detailed config. * * @var array */ - private $meta_column_mapping; + protected $meta_column_mapping; /** * Column mapping from source table to destination custom table. See __construct for detailed config. * * @var array */ - private $core_column_mapping; + protected $core_column_mapping; + + protected $errors; /** * MetaToCustomTableMigrator constructor. @@ -84,13 +86,21 @@ abstract class MetaToCustomTableMigrator { * .... * ). */ - public function __construct( $schema_config, $meta_column_mapping, $core_column_mapping ) { + public function __construct() { // TODO: Add code to validate params. - $this->schema_config = $schema_config; - $this->meta_column_mapping = $meta_column_mapping; - $this->core_column_mapping = $core_column_mapping; + $this->schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() ); + $this->meta_column_mapping = $this->get_meta_column_config(); + $this->core_column_mapping = $this->get_core_column_mapping(); + $this->errors = array(); } + abstract function get_schema_config(); + + abstract function get_core_column_mapping(); + + abstract function get_meta_column_config(); + + /** * Generate SQL for data insertion. * @@ -112,10 +122,13 @@ abstract class MetaToCustomTableMigrator { return "INSERT IGNORE INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. } - public function generate_update_sql_for_batch( $batch ) { + public function generate_update_sql_for_batch( $batch, $entity_row_mapping ) { $table = $this->schema_config['destination']['table_name']; $destination_primary_id_schema = $this->get_destination_table_primary_id_schema(); + foreach ( $batch as $entity_id => $row ) { + $batch[ $entity_id ][ $destination_primary_id_schema['destination_primary_key']['destination'] ] = $entity_row_mapping[ $entity_id ]->destination_id; + } list( $value_sql, $column_sql, $columns ) = $this->generate_column_clauses( array_merge( $destination_primary_id_schema, $this->core_column_mapping, $this->meta_column_mapping ), @@ -127,7 +140,7 @@ abstract class MetaToCustomTableMigrator { return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;"; } - private function get_destination_table_primary_id_schema() { + protected function get_destination_table_primary_id_schema() { return array( 'destination_primary_key' => array( 'destination' => $this->schema_config['destination']['primary_key'], @@ -136,7 +149,7 @@ abstract class MetaToCustomTableMigrator { ); } - private function generate_column_clauses( $columns_schema, $batch ) { + protected function generate_column_clauses( $columns_schema, $batch ) { global $wpdb; $columns = array(); @@ -160,7 +173,7 @@ abstract class MetaToCustomTableMigrator { $value_sql = implode( ',', $values ); - $column_sql = implode( '`, `', MigrationHelper::escape_backtick( $columns ) ); + $column_sql = implode( '`, `', $columns ); return array( $value_sql, $column_sql, $columns ); } @@ -175,6 +188,67 @@ abstract class MetaToCustomTableMigrator { return "ON DUPLICATE KEY UPDATE $update_value_clause"; } + /** + * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. + * + * @param int $batch_size Batch size of records to migrate. + * + * @return array True if migration is completed, false if there are still records to process. + */ + public function process_migration_batch_for_ids( $entity_ids ) { + $data = $this->fetch_data_for_migration_for_ids( $entity_ids ); + + foreach ( $data['errors'] as $entity_id => $error ) { + $this->errors[ $entity_id ] = "Error in importing post id $entity_id: " . print_r( $error, true ); + } + + if ( count( $data['data'] ) === 0 ) { + return array(); + } + + $entity_ids = array_keys( $data['data'] ); + $already_migrated = $this->get_already_migrated_records( $entity_ids ); + + $to_insert = array_diff_key( $data['data'], $already_migrated ); + $this->process_insert_batch( $to_insert ); + + $to_update = array_intersect_key( $data['data'], $already_migrated ); + $this->process_update_batch( $to_update, $already_migrated ); + + return array( + 'errors' => $this->errors + ); + } + + protected function process_insert_batch( $batch ) { + global $wpdb; + if ( 0 === count( $batch ) ) { + return; + } + $queries = $this->generate_insert_sql_for_batch( $batch ); + $result = $wpdb->query( $queries ); + $wpdb->query( "COMMIT;" ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + if ( count( $batch ) !== $result ) { + // Some rows were not inserted. + // TODO: Find and log the entity ids that were not inserted. + } + } + + protected function process_update_batch( $batch, $already_migrated ) { + global $wpdb; + if ( 0 === count( $batch ) ) { + return; + } + $queries = $this->generate_update_sql_for_batch( $batch, $already_migrated ); + $result = $wpdb->query( $queries ); + $wpdb->query( "COMMIT;" ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + if ( count( $batch ) !== $result ) { + // Some rows were not inserted. + // TODO: Find and log the entity ids that were not updateed. + } + } + + /** * Fetch data for migration. * @@ -221,6 +295,33 @@ abstract class MetaToCustomTableMigrator { return $this->process_and_sanitize_data( $entity_data, $meta_data ); } + public function get_already_migrated_records( $entity_ids ) { + global $wpdb; + $source_table = $this->schema_config['source']['entity']['table_name']; + $source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column']; + $source_primary_key_column = $this->schema_config['source']['entity']['primary_key']; + + $destination_table = $this->schema_config['destination']['table_name']; + $destination_source_join_column = $this->schema_config['destination']['source_rel_column']; + $destination_primary_key_column = $this->schema_config['destination']['primary_key']; + + $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ); + + $already_migrated_entity_ids = $wpdb->get_results( + $wpdb->prepare( + " +SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id +FROM `$destination_table` destination +JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column` +WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) + ", + $entity_ids + ) + ); + + return array_column( $already_migrated_entity_ids, null, 'source_id' ); + } + /** * Helper method to build query used to fetch data from core source table. @@ -231,16 +332,11 @@ abstract class MetaToCustomTableMigrator { * * @return string Query that can be used to fetch data. */ - private function build_entity_table_query( $entity_ids ) { + protected function build_entity_table_query( $entity_ids ) { global $wpdb; - $source_entity_table = MigrationHelper::escape_backtick( $this->schema_config['source']['entity']['table_name'] ); - $source_meta_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['meta_rel_column'] ); - $source_destination_rel_id_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['destination_rel_column'] ); - $source_primary_key_column = MigrationHelper::add_table_name_to_column( $source_entity_table, $this->schema_config['source']['entity']['primary_key'] ); - - $destination_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['table_name'] ); - $destination_source_rel_id_column = MigrationHelper::add_table_name_to_column( $destination_table, $this->schema_config['destination']['source_rel_column'] ); - $destination_primary_key = MigrationHelper::add_table_name_to_column( $destination_table, $this->schema_config['destination']['primary_key'] ); + $source_entity_table = $this->schema_config['source']['entity']['table_name']; + $source_meta_rel_id_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['meta_rel_column']}`"; + $source_primary_key_column = "`$source_entity_table`.`{$this->schema_config['source']['entity']['primary_key']}`"; $where_clause = "$source_primary_key_column IN (" . implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; $entity_keys = array(); @@ -249,7 +345,7 @@ abstract class MetaToCustomTableMigrator { $select_clause = $column_schema['select_clause']; $entity_keys[] = "$select_clause AS $column_name"; } else { - $entity_keys[] = MigrationHelper::add_table_name_to_column( $source_entity_table, $column_name ); + $entity_keys[] = "$source_entity_table.$column_name"; } } $entity_column_string = implode( ', ', $entity_keys ); @@ -258,15 +354,8 @@ abstract class MetaToCustomTableMigrator { " SELECT $source_meta_rel_id_column as entity_meta_rel_id, - $source_destination_rel_id_column AS destination_rel_id, - $destination_primary_key AS destination_primary_key, - CASE - WHEN $destination_source_rel_id_column IS NOT NULL THEN 'update' - ELSE 'insert' - END as insert_switch, $entity_column_string FROM `$source_entity_table` -LEFT JOIN `$destination_table` ON $destination_source_rel_id_column = $source_destination_rel_id_column WHERE $where_clause; ", $entity_ids @@ -284,13 +373,13 @@ WHERE $where_clause; * * @return string|void Query for fetching meta data. */ - private function build_meta_data_query( $entity_ids ) { + protected function build_meta_data_query( $entity_ids ) { global $wpdb; - $meta_table = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['table_name'] ); + $meta_table = $this->schema_config['source']['meta']['table_name']; $meta_keys = array_keys( $this->meta_column_mapping ); - $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_key_column'] ); - $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['meta_value_column'] ); - $meta_table_relational_key = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['entity_id_column'] ); + $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $meta_table_relational_key = $this->schema_config['source']['meta']['entity_id_column']; $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); @@ -331,9 +420,8 @@ WHERE */ $sanitized_entity_data = array(); $error_records = array(); - $insert_switch = array(); - $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $insert_switch, $entity_data ); - $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $insert_switch, $meta_data ); + $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data ); + $this->processs_and_sanitize_meta_data( $sanitized_entity_data, $error_records, $meta_data ); return array( 'data' => $sanitized_entity_data, @@ -348,10 +436,10 @@ WHERE * @param array $error_records Error records. * @param array $entity_data Original source data. */ - private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, &$insert_switch, $entity_data ) { + private function process_and_sanitize_entity_data( &$sanitized_entity_data, &$error_records, $entity_data ) { foreach ( $entity_data as $entity ) { $row_data = array(); - foreach ( array_merge( $this->get_destination_table_primary_id_schema(), $this->core_column_mapping ) as $column_name => $schema ) { + foreach ( $this->core_column_mapping as $column_name => $schema ) { $custom_table_column_name = $schema['destination'] ?? $column_name; $value = $entity->$column_name; $value = $this->validate_data( $value, $schema['type'] ); @@ -361,8 +449,6 @@ WHERE $row_data[ $custom_table_column_name ] = $value; } } - $insert_switch[ $entity->entity_meta_rel_id ] = $entity->insert_switch; // Save the insert_switch in row data so that we can use it later. - $sanitized_entity_data[ $entity->entity_meta_rel_id ] = $row_data; } } @@ -374,16 +460,14 @@ WHERE * @param array $error_records Error records. * @param array $meta_data Original source data. */ - private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, &$insert_switch, $meta_data ) { + private function processs_and_sanitize_meta_data( &$sanitized_entity_data, &$error_records, $meta_data ) { foreach ( $meta_data as $datum ) { $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); if ( is_wp_error( $value ) ) { $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}"; } else { - $insert_switch_string = $insert_switch[ $datum->entity_id ]; - - $sanitized_entity_data[ $insert_switch_string ][ $datum->entity_id ][ $column_schema['destination'] ] = $value; + $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] = $value; } } } diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index bef0c8434cc..cbe9b78d7b1 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -90,6 +90,12 @@ class MetaToMetaTableMigrator { */ public function fetch_data_for_migration_for_ids( $order_post_ids ) { global $wpdb; + if ( empty( $order_post_ids ) ) { + return array( + 'data' => array(), + 'errors' => array(), + ); + } $meta_query = $this->build_meta_table_query( $order_post_ids ); @@ -114,34 +120,34 @@ class MetaToMetaTableMigrator { */ private function build_meta_table_query( $entity_ids ) { global $wpdb; - $source_meta_table = MigrationHelper::escape_backtick( $this->schema_config['source']['meta']['table_name'] ); - $source_meta_key_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['meta_key_column'] ); - $source_meta_value_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['meta_value_column'] ); - $source_entity_id_column = MigrationHelper::add_table_name_to_column( $source_meta_table, $this->schema_config['source']['meta']['entity_id_column'] ); + $source_meta_table = $this->schema_config['source']['meta']['table_name']; + $source_meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $source_meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $source_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; $order_by = "$source_entity_id_column ASC"; - $where_clause = "$source_entity_id_column IN (" . implode( array_fill( 0, count( $entity_ids ), '%d') ) . ')'; + $where_clause = "$source_entity_id_column IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d') ) . ')'; - $destination_entity_table = MigrationHelper::escape_backtick( $this->schema_config['destination']['entity']['table_name'] ); - $destination_entity_id_column = MigrationHelper::add_table_name_to_column( $destination_entity_table, $this->schema_config['destination']['entity']['id_column'] ); - $destination_source_id_mapping_column = MigrationHelper::add_table_name_to_column( $destination_entity_table, $this->schema_config['destination']['entity']['source_id_column'] ); + $destination_entity_table = $this->schema_config['destination']['entity']['table_name']; + $destination_entity_id_column = $this->schema_config['destination']['entity']['id_column']; + $destination_source_id_mapping_column = $this->schema_config['destination']['entity']['source_id_column']; if ( $this->schema_config['source']['excluded_keys'] ) { $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. - $exclude_clause = $wpdb->prepare( "$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + $exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); $where_clause = "$where_clause AND $exclude_clause"; } return $wpdb->prepare( " SELECT - $source_entity_id_column as source_entity_id, - $destination_entity_id_column as destination_entity_id, - $source_meta_key_column as meta_key, - $source_meta_value_column as meta_value -FROM `$source_meta_table` -JOIN `$destination_entity_table` ON $destination_source_id_mapping_column = $source_entity_id_column + source.`$source_entity_id_column` as source_entity_id, + destination.`$destination_entity_id_column` as destination_entity_id, + source.`$source_meta_key_column` as meta_key, + source.`$source_meta_value_column` as meta_value +FROM `$source_meta_table` source +JOIN `$destination_entity_table` destination ON destination.`$destination_source_id_mapping_column` = source.`$source_entity_id_column` WHERE $where_clause ORDER BY $order_by ", $entity_ids diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index cbc0e43da17..d03b5e5cdfa 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -6,6 +6,9 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; use Automattic\WooCommerce\DataBase\Migrations\MigrationErrorLogger; +use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderAddressTableMigrator; +use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderOpTableMigrator; +use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderTableMigrator; /** * Class WPPostToCOTMigrator @@ -56,366 +59,23 @@ class WPPostToCOTMigrator { */ private $meta_table_migrator; - /** - * Names of different order tables. - * - * @var array - */ - private $table_names; - /** * WPPostToCOTMigrator constructor. */ public function __construct() { - global $wpdb; - // TODO: Remove hardcoding. - $this->table_names = array( - 'orders' => $wpdb->prefix . 'wc_orders', - 'addresses' => $wpdb->prefix . 'wc_order_addresses', - 'op_data' => $wpdb->prefix . 'wc_order_operational_data', - 'meta' => $wpdb->prefix . 'wc_orders_meta', - ); + $this->order_table_migrator = new WPPostToOrderTableMigrator(); + $this->billing_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'billing' ); + $this->shipping_address_table_migrator = new WPPostToOrderAddressTableMigrator( 'shipping' ); + $this->operation_data_table_migrator = new WPPostToOrderOpTableMigrator(); - $order_config = $this->get_config_for_order_table(); - $billing_address_config = $this->get_config_for_address_table_billing(); - $shipping_address_config = $this->get_config_for_address_table_shipping(); - $operation_data_config = $this->get_config_for_operational_data_table(); - $meta_data_config = $this->get_config_for_meta_table(); - - $this->order_table_migrator = new MetaToCustomTableMigrator( $order_config['schema'], $order_config['meta'], $order_config['core'] ); - $this->billing_address_table_migrator = new MetaToCustomTableMigrator( $billing_address_config['schema'], $billing_address_config['meta'], $billing_address_config['core'] ); - $this->shipping_address_table_migrator = new MetaToCustomTableMigrator( $shipping_address_config['schema'], $shipping_address_config['meta'], $shipping_address_config['core'] ); - $this->operation_data_table_migrator = new MetaToCustomTableMigrator( $operation_data_config['schema'], $operation_data_config['meta'], $operation_data_config['core'] ); + $meta_data_config = $this->get_config_for_meta_table(); $this->meta_table_migrator = new MetaToMetaTableMigrator( $meta_data_config ); $this->error_logger = new MigrationErrorLogger(); } - /** - * Returns migration configuration for order table. - * - * @return array Config for order table. - */ - private function get_config_for_order_table() { - global $wpdb; - $order_table_schema_config = array( - 'source' => array( - 'entity' => array( - 'table_name' => $wpdb->posts, - 'meta_rel_column' => 'ID', - 'destination_rel_column' => 'ID', - 'primary_key' => 'ID', - ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', - ), - ), - 'destination' => array( - 'table_name' => $this->table_names['orders'], - 'source_rel_column' => 'post_id', - ), - ); - - $order_table_core_config = array( - 'ID' => array( - 'type' => 'int', - 'destination' => 'post_id', - ), - 'post_status' => array( - 'type' => 'string', - 'destination' => 'status', - ), - 'post_date_gmt' => array( - 'type' => 'date', - 'destination' => 'date_created_gmt', - ), - 'post_modified_gmt' => array( - 'type' => 'date', - 'destination' => 'date_updated_gmt', - ), - 'post_parent' => array( - 'type' => 'int', - 'destination' => 'parent_order_id', - ), - ); - - $order_table_meta_config = array( - '_order_currency' => array( - 'type' => 'string', - 'destination' => 'currency', - ), - '_order_tax' => array( - 'type' => 'decimal', - 'destination' => 'tax_amount', - ), - '_order_total' => array( - 'type' => 'decimal', - 'destination' => 'total_amount', - ), - '_customer_user' => array( - 'type' => 'int', - 'destination' => 'customer_id', - ), - '_billing_email' => array( - 'type' => 'string', - 'destination' => 'billing_email', - ), - '_payment_method' => array( - 'type' => 'string', - 'destination' => 'payment_method', - ), - '_payment_method_title' => array( - 'type' => 'string', - 'destination' => 'payment_method_title', - ), - '_customer_ip_address' => array( - 'type' => 'string', - 'destination' => 'ip_address', - ), - '_customer_user_agent' => array( - 'type' => 'string', - 'destination' => 'user_agent', - ), - '_transaction_id' => array( - 'type' => 'string', - 'destination' => 'transaction_id', - ), - ); - - return array( - 'schema' => $order_table_schema_config, - 'core' => $order_table_core_config, - 'meta' => $order_table_meta_config, - ); - } - - /** - * Get configuration for billing addresses for migration. - * - * @return array Billing address migration config. - */ - private function get_config_for_address_table_billing() { - return $this->get_config_for_address_table( 'billing' ); - } - - /** - * Get configuration for shipping addresses for migration. - * - * @return array Shipping address migration config. - */ - private function get_config_for_address_table_shipping() { - return $this->get_config_for_address_table( 'shipping' ); - } - - /** - * Generate config for address data. - * - * @param string $type Type of address, this will be using in fetching meta keys. - * - * @return array Config for address table. - */ - private function get_config_for_address_table( $type ) { - global $wpdb; - // We join order core table and post meta table to get data for address, since we need order ID. - // So order core record needs to be already present. - $schema_config = array( - 'source' => array( - 'entity' => array( - 'table_name' => $this->table_names['orders'], - 'meta_rel_column' => 'post_id', - 'destination_rel_column' => 'id', - 'primary_key' => 'post_id', - ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', - ), - ), - 'destination' => array( - 'table_name' => $this->table_names['addresses'], - 'source_rel_column' => 'order_id', - ), - ); - - $core_config = array( - 'id' => array( - 'type' => 'int', - 'destination' => 'order_id', - ), - 'type' => array( - 'type' => 'string', - 'destination' => 'address_type', - 'select_clause' => "'$type'", - ), - ); - - $meta_config = array( - "_{$type}_first_name" => array( - 'type' => 'string', - 'destination' => 'first_name', - ), - "_{$type}_last_name" => array( - 'type' => 'string', - 'destination' => 'last_name', - ), - "_{$type}_company" => array( - 'type' => 'string', - 'destination' => 'company', - ), - "_{$type}_address_1" => array( - 'type' => 'string', - 'destination' => 'address_1', - ), - "_{$type}_address_2" => array( - 'type' => 'string', - 'destination' => 'address_2', - ), - "_{$type}_city" => array( - 'type' => 'string', - 'destination' => 'city', - ), - "_{$type}_state" => array( - 'type' => 'string', - 'destination' => 'state', - ), - "_{$type}_postcode" => array( - 'type' => 'string', - 'destination' => 'postcode', - ), - "_{$type}_country" => array( - 'type' => 'string', - 'destination' => 'country', - ), - "_{$type}_email" => array( - 'type' => 'string', - 'destination' => 'email', - ), - "_{$type}_phone" => array( - 'type' => 'string', - 'destination' => 'phone', - ), - ); - - return array( - 'schema' => $schema_config, - 'core' => $core_config, - 'meta' => $meta_config, - ); - } - - /** - * Generate config for operational data. - * - * @return array Config for operational data table. - */ - private function get_config_for_operational_data_table() { - global $wpdb; - - $schema_config = array( - 'source' => array( - 'entity' => array( - 'table_name' => $this->table_names['orders'], - 'meta_rel_column' => 'post_id', - 'destination_rel_column' => 'id', - 'primary_key' => 'post_id', - ), - 'meta' => array( - 'table_name' => $wpdb->postmeta, - 'meta_key_column' => 'meta_key', - 'meta_value_column' => 'meta_value', - 'entity_id_column' => 'post_id', - ), - ), - 'destination' => array( - 'table_name' => $this->table_names['op_data'], - 'source_rel_column' => 'order_id', - ), - ); - - $core_config = array( - 'id' => array( - 'type' => 'int', - 'destination' => 'order_id', - ), - ); - - $meta_config = array( - '_created_via' => array( - 'type' => 'string', - 'destination' => 'created_via', - ), - '_order_version' => array( - 'type' => 'string', - 'destination' => 'woocommerce_version', - ), - '_prices_include_tax' => array( - 'type' => 'bool', - 'destination' => 'prices_include_tax', - ), - '_recorded_coupon_usage_counts' => array( - 'type' => 'bool', - 'destination' => 'coupon_usages_are_counted', - ), - '_download_permissions_granted' => array( - 'type' => 'bool', - 'destination' => 'download_permission_granted', - ), - '_cart_hash' => array( - 'type' => 'string', - 'destination' => 'cart_hash', - ), - '_new_order_email_sent' => array( - 'type' => 'bool', - 'destination' => 'new_order_email_sent', - ), - '_order_key' => array( - 'type' => 'string', - 'destination' => 'order_key', - ), - '_order_stock_reduced' => array( - 'type' => 'bool', - 'destination' => 'order_stock_reduced', - ), - '_date_paid' => array( - 'type' => 'date_epoch', - 'destination' => 'date_paid_gmt', - ), - '_date_completed' => array( - 'type' => 'date_epoch', - 'destination' => 'date_completed_gmt', - ), - '_order_shipping_tax' => array( - 'type' => 'decimal', - 'destination' => 'shipping_tax_amount', - ), - '_order_shipping' => array( - 'type' => 'decimal', - 'destination' => 'shipping_total_amount', - ), - '_cart_discount_tax' => array( - 'type' => 'decimal', - 'destination' => 'discount_tax_amount', - ), - '_cart_discount' => array( - 'type' => 'decimal', - 'destination' => 'discount_total_amount', - ), - ); - - return array( - 'schema' => $schema_config, - 'core' => $core_config, - 'meta' => $meta_config, - ); - } - /** * Generate config for meta data migration. * @@ -423,11 +83,18 @@ class WPPostToCOTMigrator { */ private function get_config_for_meta_table() { global $wpdb; + // TODO: Remove hardcoding. + $this->table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); - $excluded_columns = array_keys( $this->get_config_for_order_table()['meta'] ); - $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_operational_data_table()['meta'] ) ); - $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_address_table_billing()['meta'] ) ); - $excluded_columns = array_merge( $excluded_columns, array_keys( $this->get_config_for_address_table_shipping()['meta'] ) ); + $excluded_columns = array_keys( $this->order_table_migrator->get_meta_column_config() ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->billing_address_table_migrator->get_meta_column_config() ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->shipping_address_table_migrator->get_meta_column_config() ) ); + $excluded_columns = array_merge( $excluded_columns, array_keys( $this->operation_data_table_migrator->get_meta_column_config() ) ); return array( 'source' => array( @@ -456,6 +123,66 @@ class WPPostToCOTMigrator { ); } + /** + * @param MetaToCustomTableMigrator $migrator + * @param $type + * @param $order_post_ids + */ + private function process_next_address_batch( $migrator, $type, $order_post_ids ) { + global $wpdb; + + if ( 0 === count( $order_post_ids ) ) { + return; + } + + $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + + foreach ( $data['errors'] as $order_id => $error ) { + // TODO: Add name of the migrator in error message. + $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); + } + + $to_insert = isset( $data['data']['insert'] ) ? $data['data']['insert'] : array(); + $to_update = isset( $data['data']['update'] ) ? $data['data']['update'] : array(); + + if ( 0 === count( $to_insert ) && 0 === count( $to_update ) ) { + return; + } + + $order_post_ids_placeholders = implode( ',', array_fill( 0, count( $order_post_ids ), '%d' ) ); + + $already_migrated = $wpdb->get_results( + $wpdb->prepare( + " +SELECT addresses.id, addresses.order_id, orders.post_id FROM {$this->table_names['addresses']} as addresses +JOIN {$this->table_names['orders']} orders ON addresses.order_id = orders.id +WHERE + address_type = %s AND + orders.post_id IN ( $order_post_ids_placeholders ) + ", + $type, + $order_post_ids + ) + ); + + $order_post_ids_to_migrate = array_diff( $order_post_ids, array_column( $already_migrated, 'post_id' ) ); + $to_insert = array_intersect_key( $to_insert, array_flip( $order_post_ids_to_migrate ) ); + $this->execute_action_for_batch( $migrator, $to_insert, 'insert' ); + + if ( 0 < count( $to_update ) ) { + echo 'comes here'; + } + $to_update_intersect = array(); + $ids_to_update = array_flip( array_column( $already_migrated, 'id' ) ); + foreach ( $to_update as $post_id => $address_record ) { + if ( isset( $ids_to_update[ $address_record['id'] ] ) ) { + $to_update_intersect[ $post_id ] = $address_record; + } + } + + $this->execute_action_for_batch( $migrator, $to_update_intersect, 'update' ); + } + /** * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. * @@ -464,34 +191,17 @@ class WPPostToCOTMigrator { * @return bool True if migration is completed, false if there are still records to process. */ public function process_next_migration_batch( $batch_size = 100 ) { - global $wpdb; - - $order_data = $this->order_table_migrator->fetch_data_for_migration_for_ids( $this->get_next_batch_ids( $batch_size ) ); - - foreach ( $order_data['errors'] as $post_id => $error ) { - $this->error_logger->log( 'info', "Error in importing post id $post_id: " . print_r( $error, true ) ); - } - - if ( count( $order_data['data'] ) === 0 ) { + $order_post_ids = $this->get_next_batch_ids( $batch_size ); + if ( 0 === count( $order_post_ids ) ) { return true; } - $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $order_data['data'], 'insert' ); - $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. - $wpdb->query("COMMIT;"); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? - if ( count( $order_data['data'] ) !== $result ) { - // Some rows were not inserted. - // TODO: Find and log the entity ids that were not inserted. - echo ' error '; - } + $this->order_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->billing_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->shipping_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); + $this->operation_data_table_migrator->process_migration_batch_for_ids( $order_post_ids ); - $order_post_ids = array_column( $order_data['data'], 'post_id' ); - $this->process_next_migrator_batch( $this->billing_address_table_migrator, $order_post_ids ); - $this->process_next_migrator_batch( $this->shipping_address_table_migrator, $order_post_ids ); - $this->process_next_migrator_batch( $this->operation_data_table_migrator, $order_post_ids ); - $this->process_meta_migration( $order_post_ids ); - - $last_post_migrated = max( array_keys( $order_data['data'] ) ); + $last_post_migrated = max( $order_post_ids ); $this->update_checkpoint( $last_post_migrated ); return false; @@ -505,20 +215,18 @@ class WPPostToCOTMigrator { * @param string $order_by Order by clause. */ private function process_next_migrator_batch( $migrator, $order_post_ids ) { - global $wpdb; - $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + foreach ( $data['errors'] as $order_id => $error ) { // TODO: Add name of the migrator in error message. $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); } - $queries = $migrator->generate_insert_sql_for_batch( $data['data'], 'insert' ); - $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Insert statements should already be escaped. - $wpdb->query('COMMIT;'); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? - if ( count( $data['data'] ) !== $result ) { - // Some rows were not inserted. - // TODO: Find and log the entity ids that were not inserted. - echo 'error'; - } + + $to_replace = isset( $data['data']['update'] ) ? $data['data']['update'] : array(); + $this->execute_action_for_batch( $migrator, $to_replace, 'update' ); + + $to_insert = isset( $data['data']['insert'] ) ? $data['data']['insert'] : array(); + $this->execute_action_for_batch( $migrator, $to_insert, 'insert' ); } /** @@ -528,8 +236,8 @@ class WPPostToCOTMigrator { */ private function process_meta_migration( $order_post_ids ) { global $wpdb; - $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration_for_ids( $order_post_ids ); - $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); + $data_to_migrate = $this->meta_table_migrator->fetch_data_for_migration_for_ids( $order_post_ids ); + $insert_queries = $this->meta_table_migrator->generate_insert_sql_for_batch( $data_to_migrate['data'], 'insert' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $insert_queries should already be escaped in the generating function. $result = $wpdb->query( $insert_queries ); if ( count( $data_to_migrate['data'] ) !== $result ) { @@ -574,8 +282,8 @@ class WPPostToCOTMigrator { private function get_next_batch_ids( $batch_size ) { global $wpdb; - $checkpoint = $this->get_checkpoint(); - $post_ids = $wpdb->get_col( + $checkpoint = $this->get_checkpoint(); + $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = '%s' LIMIT %d", $checkpoint['id'], @@ -622,7 +330,7 @@ class WPPostToCOTMigrator { * @param int $id Order ID. */ public function update_checkpoint( $id ) { - return update_option( 'wc_cot_migration', array( 'id' => $id ) ); + return update_option( 'wc_cot_migration', array( 'id' => $id ), false ); } public function delete_checkpoint() { diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php index e69de29bb2d..55c7fb438bc 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php @@ -0,0 +1,141 @@ +type = $type; + parent::__construct(); + } + + function get_schema_config() { + global $wpdb; + // TODO: Remove hardcoding. + $this->table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + 'primary_key' => 'post_id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['addresses'], + 'source_rel_column' => 'order_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + function get_core_column_mapping() { + $type = $this->type; + return array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + 'type' => array( + 'type' => 'string', + 'destination' => 'address_type', + 'select_clause' => "'$type'", + ), + ); + } + + function get_meta_column_config() { + $type = $this->type; + return array( + "_{$type}_first_name" => array( + 'type' => 'string', + 'destination' => 'first_name', + ), + "_{$type}_last_name" => array( + 'type' => 'string', + 'destination' => 'last_name', + ), + "_{$type}_company" => array( + 'type' => 'string', + 'destination' => 'company', + ), + "_{$type}_address_1" => array( + 'type' => 'string', + 'destination' => 'address_1', + ), + "_{$type}_address_2" => array( + 'type' => 'string', + 'destination' => 'address_2', + ), + "_{$type}_city" => array( + 'type' => 'string', + 'destination' => 'city', + ), + "_{$type}_state" => array( + 'type' => 'string', + 'destination' => 'state', + ), + "_{$type}_postcode" => array( + 'type' => 'string', + 'destination' => 'postcode', + ), + "_{$type}_country" => array( + 'type' => 'string', + 'destination' => 'country', + ), + "_{$type}_email" => array( + 'type' => 'string', + 'destination' => 'email', + ), + "_{$type}_phone" => array( + 'type' => 'string', + 'destination' => 'phone', + ), + ); + } + + public function get_already_migrated_records( $entity_ids ) { + global $wpdb; + $source_table = $this->schema_config['source']['entity']['table_name']; + $source_destination_join_column = $this->schema_config['source']['entity']['destination_rel_column']; + $source_primary_key_column = $this->schema_config['source']['entity']['primary_key']; + + $destination_table = $this->schema_config['destination']['table_name']; + $destination_source_join_column = $this->schema_config['destination']['source_rel_column']; + $destination_primary_key_column = $this->schema_config['destination']['primary_key']; + + $address_type = $this->type; + + $entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) ); + + $already_migrated_entity_ids = $wpdb->get_results( + $wpdb->prepare( + " +SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id +FROM `$destination_table` destination +JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column` +WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) AND destination.`address_type` = '$address_type' + ", + $entity_ids + ) + ); + + return array_column( $already_migrated_entity_ids, null, 'source_id' ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php new file mode 100644 index 00000000000..6c7645a6b31 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php @@ -0,0 +1,114 @@ +table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $this->table_names['orders'], + 'meta_rel_column' => 'post_id', + 'destination_rel_column' => 'id', + 'primary_key' => 'post_id', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['op_data'], + 'source_rel_column' => 'order_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + public function get_core_column_mapping() { + return array( + 'id' => array( + 'type' => 'int', + 'destination' => 'order_id', + ), + ); + } + + public function get_meta_column_config() { + return array( + '_created_via' => array( + 'type' => 'string', + 'destination' => 'created_via', + ), + '_order_version' => array( + 'type' => 'string', + 'destination' => 'woocommerce_version', + ), + '_prices_include_tax' => array( + 'type' => 'bool', + 'destination' => 'prices_include_tax', + ), + '_recorded_coupon_usage_counts' => array( + 'type' => 'bool', + 'destination' => 'coupon_usages_are_counted', + ), + '_download_permissions_granted' => array( + 'type' => 'bool', + 'destination' => 'download_permission_granted', + ), + '_cart_hash' => array( + 'type' => 'string', + 'destination' => 'cart_hash', + ), + '_new_order_email_sent' => array( + 'type' => 'bool', + 'destination' => 'new_order_email_sent', + ), + '_order_key' => array( + 'type' => 'string', + 'destination' => 'order_key', + ), + '_order_stock_reduced' => array( + 'type' => 'bool', + 'destination' => 'order_stock_reduced', + ), + '_date_paid' => array( + 'type' => 'date_epoch', + 'destination' => 'date_paid_gmt', + ), + '_date_completed' => array( + 'type' => 'date_epoch', + 'destination' => 'date_completed_gmt', + ), + '_order_shipping_tax' => array( + 'type' => 'decimal', + 'destination' => 'shipping_tax_amount', + ), + '_order_shipping' => array( + 'type' => 'decimal', + 'destination' => 'shipping_total_amount', + ), + '_cart_discount_tax' => array( + 'type' => 'decimal', + 'destination' => 'discount_tax_amount', + ), + '_cart_discount' => array( + 'type' => 'decimal', + 'destination' => 'discount_total_amount', + ), + ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php new file mode 100644 index 00000000000..b0b7a7ec840 --- /dev/null +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php @@ -0,0 +1,117 @@ +table_names = array( + 'orders' => $wpdb->prefix . 'wc_orders', + 'addresses' => $wpdb->prefix . 'wc_order_addresses', + 'op_data' => $wpdb->prefix . 'wc_order_operational_data', + 'meta' => $wpdb->prefix . 'wc_orders_meta', + ); + + return array( + 'source' => array( + 'entity' => array( + 'table_name' => $wpdb->posts, + 'meta_rel_column' => 'ID', + 'destination_rel_column' => 'ID', + 'primary_key' => 'ID', + ), + 'meta' => array( + 'table_name' => $wpdb->postmeta, + 'meta_key_column' => 'meta_key', + 'meta_value_column' => 'meta_value', + 'entity_id_column' => 'post_id', + ), + ), + 'destination' => array( + 'table_name' => $this->table_names['orders'], + 'source_rel_column' => 'post_id', + 'primary_key' => 'id', + 'primary_key_type' => 'int', + ), + ); + } + + public function get_core_column_mapping() { + return array( + 'ID' => array( + 'type' => 'int', + 'destination' => 'post_id', + ), + 'post_status' => array( + 'type' => 'string', + 'destination' => 'status', + ), + 'post_date_gmt' => array( + 'type' => 'date', + 'destination' => 'date_created_gmt', + ), + 'post_modified_gmt' => array( + 'type' => 'date', + 'destination' => 'date_updated_gmt', + ), + 'post_parent' => array( + 'type' => 'int', + 'destination' => 'parent_order_id', + ), + ); + } + + public function get_meta_column_config() { + return array( + '_order_currency' => array( + 'type' => 'string', + 'destination' => 'currency', + ), + '_order_tax' => array( + 'type' => 'decimal', + 'destination' => 'tax_amount', + ), + '_order_total' => array( + 'type' => 'decimal', + 'destination' => 'total_amount', + ), + '_customer_user' => array( + 'type' => 'int', + 'destination' => 'customer_id', + ), + '_billing_email' => array( + 'type' => 'string', + 'destination' => 'billing_email', + ), + '_payment_method' => array( + 'type' => 'string', + 'destination' => 'payment_method', + ), + '_payment_method_title' => array( + 'type' => 'string', + 'destination' => 'payment_method_title', + ), + '_customer_ip_address' => array( + 'type' => 'string', + 'destination' => 'ip_address', + ), + '_customer_user_agent' => array( + 'type' => 'string', + 'destination' => 'user_agent', + ), + '_transaction_id' => array( + 'type' => 'string', + 'destination' => 'transaction_id', + ), + ); + } +} diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php index 72e1c3ad4fd..d0af0007a4f 100644 --- a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php +++ b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php @@ -40,9 +40,12 @@ class MigrationHelper { case 'insert_ignore': $insert_query = 'INSERT IGNORE'; break; - case 'replace': + case 'replace': // delete and then insert. $insert_query = 'REPLACE'; break; + case 'update': + $insert_query = 'UPDATE'; + break; case 'insert': default: $insert_query = 'INSERT'; @@ -51,6 +54,13 @@ class MigrationHelper { return $insert_query; } + public static function escape_schema_for_backtick( $schema_config ) { + array_walk( $schema_config['source']['entity'], array( self::class, 'escape_and_add_backtick' ) ); + array_walk( $schema_config['source']['meta'], array( self::class, 'escape_and_add_backtick' ) ); + array_walk( $schema_config['destination'], array( self::class, 'escape_and_add_backtick' ) ); + return $schema_config; + } + /** * Helper method to escape backtick in column and table names. * WP does not provide a method to escape table/columns names yet, but hopefully soon in @link https://core.trac.wordpress.org/ticket/52506 @@ -59,8 +69,8 @@ class MigrationHelper { * * @return array|string|string[] Escaped identifier. */ - public static function escape_backtick( $identifier ) { - return str_replace( '`', '``', $identifier ); + public static function escape_and_add_backtick( $identifier ) { + return '`' . str_replace( '`', '``', $identifier ) . '`'; } /** @@ -74,10 +84,4 @@ class MigrationHelper { return self::$wpdb_placeholder_for_type[ $type ]; } - public static function add_table_name_to_column( $table_name, $column_name ) { - $table_name = self::escape_backtick( $table_name ); - $column_name = self::escape_backtick( $column_name ); - return "`$table_name`.`$column_name`"; - } - } From eff7569dfc7fd40501989ff9b9869064ad153582 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 18:16:42 +0530 Subject: [PATCH 127/206] Add unit tests for handling duplicates. --- .../WPPostToCOTMigratorTest.php | 240 +++++++++++------- 1 file changed, 152 insertions(+), 88 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 57d7868b06d..4de5d1cfce6 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -35,136 +35,113 @@ class WPPostToCOTMigratorTest extends \WC_Unit_Test_Case { parent::setUp(); $this->create_order_custom_table_if_not_exist(); $this->data_store = wc_get_container()->get( OrdersTableDataStore::class ); - $this->sut = wc_get_container()->get( WPPostToCOTMigrator::class ); + $this->sut = wc_get_container()->get( WPPostToCOTMigrator::class ); } public function test_process_next_migration_batch_normal_order() { - $order = wc_get_order( $this->create_complex_wp_post_order() ); $this->clear_all_orders_and_reset_checkpoint(); $this->sut->process_next_migration_batch( 100 ); - $db_order = $this->get_order_from_cot( $order ); - - // Verify core data. - $this->assertEquals( $order->get_id(), $db_order->post_id ); - $this->assertEquals( 'wc-' . $order->get_status(), $db_order->status ); - $this->assertEquals( 'INR', $db_order->currency ); - $this->assertEquals( $order->get_customer_id(), $db_order->customer_id ); - $this->assertEquals( $order->get_billing_email(), $db_order->billing_email ); - $this->assertEquals( $order->get_payment_method(), $db_order->payment_method ); - $this->assertEquals( - $order->get_date_created()->date( DATE_ISO8601 ), - ( new WC_DateTime( $db_order->date_created_gmt ) )->date( DATE_ISO8601 ) - ); - $this->assertEquals( $order->get_date_modified()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order->date_updated_gmt ) )->date( DATE_ISO8601 ) ); - $this->assertEquals( $order->get_parent_id(), $db_order->parent_order_id ); - $this->assertEquals( $order->get_payment_method_title(), $db_order->payment_method_title ); - $this->assertEquals( $order->get_transaction_id(), $db_order->transaction_id ); - $this->assertEquals( $order->get_customer_ip_address(), $db_order->ip_address ); - $this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent ); - - // Verify order billing address. - $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'billing' ); - $this->assertEquals( $order->get_billing_first_name(), $db_order_address->first_name ); - $this->assertEquals( $order->get_billing_last_name(), $db_order_address->last_name ); - $this->assertEquals( $order->get_billing_company(), $db_order_address->company ); - $this->assertEquals( $order->get_billing_address_1(), $db_order_address->address_1 ); - $this->assertEquals( $order->get_billing_address_2(), $db_order_address->address_2 ); - $this->assertEquals( $order->get_billing_city(), $db_order_address->city ); - $this->assertEquals( $order->get_billing_postcode(), $db_order_address->postcode ); - $this->assertEquals( $order->get_billing_country(), $db_order_address->country ); - $this->assertEquals( $order->get_billing_email(), $db_order_address->email ); - $this->assertEquals( $order->get_billing_phone(), $db_order_address->phone ); - - // Verify order shipping address. - $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'shipping' ); - $this->assertEquals( $order->get_shipping_first_name(), $db_order_address->first_name ); - $this->assertEquals( $order->get_shipping_last_name(), $db_order_address->last_name ); - $this->assertEquals( $order->get_shipping_company(), $db_order_address->company ); - $this->assertEquals( $order->get_shipping_address_1(), $db_order_address->address_1 ); - $this->assertEquals( $order->get_shipping_address_2(), $db_order_address->address_2 ); - $this->assertEquals( $order->get_shipping_city(), $db_order_address->city ); - $this->assertEquals( $order->get_shipping_postcode(), $db_order_address->postcode ); - $this->assertEquals( $order->get_shipping_country(), $db_order_address->country ); - $this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone ); - - // Verify order operational data. - $db_order_op_data = $this->get_order_operational_data_from_cot( $db_order->id ); - $this->assertEquals( $order->get_created_via(), $db_order_op_data->created_via ); - $this->assertEquals( $order->get_version(), $db_order_op_data->woocommerce_version ); - $this->assertEquals( $order->get_prices_include_tax(), $db_order_op_data->prices_include_tax ); - $this->assertEquals( - wc_string_to_bool( $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ), - $db_order_op_data->coupon_usages_are_counted - ); - $this->assertEquals( - wc_string_to_bool( $order->get_data_store()->get_download_permissions_granted( $order ) ), - $db_order_op_data->download_permission_granted - ); - $this->assertEquals( $order->get_cart_hash(), $db_order_op_data->cart_hash ); - $this->assertEquals( - wc_string_to_bool( $order->get_meta( '_new_order_email_sent' ) ), - $db_order_op_data->new_order_email_sent - ); - $this->assertEquals( $order->get_order_key(), $db_order_op_data->order_key ); - $this->assertEquals( $order->get_data_store()->get_stock_reduced( $order ), $db_order_op_data->order_stock_reduced ); - $this->assertEquals( - $order->get_date_paid()->date( DATE_ISO8601 ), - ( new WC_DateTime( $db_order_op_data->date_paid_gmt ) )->date( DATE_ISO8601 ) - ); - $this->assertEquals( - $order->get_date_completed()->date( DATE_ISO8601 ), - ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) - ); - $this->assertEquals( $order->get_shipping_tax(), $db_order_op_data->shipping_tax_amount ); - $this->assertEquals( $order->get_shipping_total(), $db_order_op_data->shipping_total_amount ); - $this->assertEquals( $order->get_discount_tax(), $db_order_op_data->discount_tax_amount ); - $this->assertEquals( $order->get_discount_total(), $db_order_op_data->discount_total_amount ); - } - - public function test_process_next_migration_batch_interrupted_migrating_order() { + $this->assert_core_data_is_migrated( $order ); + $this->assert_order_addresses_are_migrated( $order ); + $this->assert_order_op_data_is_migrated( $order ); } public function test_process_next_migration_batch_already_migrated_order() { + global $wpdb; + $order = wc_get_order( $this->create_complex_wp_post_order() ); + $this->clear_all_orders_and_reset_checkpoint(); + // Run the migration once. + $this->sut->process_next_migration_batch( 100 ); + + // Delete checkpoint and run migration again, assert there are still no duplicates. + $this->sut->update_checkpoint( 0 ); + $this->sut->process_next_migration_batch( 100 ); + + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_orders_table_name()} +WHERE post_id = {$order->get_id()}" + ), + 'Order record is duplicated.' + ); + $order_id = $wpdb->get_var( "SELECT id FROM {$this->data_store::get_orders_table_name()} WHERE post_id = {$order->get_id()}" ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()} +WHERE order_id = {$order_id} AND address_type = 'billing' +" + ) + ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_addresses_table_name()} +WHERE order_id = {$order_id} AND address_type = 'shipping' +" + ) + ); + $this->assertEquals( + 1, + $wpdb->get_var( + " +SELECT COUNT(*) FROM {$this->data_store::get_operational_data_table_name()} +WHERE order_id = {$order_id} +" + ) + ); + } + + public function test_process_next_migration_batch_interrupted_migrating_order() { + $this->markTestSkipped(); } public function test_process_next_migration_batch_invalid_order_data() { - + $this->markTestSkipped(); } public function test_process_next_migration_batch_invalid_valid_order_combo() { - + $this->markTestSkipped(); } public function test_process_next_migration_batch_stale_order() { - + $this->markTestSkipped(); } private function get_order_from_cot( $post_order ) { global $wpdb; $order_table = $this->data_store::get_orders_table_name(); - $query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};"; + $query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};"; + return $wpdb->get_row( $query ); } private function get_address_details_from_cot( $order_id, $address_type ) { global $wpdb; $address_table = $this->data_store::get_addresses_table_name(); + return $wpdb->get_row( "SELECT * FROM $address_table WHERE order_id = $order_id AND address_type = '$address_type';" ); } private function get_order_operational_data_from_cot( $order_id ) { global $wpdb; $operational_data_table = $this->data_store::get_operational_data_table_name(); + return $wpdb->get_row( "SELECT * FROM $operational_data_table WHERE order_id = $order_id;" ); } private function create_complex_wp_post_order() { update_option( 'woocommerce_prices_include_tax', 'yes' ); update_option( 'woocommerce_calc_taxes', 'yes' ); - $customer = CustomerHelper::create_customer(); + $uniq_cust_id = wp_generate_password( 10, false ); + $customer = CustomerHelper::create_customer( "user$uniq_cust_id", $uniq_cust_id, "user$uniq_cust_id@example.com" ); $tax_rate = array( 'tax_rate_country' => '', 'tax_rate_state' => '', @@ -228,6 +205,93 @@ class WPPostToCOTMigratorTest extends \WC_Unit_Test_Case { return $order->get_id(); } + private function assert_core_data_is_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + + // Verify core data. + $this->assertEquals( $order->get_id(), $db_order->post_id ); + $this->assertEquals( 'wc-' . $order->get_status(), $db_order->status ); + $this->assertEquals( 'INR', $db_order->currency ); + $this->assertEquals( $order->get_customer_id(), $db_order->customer_id ); + $this->assertEquals( $order->get_billing_email(), $db_order->billing_email ); + $this->assertEquals( $order->get_payment_method(), $db_order->payment_method ); + $this->assertEquals( + $order->get_date_created()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order->date_created_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( $order->get_date_modified()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order->date_updated_gmt ) )->date( DATE_ISO8601 ) ); + $this->assertEquals( $order->get_parent_id(), $db_order->parent_order_id ); + $this->assertEquals( $order->get_payment_method_title(), $db_order->payment_method_title ); + $this->assertEquals( $order->get_transaction_id(), $db_order->transaction_id ); + $this->assertEquals( $order->get_customer_ip_address(), $db_order->ip_address ); + $this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent ); + } + + private function assert_order_addresses_are_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + + // Verify order billing address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'billing' ); + $this->assertEquals( $order->get_billing_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_billing_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_billing_company(), $db_order_address->company ); + $this->assertEquals( $order->get_billing_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_billing_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_billing_city(), $db_order_address->city ); + $this->assertEquals( $order->get_billing_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_billing_country(), $db_order_address->country ); + $this->assertEquals( $order->get_billing_email(), $db_order_address->email ); + $this->assertEquals( $order->get_billing_phone(), $db_order_address->phone ); + + // Verify order shipping address. + $db_order_address = $this->get_address_details_from_cot( $db_order->id, 'shipping' ); + $this->assertEquals( $order->get_shipping_first_name(), $db_order_address->first_name ); + $this->assertEquals( $order->get_shipping_last_name(), $db_order_address->last_name ); + $this->assertEquals( $order->get_shipping_company(), $db_order_address->company ); + $this->assertEquals( $order->get_shipping_address_1(), $db_order_address->address_1 ); + $this->assertEquals( $order->get_shipping_address_2(), $db_order_address->address_2 ); + $this->assertEquals( $order->get_shipping_city(), $db_order_address->city ); + $this->assertEquals( $order->get_shipping_postcode(), $db_order_address->postcode ); + $this->assertEquals( $order->get_shipping_country(), $db_order_address->country ); + $this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone ); + } + + private function assert_order_op_data_is_migrated( $order ) { + $db_order = $this->get_order_from_cot( $order ); + // Verify order operational data. + $db_order_op_data = $this->get_order_operational_data_from_cot( $db_order->id ); + $this->assertEquals( $order->get_created_via(), $db_order_op_data->created_via ); + $this->assertEquals( $order->get_version(), $db_order_op_data->woocommerce_version ); + $this->assertEquals( $order->get_prices_include_tax(), $db_order_op_data->prices_include_tax ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_recorded_coupon_usage_counts( $order ) ), + $db_order_op_data->coupon_usages_are_counted + ); + $this->assertEquals( + wc_string_to_bool( $order->get_data_store()->get_download_permissions_granted( $order ) ), + $db_order_op_data->download_permission_granted + ); + $this->assertEquals( $order->get_cart_hash(), $db_order_op_data->cart_hash ); + $this->assertEquals( + wc_string_to_bool( $order->get_meta( '_new_order_email_sent' ) ), + $db_order_op_data->new_order_email_sent + ); + $this->assertEquals( $order->get_order_key(), $db_order_op_data->order_key ); + $this->assertEquals( $order->get_data_store()->get_stock_reduced( $order ), $db_order_op_data->order_stock_reduced ); + $this->assertEquals( + $order->get_date_paid()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_paid_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( + $order->get_date_completed()->date( DATE_ISO8601 ), + ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) + ); + $this->assertEquals( $order->get_shipping_tax(), $db_order_op_data->shipping_tax_amount ); + $this->assertEquals( $order->get_shipping_total(), $db_order_op_data->shipping_total_amount ); + $this->assertEquals( $order->get_discount_tax(), $db_order_op_data->discount_tax_amount ); + $this->assertEquals( $order->get_discount_total(), $db_order_op_data->discount_total_amount ); + } + private function clear_all_orders_and_reset_checkpoint() { global $wpdb; $order_tables = $this->data_store->get_all_table_names(); @@ -243,7 +307,7 @@ class WPPostToCOTMigratorTest extends \WC_Unit_Test_Case { $order_table_controller->show_feature(); $this->synchronizer = wc_get_container() ->get( DataSynchronizer::class ); - if( ! $this->synchronizer->check_orders_table_exists() ) { + if ( ! $this->synchronizer->check_orders_table_exists() ) { $this->synchronizer->create_database_tables(); } } From 02376e6ec5b73417a33d2f31b65f3fa3653961e0 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 19:19:17 +0530 Subject: [PATCH 128/206] Add return statement to try to fix CI. --- .../Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 4de5d1cfce6..c2ae02a084e 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -31,7 +31,7 @@ class WPPostToCOTMigratorTest extends \WC_Unit_Test_Case { */ private $data_store; - public function setUp() { + public function setUp(): void { parent::setUp(); $this->create_order_custom_table_if_not_exist(); $this->data_store = wc_get_container()->get( OrdersTableDataStore::class ); From d5b164622f1e1695beaa4518796330cc9013b477 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 19:44:29 +0530 Subject: [PATCH 129/206] Applied coding standards. --- .../MetaToCustomTableMigrator.php | 213 ++++++++++++------ 1 file changed, 143 insertions(+), 70 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index ad8d6e2e05b..7347781a3fe 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -35,34 +35,72 @@ abstract class MetaToCustomTableMigrator { */ protected $core_column_mapping; + /** + * Store errors along with entity IDs from migrations. + * + * @var array + */ protected $errors; /** * MetaToCustomTableMigrator constructor. + */ + public function __construct() { + $this->schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() ); + $this->meta_column_mapping = $this->get_meta_column_config(); + $this->core_column_mapping = $this->get_core_column_mapping(); + $this->errors = array(); + } + + /** + * Specify schema config the source and destination table. * - * @param array $schema_config This parameters provides general but essential information about tables under migrations. Must be of the form- + * @return array Schema, must of the form: * array( - * 'entity_schema' => - * array ( - * 'primary_id' => 'primary_id column name of source table', - * 'table_name' => 'name of the source table'. - * ), - * 'entity_meta_schema' => - * array ( - * 'meta_key_column' => 'name of meta_key column in source meta table', - * 'meta_value_column' => 'name of meta_value column in source meta table', - * 'table_name' => 'name of source meta table', - * ), - * 'destination_table' => 'name of destination custom table', - * 'entity_meta_relation' => - * array ( - * 'entity' => 'name of column in source table which is used in source meta table', - * 'meta' => 'name of column in source meta table which contains key of records in source table', - * ) - * ) - * ). + 'source' => array( + 'entity' => array( + 'table_name' => $source_table_name, + 'meta_rel_column' => $column_meta, Name of column in source table which is referenced by meta table. + 'destination_rel_column' => $column_dest, Name of column in source table which is refenced by destination table, + 'primary_key' => $primary_key, Primary key of the source table + ), + 'meta' => array( + 'table' => $meta_table_name, + 'meta_key_column' => $meta_key_column_name, + 'meta_value_column' => $meta_value_column_name, + 'entity_id_column' => $entity_id_column, Name of the column having entity IDs. + ), + ), + 'destination' => array( + 'table_name' => $table_name, Name of destination table, + 'source_rel_column' => $column_source_id, Name of the column in destination table which is referenced by source table. + 'primary_key' => $table_primary_key, + 'primary_key_type' => $type bool|int|string|decimal + ) + */ + abstract public function get_schema_config(); + + /** + * Specify column config from the source table. * - * @param array $meta_column_mapping Mapping information of keys in source meta table. Must be of the form: + * @return array Config, must be of the form: + * array( + * '$source_column_name_1' => array( // $source_column_name_1 is column name in source table, or a select statement. + * 'type' => 'type of value, could be string/int/date/float.', + * 'destination' => 'name of the column in column name where this data should be inserted in.', + * ), + * '$source_column_name_2' => array( + * ...... + * ), + * .... + * ). + */ + abstract public function get_core_column_mapping(); + + /** + * Specify meta keys config from source meta table. + * + * @return array Config, must be of the form. * array( * '$meta_key_1' => array( // $meta_key_1 is the name of meta_key in source meta table. * 'type' => 'type of value, could be string/int/date/float', @@ -73,42 +111,16 @@ abstract class MetaToCustomTableMigrator { * ), * .... * ). - * - * @param array $core_column_mapping Mapping of keys in source table, similar to meta_column_mapping param, must be of the form: - * array( - * '$source_column_name_1' => array( // $source_column_name_1 is column name in source table. - * 'type' => 'type of value, could be string/int/date/float.', - * 'destination' => 'name of the column in column name where this data should be inserted in.', - * ), - * '$source_column_name_2' => array( - * ...... - * ), - * .... - * ). */ - public function __construct() { - // TODO: Add code to validate params. - $this->schema_config = MigrationHelper::escape_schema_for_backtick( $this->get_schema_config() ); - $this->meta_column_mapping = $this->get_meta_column_config(); - $this->core_column_mapping = $this->get_core_column_mapping(); - $this->errors = array(); - } - - abstract function get_schema_config(); - - abstract function get_core_column_mapping(); - - abstract function get_meta_column_config(); - + abstract public function get_meta_column_config(); /** * Generate SQL for data insertion. * - * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration()` method. - * @param string $insert_switch Insert command to use in generating queries, could be insert, insert_ignore, or replace. + * @param array $batch Data to generate queries for. Will be 'data' array returned by `$this->fetch_data_for_migration_for_ids()` method. * * @return string Generated queries for insertion for this batch, would be of the form: - * INSERT/INSERT IGNORE/REPLACE INTO $table_name ($columns) values + * INSERT IGNORE INTO $table_name ($columns) values * ($value for row 1) * ($value for row 2) * ... @@ -118,10 +130,26 @@ abstract class MetaToCustomTableMigrator { list( $value_sql, $column_sql ) = $this->generate_column_clauses( array_merge( $this->core_column_mapping, $this->meta_column_mapping ), $batch ); - return "INSERT IGNORE INTO $table (`$column_sql`) VALUES $value_sql;"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, -- $insert_query is hardcoded, $value_sql is already escaped. } + /** + * Generate SQL for data updating. + * + * @param array $batch Data to generate queries for. Will be `data` array returned by fetch_data_for_migration_for_ids() method. + * + * @param array $entity_row_mapping Maps rows to update data with their original IDs. Will be returned by `generate_update_sql_for_batch`. + * + * @return string Generated queries for batch update. Would be of the form: + * INSERT INTO $table ( $columns ) VALUES + * ($value for row 1) + * ($valye for row 2) + * ... + * ON DUPLICATE KEY UPDATE + * $column1 = VALUES($column1) + * $column2 = VALUES($column2) + * ... + */ public function generate_update_sql_for_batch( $batch, $entity_row_mapping ) { $table = $this->schema_config['destination']['table_name']; @@ -140,6 +168,11 @@ abstract class MetaToCustomTableMigrator { return "INSERT INTO $table (`$column_sql`) VALUES $value_sql $duplicate_update_key_statement;"; } + /** + * Generate schema for primary ID column of destination table. + * + * @return array[] Schema for primary ID column. + */ protected function get_destination_table_primary_id_schema() { return array( 'destination_primary_key' => array( @@ -149,6 +182,14 @@ abstract class MetaToCustomTableMigrator { ); } + /** + * Generate values and columns clauses to be used in INSERT and INSERT..ON DUPLICATE KEY UPDATE statements. + * + * @param array $columns_schema Columns config for destination table. + * @param array $batch Actual data to migrate as returned by `data` in `fetch_data_for_migration_for_ids` method. + * + * @return array SQL clause for values, columns placeholders, and columns. + */ protected function generate_column_clauses( $columns_schema, $batch ) { global $wpdb; @@ -178,8 +219,15 @@ abstract class MetaToCustomTableMigrator { return array( $value_sql, $column_sql, $columns ); } + /** + * Generates ON DUPLICATE KEY UPDATE clause to be used in migration. + * + * @param array $columns List of column names. + * + * @return string SQL clause for INSERT...ON DUPLICATE KEY UPDATE + */ private function generate_on_duplicate_statement_clause( $columns ) { - $update_value_statements = []; + $update_value_statements = array(); foreach ( $columns as $column ) { $update_value_statements[] = "$column = VALUES( $column )"; } @@ -191,15 +239,15 @@ abstract class MetaToCustomTableMigrator { /** * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. * - * @param int $batch_size Batch size of records to migrate. + * @param array $entity_ids List of entity IDs to perform migrations for. * - * @return array True if migration is completed, false if there are still records to process. + * @return array List of errors happened during migration. */ public function process_migration_batch_for_ids( $entity_ids ) { $data = $this->fetch_data_for_migration_for_ids( $entity_ids ); foreach ( $data['errors'] as $entity_id => $error ) { - $this->errors[ $entity_id ] = "Error in importing post id $entity_id: " . print_r( $error, true ); + $this->errors[ $entity_id ] = "Error in importing post id $entity_id: " . $error->get_message(); } if ( count( $data['data'] ) === 0 ) { @@ -216,32 +264,45 @@ abstract class MetaToCustomTableMigrator { $this->process_update_batch( $to_update, $already_migrated ); return array( - 'errors' => $this->errors + 'errors' => $this->errors, ); } + /** + * Process batch for insertion into destination table. + * + * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`. + */ protected function process_insert_batch( $batch ) { global $wpdb; if ( 0 === count( $batch ) ) { return; } $queries = $this->generate_insert_sql_for_batch( $batch ); - $result = $wpdb->query( $queries ); - $wpdb->query( "COMMIT;" ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared. + $result = $wpdb->query( $queries ); + $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $batch ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not inserted. } } + /** + * Process batch for update into destination table. + * + * @param array $batch Data to insert, will be of the form as returned by `data` in `fetch_data_for_migration_for_ids`. + * @param array $already_migrated Maps rows to update data with their original IDs. + */ protected function process_update_batch( $batch, $already_migrated ) { global $wpdb; if ( 0 === count( $batch ) ) { return; } $queries = $this->generate_update_sql_for_batch( $batch, $already_migrated ); - $result = $wpdb->query( $queries ); - $wpdb->query( "COMMIT;" ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Queries should already be prepared. + $result = $wpdb->query( $queries ); + $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $batch ) !== $result ) { // Some rows were not inserted. // TODO: Find and log the entity ids that were not updateed. @@ -252,9 +313,7 @@ abstract class MetaToCustomTableMigrator { /** * Fetch data for migration. * - * @param string $where_clause Where conditions to use while selecting data from source table. - * @param string $batch_size Batch size, will be used in LIMIT clause. - * @param string $order_by Will be used in ORDER BY clause. + * @param array $entity_ids Entity IDs to fetch data for. * * @return array[] Data along with errors (if any), will of the form: * array( @@ -295,6 +354,20 @@ abstract class MetaToCustomTableMigrator { return $this->process_and_sanitize_data( $entity_data, $meta_data ); } + /** + * Fetch id mappings for records that are already inserted, or can be considered duplicates. + * + * @param array $entity_ids List of entity IDs to verify. + * + * @return array Already migrated entities, would be of the form + * array( + * '$source_id1' => array( + * 'source_id' => $source_id1, + * 'destination_id' => $destination_id1, + * ), + * ... + * ) + */ public function get_already_migrated_records( $entity_ids ) { global $wpdb; $source_table = $this->schema_config['source']['entity']['table_name']; @@ -309,6 +382,7 @@ abstract class MetaToCustomTableMigrator { $already_migrated_entity_ids = $wpdb->get_results( $wpdb->prepare( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded. " SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id FROM `$destination_table` destination @@ -317,6 +391,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) ", $entity_ids ) + // phpcs:enable ); return array_column( $already_migrated_entity_ids, null, 'source_id' ); @@ -326,9 +401,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) /** * Helper method to build query used to fetch data from core source table. * - * @param string $where_clause Where conditions to use while selecting data from source table. - * @param string $batch_size Batch size, will be used in LIMIT clause. - * @param string $order_by Will be used in ORDER BY clause. + * @param array $entity_ids List of entity IDs to fetch. * * @return string Query that can be used to fetch data. */ @@ -349,7 +422,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) } } $entity_column_string = implode( ', ', $entity_keys ); - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped. + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_rel_id_column, $source_destination_rel_id_column etc is escaped for backticks. $where clause and $order_by should already be escaped. $query = $wpdb->prepare( " SELECT @@ -371,7 +444,7 @@ WHERE $where_clause; * * @param array $entity_ids List of IDs to fetch metadata for. * - * @return string|void Query for fetching meta data. + * @return string Query for fetching meta data. */ protected function build_meta_data_query( $entity_ids ) { global $wpdb; @@ -384,7 +457,7 @@ WHERE $where_clause; $meta_column_string = implode( ', ', array_fill( 0, count( $meta_keys ), '%s' ) ); $entity_id_string = implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ); - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders. + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $meta_table_relational_key, $meta_key_column, $meta_value_column and $meta_table is escaped for backticks. $entity_id_string and $meta_column_string are placeholders. $query = $wpdb->prepare( " SELECT `$meta_table_relational_key` as entity_id, `$meta_key_column` as meta_key, `$meta_value_column` as meta_value @@ -475,7 +548,7 @@ WHERE /** * Validate and transform data so that we catch as many errors as possible before inserting. * - * @param mixed $value Actual data value. + * @param mixed $value Actual data value. * @param string $type Type of data, could be decimal, int, date, string. * * @return float|int|mixed|string|\WP_Error From a220e4f2ea77a217992189a4ba78112a405eebce Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 19:46:33 +0530 Subject: [PATCH 130/206] Change parent class reference to try to fix CI. --- .../Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index c2ae02a084e..1832cb464fd 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -14,7 +14,7 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ShippingHelper; /** * Class WPPostToCOTMigratorTest. */ -class WPPostToCOTMigratorTest extends \WC_Unit_Test_Case { +class WPPostToCOTMigratorTest extends WC_Unit_Test_Case { /** * @var DataSynchronizer From 2270bfbe9d219ffe981cab8df201b1b391107b8c Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 20:04:24 +0530 Subject: [PATCH 131/206] Applied coding standards + minor fixups. --- .../CustomOrderTable/WPPostToCOTMigrator.php | 164 +++--------------- 1 file changed, 28 insertions(+), 136 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index d03b5e5cdfa..a21e0126eae 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -6,9 +6,6 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; use Automattic\WooCommerce\DataBase\Migrations\MigrationErrorLogger; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderAddressTableMigrator; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderOpTableMigrator; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToOrderTableMigrator; /** * Class WPPostToCOTMigrator @@ -27,28 +24,28 @@ class WPPostToCOTMigrator { /** * Migrator instance to migrate data into wc_order table. * - * @var MetaToCustomTableMigrator + * @var WPPostToOrderTableMigrator */ private $order_table_migrator; /** * Migrator instance to migrate billing data into address table. * - * @var MetaToCustomTableMigrator + * @var WPPostToOrderAddressTableMigrator */ private $billing_address_table_migrator; /** * Migrator instance to migrate shipping data into address table. * - * @var MetaToCustomTableMigrator + * @var WPPostToOrderAddressTableMigrator */ private $shipping_address_table_migrator; /** * Migrator instance to migrate operational data. * - * @var MetaToCustomTableMigrator + * @var WPPostToOrderOpTableMigrator */ private $operation_data_table_migrator; @@ -123,66 +120,6 @@ class WPPostToCOTMigrator { ); } - /** - * @param MetaToCustomTableMigrator $migrator - * @param $type - * @param $order_post_ids - */ - private function process_next_address_batch( $migrator, $type, $order_post_ids ) { - global $wpdb; - - if ( 0 === count( $order_post_ids ) ) { - return; - } - - $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); - - foreach ( $data['errors'] as $order_id => $error ) { - // TODO: Add name of the migrator in error message. - $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); - } - - $to_insert = isset( $data['data']['insert'] ) ? $data['data']['insert'] : array(); - $to_update = isset( $data['data']['update'] ) ? $data['data']['update'] : array(); - - if ( 0 === count( $to_insert ) && 0 === count( $to_update ) ) { - return; - } - - $order_post_ids_placeholders = implode( ',', array_fill( 0, count( $order_post_ids ), '%d' ) ); - - $already_migrated = $wpdb->get_results( - $wpdb->prepare( - " -SELECT addresses.id, addresses.order_id, orders.post_id FROM {$this->table_names['addresses']} as addresses -JOIN {$this->table_names['orders']} orders ON addresses.order_id = orders.id -WHERE - address_type = %s AND - orders.post_id IN ( $order_post_ids_placeholders ) - ", - $type, - $order_post_ids - ) - ); - - $order_post_ids_to_migrate = array_diff( $order_post_ids, array_column( $already_migrated, 'post_id' ) ); - $to_insert = array_intersect_key( $to_insert, array_flip( $order_post_ids_to_migrate ) ); - $this->execute_action_for_batch( $migrator, $to_insert, 'insert' ); - - if ( 0 < count( $to_update ) ) { - echo 'comes here'; - } - $to_update_intersect = array(); - $ids_to_update = array_flip( array_column( $already_migrated, 'id' ) ); - foreach ( $to_update as $post_id => $address_record ) { - if ( isset( $ids_to_update[ $address_record['id'] ] ) ) { - $to_update_intersect[ $post_id ] = $address_record; - } - } - - $this->execute_action_for_batch( $migrator, $to_update_intersect, 'update' ); - } - /** * Process next migration batch, uses option `wc_cot_migration` to checkpoints of what have been processed so far. * @@ -195,38 +132,24 @@ WHERE if ( 0 === count( $order_post_ids ) ) { return true; } + $this->process_migration_for_ids( $order_post_ids ); + $last_post_migrated = max( $order_post_ids ); + $this->update_checkpoint( $last_post_migrated ); + } + /** + * Process migration for specific order post IDs. + * + * @param array $order_post_ids List of post IDs to migrate. + */ + public function process_migration_for_ids( $order_post_ids ) { $this->order_table_migrator->process_migration_batch_for_ids( $order_post_ids ); $this->billing_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); $this->shipping_address_table_migrator->process_migration_batch_for_ids( $order_post_ids ); $this->operation_data_table_migrator->process_migration_batch_for_ids( $order_post_ids ); - - $last_post_migrated = max( $order_post_ids ); - $this->update_checkpoint( $last_post_migrated ); - - return false; - } - - /** - * Process next batch for a given address type. - * - * @param MetaToCustomTableMigrator $migrator Migrator instance for address type. - * @param array $order_post_ids Array of post IDs for orders. - * @param string $order_by Order by clause. - */ - private function process_next_migrator_batch( $migrator, $order_post_ids ) { - $data = $migrator->fetch_data_for_migration_for_ids( $order_post_ids ); - - foreach ( $data['errors'] as $order_id => $error ) { - // TODO: Add name of the migrator in error message. - $this->error_logger->log( 'info', "Error in importing data for Order ID $order_id: " . print_r( $error, true ) ); - } - - $to_replace = isset( $data['data']['update'] ) ? $data['data']['update'] : array(); - $this->execute_action_for_batch( $migrator, $to_replace, 'update' ); - - $to_insert = isset( $data['data']['insert'] ) ? $data['data']['insert'] : array(); - $this->execute_action_for_batch( $migrator, $to_insert, 'insert' ); + // TODO: Add resilience for meta migrations. + // $this->process_meta_migration( $order_post_ids ); + // TODO: Return merged error array. } /** @@ -250,34 +173,18 @@ WHERE * Method to migrate single record. * * @param int $post_id Post ID of record to migrate. - * - * @return bool|\WP_Error */ public function process_single( $post_id ) { - global $wpdb; - - $where_clause = $wpdb->prepare( 'ID = %d', $post_id ); - $data = $this->order_table_migrator->fetch_data_for_migration_for_ids( $where_clause, 1, 'ID ASC' ); - if ( isset( $data['errors'][ $post_id ] ) ) { - return new \WP_Error( $data['errors'][ $post_id ] ); - } - - $queries = $this->order_table_migrator->generate_insert_sql_for_batch( $data['data'], 'replace' ); - $result = $wpdb->query( $queries ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $queries is already prepared. - if ( 1 !== $result ) { - // TODO: Fetch and return last error. - echo 'error'; - - return new \WP_Error( 'error' ); - } - - return true; + $this->process_migration_for_ids( array( $post_id ) ); + // TODO: Return error. } /** * Helper function to get where clause to send to MetaToCustomTableMigrator instance. * - * @return array Where clause. + * @param int $batch_size Number of orders in batch. + * + * @return array List of IDs in the current patch. */ private function get_next_batch_ids( $batch_size ) { global $wpdb; @@ -285,7 +192,7 @@ WHERE $checkpoint = $this->get_checkpoint(); $post_ids = $wpdb->get_col( $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = '%s' LIMIT %d", + "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = %s LIMIT %d", $checkpoint['id'], 'shop_order', $batch_size @@ -295,26 +202,6 @@ WHERE return $post_ids; } - /** - * Helper method to create `ID in (.., .., ...)` clauses. - * - * @param array $ids List of IDs. - * @param string $column_name Name of the ID column. - * - * @return string Prepared clause for where. - */ - private function get_where_id_clause( $ids, $column_name = 'ID' ) { - global $wpdb; - - if ( 0 === count( $ids ) ) { - return ''; - } - - $id_placeholder_array = '(' . implode( ',', array_fill( 0, count( $ids ), '%d' ) ) . ')'; - - return $wpdb->prepare( "`$column_name` IN $id_placeholder_array", $ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Both $column_name and $id_placeholder_array should already be prepared. - } - /** * Current checkpoint status. * @@ -333,6 +220,11 @@ WHERE return update_option( 'wc_cot_migration', array( 'id' => $id ), false ); } + /** + * Remove checkpoint. + * + * @return bool Whether checkpoint was removed. + */ public function delete_checkpoint() { return delete_option( 'wp_cot_migration' ); } From 5b3e9f78d24dd97bc28bd720942e1919d56fcb2e Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 20:10:15 +0530 Subject: [PATCH 132/206] Applied coding standards. --- .../WPPostToOrderTableMigrator.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php index b0b7a7ec840..e611ede6b2c 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php @@ -10,6 +10,11 @@ namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; */ class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator { + /** + * Get schema config for wp_posts and wc_order table. + * + * @return array Config. + */ public function get_schema_config() { global $wpdb; @@ -45,6 +50,11 @@ class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator { ); } + /** + * Get columns config. + * + * @return \string[][] Config. + */ public function get_core_column_mapping() { return array( 'ID' => array( @@ -70,6 +80,11 @@ class WPPostToOrderTableMigrator extends MetaToCustomTableMigrator { ); } + /** + * Get meta data config. + * + * @return \string[][] Config. + */ public function get_meta_column_config() { return array( '_order_currency' => array( From f8d5746a2d6bb2687440a296e8847392372e1be8 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 20:16:18 +0530 Subject: [PATCH 133/206] Applied coding standards --- .../WPPostToOrderAddressTableMigrator.php | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php index 55c7fb438bc..83d41d9db6c 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php @@ -1,17 +1,39 @@ type = $type; parent::__construct(); } - function get_schema_config() { + /** + * Get schema config for wp_posts and wc_order_address table. + * + * @return array Config. + */ + public function get_schema_config() { global $wpdb; // TODO: Remove hardcoding. $this->table_names = array( @@ -45,8 +67,14 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { ); } - function get_core_column_mapping() { + /** + * Get columns config. + * + * @return \string[][] Config. + */ + public function get_core_column_mapping() { $type = $this->type; + return array( 'id' => array( 'type' => 'int', @@ -60,8 +88,14 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { ); } - function get_meta_column_config() { + /** + * Get meta data config. + * + * @return \string[][] Config. + */ + public function get_meta_column_config() { $type = $this->type; + return array( "_{$type}_first_name" => array( 'type' => 'string', @@ -110,6 +144,20 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { ); } + /** + * We overwrite this method to add a subclause to only fetch address of current type. + * + * @param array $entity_ids List of entity IDs to verify. + * + * @return array Already migrated entities, would be of the form + * array( + * '$source_id1' => array( + * 'source_id' => $source_id1, + * 'destination_id' => $destination_id1, + * ), + * ... + * ) + */ public function get_already_migrated_records( $entity_ids ) { global $wpdb; $source_table = $this->schema_config['source']['entity']['table_name']; @@ -126,6 +174,7 @@ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { $already_migrated_entity_ids = $wpdb->get_results( $wpdb->prepare( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded. " SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id FROM `$destination_table` destination @@ -134,6 +183,7 @@ WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) AND dest ", $entity_ids ) + // phpcs:enable ); return array_column( $already_migrated_entity_ids, null, 'source_id' ); From 8e4287d19bbccd97520e5fb45d77c263966086b3 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Thu, 31 Mar 2022 20:19:26 +0530 Subject: [PATCH 134/206] Applied coding standards. --- .../MetaToMetaTableMigrator.php | 6 ++--- .../WPPostToOrderOpTableMigrator.php | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index cbe9b78d7b1..aa3273c2ceb 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -47,9 +47,9 @@ class MetaToMetaTableMigrator { $insert_query = MigrationHelper::get_insert_switch( $insert_switch ); - $meta_key_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['meta_key_column'] ); - $meta_value_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['meta_value_column'] ); - $entity_id_column = MigrationHelper::escape_backtick( $this->schema_config['destination']['meta']['entity_id_column'] ); + $meta_key_column = $this->schema_config['destination']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['destination']['meta']['meta_value_column']; + $entity_id_column = $this->schema_config['destination']['meta']['entity_id_column']; $column_sql = "(`$entity_id_column`, `$meta_key_column`, `$meta_value_column`)"; $table = $this->schema_config['destination']['meta']['table_name']; diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php index 6c7645a6b31..8147542fc5d 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php @@ -1,9 +1,22 @@ array( @@ -47,8 +66,14 @@ class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator { ); } + + /** + * Get meta data config. + * + * @return \string[][] Config. + */ public function get_meta_column_config() { - return array( + return array( '_created_via' => array( 'type' => 'string', 'destination' => 'created_via', From d7434ce3a747957080793970c70ae6308fe304e5 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 1 Apr 2022 17:16:15 +0530 Subject: [PATCH 135/206] Force type to try to fix CI. --- .../CustomOrderTable/WPPostToCOTMigratorTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 1832cb464fd..45a074a7789 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -6,7 +6,6 @@ use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToCOTMigrator; use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; -use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper; use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper; use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper; use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ShippingHelper; @@ -287,9 +286,9 @@ WHERE order_id = {$order_id} ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) ); $this->assertEquals( $order->get_shipping_tax(), $db_order_op_data->shipping_tax_amount ); - $this->assertEquals( $order->get_shipping_total(), $db_order_op_data->shipping_total_amount ); - $this->assertEquals( $order->get_discount_tax(), $db_order_op_data->discount_tax_amount ); - $this->assertEquals( $order->get_discount_total(), $db_order_op_data->discount_total_amount ); + $this->assertEquals( (float) $order->get_shipping_total(), (float) $db_order_op_data->shipping_total_amount ); + $this->assertEquals( (float) $order->get_discount_tax(), (float) $db_order_op_data->discount_tax_amount ); + $this->assertEquals( (float) $order->get_discount_total(), (float) $db_order_op_data->discount_total_amount ); } private function clear_all_orders_and_reset_checkpoint() { From b403fa0cf088b9b39a6e54051aa157ece8e9604c Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 1 Apr 2022 17:16:39 +0530 Subject: [PATCH 136/206] Remove incorrectly added file. --- plugins/woocommerce.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 plugins/woocommerce.php diff --git a/plugins/woocommerce.php b/plugins/woocommerce.php deleted file mode 100644 index e69de29bb2d..00000000000 From 81f918af877741054d400924cb7f3ba09e91d47e Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 1 Apr 2022 17:46:27 +0530 Subject: [PATCH 137/206] Force type to try to fix CI. --- .../Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 45a074a7789..777929b0a62 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -285,7 +285,7 @@ WHERE order_id = {$order_id} $order->get_date_completed()->date( DATE_ISO8601 ), ( new WC_DateTime( $db_order_op_data->date_completed_gmt ) )->date( DATE_ISO8601 ) ); - $this->assertEquals( $order->get_shipping_tax(), $db_order_op_data->shipping_tax_amount ); + $this->assertEquals( (float) $order->get_shipping_tax(), (float) $db_order_op_data->shipping_tax_amount ); $this->assertEquals( (float) $order->get_shipping_total(), (float) $db_order_op_data->shipping_total_amount ); $this->assertEquals( (float) $order->get_discount_tax(), (float) $db_order_op_data->discount_tax_amount ); $this->assertEquals( (float) $order->get_discount_total(), (float) $db_order_op_data->discount_total_amount ); From bb10c88b8f874f9dfde8194882b98a87b4f02532 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Fri, 1 Apr 2022 18:00:49 +0530 Subject: [PATCH 138/206] Minor fixes in bulk migration logic. --- .../Migrations/CustomOrderTable/WPPostToCOTMigrator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index a21e0126eae..a9ddb9b313b 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -135,6 +135,7 @@ class WPPostToCOTMigrator { $this->process_migration_for_ids( $order_post_ids ); $last_post_migrated = max( $order_post_ids ); $this->update_checkpoint( $last_post_migrated ); + return false; } /** @@ -192,7 +193,7 @@ class WPPostToCOTMigrator { $checkpoint = $this->get_checkpoint(); $post_ids = $wpdb->get_col( $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = %s LIMIT %d", + "SELECT ID FROM $wpdb->posts WHERE ID > %d AND post_type = %s ORDER BY ID ASC LIMIT %d ", $checkpoint['id'], 'shop_order', $batch_size From c5df64ca573e58d5936dc4cc7fd8cc2b6b5bf3ca Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 4 Apr 2022 13:43:19 +0530 Subject: [PATCH 139/206] Add errors to log. --- .../CustomOrderTable/MetaToCustomTableMigrator.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index 7347781a3fe..d7b0b4a20f4 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -283,8 +283,7 @@ abstract class MetaToCustomTableMigrator { $result = $wpdb->query( $queries ); $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $batch ) !== $result ) { - // Some rows were not inserted. - // TODO: Find and log the entity ids that were not inserted. + $this->errors[] = 'Error with batch: ' . $wpdb->last_error; } } @@ -304,8 +303,7 @@ abstract class MetaToCustomTableMigrator { $result = $wpdb->query( $queries ); $wpdb->query( 'COMMIT;' ); // For some reason, this seems necessary on some hosts? Maybe a MySQL configuration? if ( count( $batch ) !== $result ) { - // Some rows were not inserted. - // TODO: Find and log the entity ids that were not updateed. + $this->errors[] = 'Error with batch: ' . $wpdb->last_error; } } @@ -486,11 +484,6 @@ WHERE * @return array[] Validated and combined data with errors. */ private function process_and_sanitize_data( $entity_data, $meta_data ) { - /** - * TODO: Add more validations for: - * 1. Column size - * 2. Value limits - */ $sanitized_entity_data = array(); $error_records = array(); $this->process_and_sanitize_entity_data( $sanitized_entity_data, $error_records, $entity_data ); @@ -565,7 +558,6 @@ WHERE $value = wc_string_to_bool( $value ); break; case 'date': - // TODO: Test this validation in unit tests. try { if ( '' === $value ) { $value = null; From 909a57e78476b2299007f24a60c01f3364c92761 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 4 Apr 2022 13:47:48 +0530 Subject: [PATCH 140/206] Applied coding standards. --- .../src/Database/Migrations/MigrationHelper.php | 9 ++++++++- .../COTMigrationServiceProvider.php | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php index d0af0007a4f..40ce05cc678 100644 --- a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php +++ b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php @@ -45,7 +45,7 @@ class MigrationHelper { break; case 'update': $insert_query = 'UPDATE'; - break; + break; case 'insert': default: $insert_query = 'INSERT'; @@ -54,6 +54,13 @@ class MigrationHelper { return $insert_query; } + /** + * Helper method to escape backtick in various schema fields. + * + * @param array $schema_config Schema config. + * + * @return array Schema config escaped for backtick. + */ public static function escape_schema_for_backtick( $schema_config ) { array_walk( $schema_config['source']['entity'], array( self::class, 'escape_and_add_backtick' ) ); array_walk( $schema_config['source']['meta'], array( self::class, 'escape_and_add_backtick' ) ); diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php index cbe4221f3fb..d084b8f4ef0 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php @@ -1,6 +1,6 @@ Date: Mon, 4 Apr 2022 13:58:09 +0530 Subject: [PATCH 141/206] Applied coding standards. --- .../WPPostToCOTMigratorTest.php | 80 +++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 777929b0a62..113f7f25354 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -30,6 +30,9 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case { */ private $data_store; + /** + * Setup data_store and sut. + */ public function setUp(): void { parent::setUp(); $this->create_order_custom_table_if_not_exist(); @@ -37,6 +40,9 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case { $this->sut = wc_get_container()->get( WPPostToCOTMigrator::class ); } + /** + * Test that migration for a normal order happens as expected. + */ public function test_process_next_migration_batch_normal_order() { $order = wc_get_order( $this->create_complex_wp_post_order() ); $this->clear_all_orders_and_reset_checkpoint(); @@ -47,6 +53,9 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case { $this->assert_order_op_data_is_migrated( $order ); } + /** + * Test that already migrated order isn't migrated twice. + */ public function test_process_next_migration_batch_already_migrated_order() { global $wpdb; $order = wc_get_order( $this->create_complex_wp_post_order() ); @@ -59,6 +68,7 @@ class WPPostToCOTMigratorTest extends WC_Unit_Test_Case { $this->sut->update_checkpoint( 0 ); $this->sut->process_next_migration_batch( 100 ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $this->assertEquals( 1, $wpdb->get_var( @@ -96,52 +106,88 @@ WHERE order_id = {$order_id} " ) ); + // phpcs:enable } + /** + * Test that when an order is partially migrated, it can still be resumed as expected. + */ public function test_process_next_migration_batch_interrupted_migrating_order() { $this->markTestSkipped(); } + /** + * Test that invalid order data is not migrated but logged. + */ public function test_process_next_migration_batch_invalid_order_data() { $this->markTestSkipped(); } + /** + * Test when one order is invalid but other one is valid in a migration batch. + */ public function test_process_next_migration_batch_invalid_valid_order_combo() { $this->markTestSkipped(); } - public function test_process_next_migration_batch_stale_order() { - $this->markTestSkipped(); - } - + /** + * Helper method to get order object from COT. + * + * @param WP_Post $post_order Post object for order. + * + * @return array|object|void|null DB object from COT. + */ private function get_order_from_cot( $post_order ) { global $wpdb; $order_table = $this->data_store::get_orders_table_name(); $query = "SELECT * FROM $order_table WHERE post_id = {$post_order->get_id()};"; + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared return $wpdb->get_row( $query ); } + /** + * Helper method to get address details from DB. + * + * @param int $order_id Order ID. + * @param string $address_type Address Type. + * + * @return array|object|void|null DB object. + */ private function get_address_details_from_cot( $order_id, $address_type ) { global $wpdb; $address_table = $this->data_store::get_addresses_table_name(); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_row( "SELECT * FROM $address_table WHERE order_id = $order_id AND address_type = '$address_type';" ); } + /** + * Helper method to get operational details from COT. + * + * @param int $order_id Order ID. + * + * @return array|object|void|null DB Object. + */ private function get_order_operational_data_from_cot( $order_id ) { global $wpdb; $operational_data_table = $this->data_store::get_operational_data_table_name(); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->get_row( "SELECT * FROM $operational_data_table WHERE order_id = $order_id;" ); } + /** + * Helper method to create complex wp_post based order. + * + * @return int Order ID + */ private function create_complex_wp_post_order() { update_option( 'woocommerce_prices_include_tax', 'yes' ); update_option( 'woocommerce_calc_taxes', 'yes' ); $uniq_cust_id = wp_generate_password( 10, false ); - $customer = CustomerHelper::create_customer( "user$uniq_cust_id", $uniq_cust_id, "user$uniq_cust_id@example.com" ); - $tax_rate = array( + $customer = CustomerHelper::create_customer( "user$uniq_cust_id", $uniq_cust_id, "user$uniq_cust_id@example.com" ); + $tax_rate = array( 'tax_rate_country' => '', 'tax_rate_state' => '', 'tax_rate' => '15.0000', @@ -204,6 +250,11 @@ WHERE order_id = {$order_id} return $order->get_id(); } + /** + * Helper method to assert core data is migrated. + * + * @param WC_Order $order Order object. + */ private function assert_core_data_is_migrated( $order ) { $db_order = $this->get_order_from_cot( $order ); @@ -226,6 +277,11 @@ WHERE order_id = {$order_id} $this->assertEquals( $order->get_customer_user_agent(), $db_order->user_agent ); } + /** + * Helper method to assert addresses are migrated. + * + * @param WC_Order $order Order object. + */ private function assert_order_addresses_are_migrated( $order ) { $db_order = $this->get_order_from_cot( $order ); @@ -255,6 +311,11 @@ WHERE order_id = {$order_id} $this->assertEquals( $order->get_shipping_phone(), $db_order_address->phone ); } + /** + * Helper method to assert operational data is migrated. + * + * @param WC_Order $order Order object. + */ private function assert_order_op_data_is_migrated( $order ) { $db_order = $this->get_order_from_cot( $order ); // Verify order operational data. @@ -291,15 +352,22 @@ WHERE order_id = {$order_id} $this->assertEquals( (float) $order->get_discount_total(), (float) $db_order_op_data->discount_total_amount ); } + /** + * Helper method to clear checkout and truncate order tables. + */ private function clear_all_orders_and_reset_checkpoint() { global $wpdb; $order_tables = $this->data_store->get_all_table_names(); foreach ( $order_tables as $table ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "TRUNCATE table $table;" ); } $this->sut->delete_checkpoint(); } + /** + * Helper method to create custom tables if not present. + */ private function create_order_custom_table_if_not_exist() { $order_table_controller = wc_get_container() ->get( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ); From 891a9439a1c77d4d7879180f3ac93fece19eb439 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 4 Apr 2022 15:43:31 +0530 Subject: [PATCH 142/206] Applied coding standards. --- .../MetaToMetaTableMigrator.php | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index aa3273c2ceb..c4cb68f6bc2 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -75,7 +75,7 @@ class MetaToMetaTableMigrator { /** * Fetch data for migration. * - * @param string $where_clause Where conditions to use while selecting data from source table. + * @param array $order_post_ids Array of IDs to fetch data for. * * @return array[] Data along with errors (if any), will of the form: * array( @@ -92,7 +92,7 @@ class MetaToMetaTableMigrator { global $wpdb; if ( empty( $order_post_ids ) ) { return array( - 'data' => array(), + 'data' => array(), 'errors' => array(), ); } @@ -108,25 +108,28 @@ class MetaToMetaTableMigrator { ); } - return array( 'data' => $meta_data_rows, 'errors' => array() ); + return array( + 'data' => $meta_data_rows, + 'errors' => array(), + ); } /** * Helper method to build query used to fetch data from source meta table. * - * @param string $where_clause Where conditions to use while selecting data from source table. + * @param string $entity_ids List of entity IDs to build meta query for. * * @return string Query that can be used to fetch data. */ private function build_meta_table_query( $entity_ids ) { global $wpdb; - $source_meta_table = $this->schema_config['source']['meta']['table_name']; + $source_meta_table = $this->schema_config['source']['meta']['table_name']; $source_meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; $source_meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; $source_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; $order_by = "$source_entity_id_column ASC"; - $where_clause = "$source_entity_id_column IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d') ) . ')'; + $where_clause = "$source_entity_id_column IN (" . implode( ', ', array_fill( 0, count( $entity_ids ), '%d' ) ) . ')'; $destination_entity_table = $this->schema_config['destination']['entity']['table_name']; $destination_entity_id_column = $this->schema_config['destination']['entity']['id_column']; @@ -134,11 +137,12 @@ class MetaToMetaTableMigrator { if ( $this->schema_config['source']['excluded_keys'] ) { $key_placeholder = implode( ',', array_fill( 0, count( $this->schema_config['source']['excluded_keys'] ), '%s' ) ); - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. - $exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); - $where_clause = "$where_clause AND $exclude_clause"; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $source_meta_key_column is escated for backticks, $key_placeholder is hardcoded. + $exclude_clause = $wpdb->prepare( "source.$source_meta_key_column NOT IN ( $key_placeholder )", $this->schema_config['source']['excluded_keys'] ); + $where_clause = "$where_clause AND $exclude_clause"; } + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->prepare( " SELECT @@ -152,5 +156,6 @@ WHERE $where_clause ORDER BY $order_by ", $entity_ids ); + // phpcs:enable } } From b14b34abf829996609093d7e6b35b44bc8beffed Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 11 Apr 2022 12:18:55 +0530 Subject: [PATCH 143/206] Typos and DataBase -> Database. --- .../CustomOrderTable/MetaToCustomTableMigrator.php | 6 +++--- .../CustomOrderTable/MetaToMetaTableMigrator.php | 6 +++--- .../Migrations/CustomOrderTable/WPPostToCOTMigrator.php | 8 ++++---- .../WPPostToOrderAddressTableMigrator.php | 4 ++-- .../CustomOrderTable/WPPostToOrderOpTableMigrator.php | 4 ++-- .../CustomOrderTable/WPPostToOrderTableMigrator.php | 2 +- .../src/Database/Migrations/MigrationErrorLogger.php | 4 ++-- .../src/Database/Migrations/MigrationHelper.php | 6 ++---- .../ServiceProviders/COTMigrationServiceProvider.php | 2 +- .../CustomOrderTable/WPPostToCOTMigratorTest.php | 2 +- 10 files changed, 21 insertions(+), 23 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php index d7b0b4a20f4..f69d9dd3e32 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToCustomTableMigrator.php @@ -3,14 +3,14 @@ * Generic migration class to move any entity, entity_meta table combination to custom table. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; -use Automattic\WooCommerce\DataBase\Migrations\MigrationHelper; +use Automattic\WooCommerce\Database\Migrations\MigrationHelper; /** * Class MetaToCustomTableMigrator. * - * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable */ abstract class MetaToCustomTableMigrator { diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php index c4cb68f6bc2..f043b70ffe2 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/MetaToMetaTableMigrator.php @@ -3,16 +3,16 @@ * Generic Migration class to move any meta data associated to an entity, to a different meta table associated with a custom entity table. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; -use Automattic\WooCommerce\DataBase\Migrations\MigrationHelper; +use Automattic\WooCommerce\Database\Migrations\MigrationHelper; /** * Class MetaToMetaTableMigrator. * * Generic class for powering migrations from one meta table to another table. * - * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable */ class MetaToMetaTableMigrator { diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php index a9ddb9b313b..6e94d9106e4 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigrator.php @@ -3,14 +3,14 @@ * Class for implementing migration from wp_posts and wp_postmeta to custom order tables. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; -use Automattic\WooCommerce\DataBase\Migrations\MigrationErrorLogger; +use Automattic\WooCommerce\Database\Migrations\MigrationErrorLogger; /** * Class WPPostToCOTMigrator * - * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable */ class WPPostToCOTMigrator { @@ -154,7 +154,7 @@ class WPPostToCOTMigrator { } /** - * Process migration for metadata for given post ids.\ + * Process migration for metadata for given post ids. * * @param array $order_post_ids Post IDs. */ diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php index 83d41d9db6c..bbb2be8dda2 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderAddressTableMigrator.php @@ -3,12 +3,12 @@ * Class for WPPost to wc_order_address table migrator. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; /** * Class WPPostToOrderAddressTableMigrator * - * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable */ class WPPostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { /** diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php index 8147542fc5d..54217a421c4 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderOpTableMigrator.php @@ -3,12 +3,12 @@ * Class for WPPost to wc_order_operational_details migrator. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; /** * Class WPPostToOrderOpTableMigrator * - * @package Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable + * @package Automattic\WooCommerce\Database\Migrations\CustomOrderTable */ class WPPostToOrderOpTableMigrator extends MetaToCustomTableMigrator { diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php index e611ede6b2c..e18b3876dfe 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/WPPostToOrderTableMigrator.php @@ -3,7 +3,7 @@ * Class for WPPost To order table migrator. */ -namespace Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable; +namespace Automattic\WooCommerce\Database\Migrations\CustomOrderTable; /** * Class WPPostToOrderTableMigrator. diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php index af3288beac8..191dce8963d 100644 --- a/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php +++ b/plugins/woocommerce/src/Database/Migrations/MigrationErrorLogger.php @@ -3,14 +3,14 @@ * Error logger for custom table migrations. */ -namespace Automattic\WooCommerce\DataBase\Migrations; +namespace Automattic\WooCommerce\Database\Migrations; /** * Class MigrationErrorLogger. * * Error logging for custom table migrations. * - * @package Automattic\WooCommerce\DataBase\Migrations + * @package Automattic\WooCommerce\Database\Migrations */ class MigrationErrorLogger extends \WC_Logger { diff --git a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php index 40ce05cc678..4aba17b86e5 100644 --- a/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php +++ b/plugins/woocommerce/src/Database/Migrations/MigrationHelper.php @@ -3,14 +3,12 @@ * Helper class with utility functions for migrations. */ -namespace Automattic\WooCommerce\DataBase\Migrations; - -use function DeliciousBrains\WP_Offload_Media\Aws3\JmesPath\search; +namespace Automattic\WooCommerce\Database\Migrations; /** * Class MigrationHelper. * - * Helper class to asist with migration related operations. + * Helper class to assist with migration related operations. */ class MigrationHelper { diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php index d084b8f4ef0..2312b589eb6 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/COTMigrationServiceProvider.php @@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToCOTMigrator; +use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; /** diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php index 113f7f25354..e9e9c2e1034 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/WPPostToCOTMigratorTest.php @@ -3,7 +3,7 @@ * Tests for WPPostToCOTMigrator class. */ -use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\WPPostToCOTMigrator; +use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\WPPostToCOTMigrator; use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper; From 63ae96a2b68472c1a7bbbb677304ff65187948e3 Mon Sep 17 00:00:00 2001 From: vedanshujain Date: Mon, 11 Apr 2022 19:06:33 +0530 Subject: [PATCH 144/206] Add changelog. --- plugins/woocommerce/changelog/COTMigrations | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/COTMigrations diff --git a/plugins/woocommerce/changelog/COTMigrations b/plugins/woocommerce/changelog/COTMigrations new file mode 100644 index 00000000000..1dabd6c2ca5 --- /dev/null +++ b/plugins/woocommerce/changelog/COTMigrations @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Generic migration support for migration from posts + postsmeta table to any custom table. Additionaly, implement migrations to various COT tables using this generic support. From 464bcb7783598b79f6699a8ba97add774a47ddf8 Mon Sep 17 00:00:00 2001 From: RJChow Date: Mon, 11 Apr 2022 21:31:19 +0800 Subject: [PATCH 145/206] Changed unminify plugin to hook into step before optimization --- plugins/woocommerce-admin/unminify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/unminify.js b/plugins/woocommerce-admin/unminify.js index 93b5ee9088d..760c3cae391 100644 --- a/plugins/woocommerce-admin/unminify.js +++ b/plugins/woocommerce-admin/unminify.js @@ -47,7 +47,7 @@ class UnminifyWebpackPlugin { compilation.hooks.processAssets.tap( { name: 'UnminifyWebpackPlugin', - stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_DERIVED, }, ( assets ) => { Object.entries( assets ).forEach( From 5b31f5acf433c70c4482d1251b33ae38c36e9419 Mon Sep 17 00:00:00 2001 From: moon Date: Wed, 30 Mar 2022 15:46:43 -0700 Subject: [PATCH 146/206] Do not show payment recommendations if store owner is in a country where WC Payment is supported --- .../client/payments/payment-recommendations.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx index a1bd79bb71f..1bc484fcd9c 100644 --- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx @@ -79,10 +79,20 @@ const PaymentRecommendations: React.FC = () => { [ isInstalled ] ); + const supportsWCPayments = + paymentGatewaySuggestions.filter( ( paymentGatewaySuggestion ) => { + return ( + paymentGatewaySuggestion.id.indexOf( + 'woocommerce_payments' + ) === 0 + ); + } ).length === 1; + const triggeredPageViewRef = useRef( false ); const shouldShowRecommendations = paymentGatewaySuggestions && paymentGatewaySuggestions.length > 0 && + ! supportsWCPayments && ! isDismissed; useEffect( () => { From 721ff370952e4b7a3638764c803b434045820d6a Mon Sep 17 00:00:00 2001 From: moon Date: Wed, 30 Mar 2022 21:02:52 -0700 Subject: [PATCH 147/206] Support woocommerce_payment_gateways_setting_additional_rows filter to allow adding additional table rows --- .../admin/settings/class-wc-settings-payment-gateways.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php index 6d4362ecb83..e7eca9216e6 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php @@ -124,6 +124,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { ); $columns = apply_filters( 'woocommerce_payment_gateways_setting_columns', $default_columns ); + $additional_rows = apply_filters( 'woocommerce_payment_gateways_setting_additional_rows', array(), count( $columns ) ); foreach ( $columns as $key => $column ) { echo '' . esc_html( $column ) . ''; @@ -201,6 +202,9 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { echo ''; } + foreach ( $additional_rows as $additional_row ) { + echo $additional_row; + } ?> From 08e5883246e532932d38254a09522c72ea5a8de6 Mon Sep 17 00:00:00 2001 From: moon Date: Wed, 30 Mar 2022 21:03:20 -0700 Subject: [PATCH 148/206] Add other payment methods link when the store is in WC Payments eligible country --- .../PaymentGatewaysController.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php index 6dedede0469..664ba538706 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions; +use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; use Automattic\WooCommerce\Admin\Features\TransientNotices; defined( 'ABSPATH' ) || exit; @@ -21,6 +22,7 @@ class PaymentGatewaysController { add_filter( 'woocommerce_rest_prepare_payment_gateway', array( __CLASS__, 'extend_response' ), 10, 3 ); add_filter( 'admin_init', array( __CLASS__, 'possibly_do_connection_return_action' ) ); add_action( 'woocommerce_admin_payment_gateway_connection_return', array( __CLASS__, 'handle_successfull_connection' ) ); + add_filter( 'woocommerce_payment_gateways_setting_additional_rows', array( __CLASS__, 'add_other_payment_methods_link' ), 10, 2 ); } /** @@ -145,4 +147,22 @@ class PaymentGatewaysController { wp_safe_redirect( wc_admin_url() ); } + + /** + * Add "Other payment methods" link in WooCommerce -> Settings -> Payments + * When the store is in WC Payments eligible country. + * See https://github.com/woocommerce/woocommerce/issues/32130 for more details. + * + * @return void + */ + public static function add_other_payment_methods_link( $rows, $no_of_cols ) { + if ( WooCommercePayments::is_supported() ) { + $link = 'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations'; + $link_text = __( 'Other payment methods', 'woocommerce' ); + $external_link_icon = ''; + $row = "{$link_text} {$external_link_icon}"; + $rows[] = $row; + } + return $rows; + } } From 06f66ba20c80108c52bfd1debbdb9fb015e41552 Mon Sep 17 00:00:00 2001 From: moon Date: Thu, 31 Mar 2022 14:04:05 -0700 Subject: [PATCH 149/206] Removed the filter and moved the link logic due to security reason --- .../class-wc-settings-payment-gateways.php | 25 ++++++++++++++++--- .../PaymentGatewaysController.php | 19 -------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php index e7eca9216e6..cb87b1a4500 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php @@ -5,6 +5,8 @@ * @package WooCommerce\Admin */ +use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; + defined( 'ABSPATH' ) || exit; if ( class_exists( 'WC_Settings_Payment_Gateways', false ) ) { @@ -124,7 +126,6 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { ); $columns = apply_filters( 'woocommerce_payment_gateways_setting_columns', $default_columns ); - $additional_rows = apply_filters( 'woocommerce_payment_gateways_setting_additional_rows', array(), count( $columns ) ); foreach ( $columns as $key => $column ) { echo '' . esc_html( $column ) . ''; @@ -202,8 +203,26 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { echo ''; } - foreach ( $additional_rows as $additional_row ) { - echo $additional_row; + /** + * Add "Other payment methods" link in WooCommerce -> Settings -> Payments + * When the store is in WC Payments eligible country. + * See https://github.com/woocommerce/woocommerce/issues/32130 for more details. + */ + if ( WooCommercePayments::is_supported() ) { + $columns_count = count( $columns ); + $link_text = __( 'Other payment methods', 'woocommerce' ); + $external_link_icon = ''; + echo ''; + // phpcs:ignore -- ignoring the error since the value is harded. + echo ""; + echo ""; + // phpcs:ignore + echo $link_text; + // phpcs:ignore + echo $external_link_icon; + echo ''; + echo ''; + echo ''; } ?> diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php index 664ba538706..147140e8042 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php @@ -22,7 +22,6 @@ class PaymentGatewaysController { add_filter( 'woocommerce_rest_prepare_payment_gateway', array( __CLASS__, 'extend_response' ), 10, 3 ); add_filter( 'admin_init', array( __CLASS__, 'possibly_do_connection_return_action' ) ); add_action( 'woocommerce_admin_payment_gateway_connection_return', array( __CLASS__, 'handle_successfull_connection' ) ); - add_filter( 'woocommerce_payment_gateways_setting_additional_rows', array( __CLASS__, 'add_other_payment_methods_link' ), 10, 2 ); } /** @@ -147,22 +146,4 @@ class PaymentGatewaysController { wp_safe_redirect( wc_admin_url() ); } - - /** - * Add "Other payment methods" link in WooCommerce -> Settings -> Payments - * When the store is in WC Payments eligible country. - * See https://github.com/woocommerce/woocommerce/issues/32130 for more details. - * - * @return void - */ - public static function add_other_payment_methods_link( $rows, $no_of_cols ) { - if ( WooCommercePayments::is_supported() ) { - $link = 'https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations'; - $link_text = __( 'Other payment methods', 'woocommerce' ); - $external_link_icon = ''; - $row = "{$link_text} {$external_link_icon}"; - $rows[] = $row; - } - return $rows; - } } From 51782d822847847f56fdd23229277d4493e415af Mon Sep 17 00:00:00 2001 From: moon Date: Thu, 31 Mar 2022 14:05:50 -0700 Subject: [PATCH 150/206] Remove unused namespace import --- .../PaymentGatewaySuggestions/PaymentGatewaysController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php index 147140e8042..6dedede0469 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php @@ -5,7 +5,6 @@ namespace Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions; -use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks\WooCommercePayments; use Automattic\WooCommerce\Admin\Features\TransientNotices; defined( 'ABSPATH' ) || exit; From 4fe8cf2f71949e88c2eb90e986b2528305cc46ea Mon Sep 17 00:00:00 2001 From: moon Date: Fri, 8 Apr 2022 09:32:42 -0700 Subject: [PATCH 151/206] Add id to the link --- .../admin/settings/class-wc-settings-payment-gateways.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php index cb87b1a4500..9b9e97abe89 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php @@ -215,7 +215,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page { echo ''; // phpcs:ignore -- ignoring the error since the value is harded. echo ""; - echo ""; + echo ""; // phpcs:ignore echo $link_text; // phpcs:ignore From ff2eb6f98d636f93bceb21be1995824e484831f0 Mon Sep 17 00:00:00 2001 From: moon Date: Fri, 8 Apr 2022 09:58:58 -0700 Subject: [PATCH 152/206] Record wcadmin_settings_payments_recommendations_other_options event with available payment methods --- plugins/woocommerce/legacy/js/admin/settings.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js index 29276907af6..cfe14b123d0 100644 --- a/plugins/woocommerce/legacy/js/admin/settings.js +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -185,5 +185,20 @@ } } ); + $('#settings-other-payment-methods').on('click', function( e ) { + if ( typeof window.wcTracks.recordEvent === 'undefined' ) { + return; + } + + var payment_methods = $.map( $( "td.wc_payment_gateways_wrapper tbody tr[data-gateway_id] "), function( tr ) { + return $( tr ).attr( 'data-gateway_id' ); + }); + + window.wcTracks.recordEvent( 'wcadmin_settings_payments_recommendations_other_options' , { + available_payment_methods: payment_methods + }); + + }); + }); })( jQuery, woocommerce_settings_params, wp ); From 869a3baf3cdac998674fffa7382e2933c48d33c0 Mon Sep 17 00:00:00 2001 From: moon Date: Fri, 8 Apr 2022 10:19:14 -0700 Subject: [PATCH 153/206] Remove wcadmin prefix --- plugins/woocommerce/legacy/js/admin/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js index cfe14b123d0..421debd3d19 100644 --- a/plugins/woocommerce/legacy/js/admin/settings.js +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -194,7 +194,7 @@ return $( tr ).attr( 'data-gateway_id' ); }); - window.wcTracks.recordEvent( 'wcadmin_settings_payments_recommendations_other_options' , { + window.wcTracks.recordEvent( 'settings_payments_recommendations_other_options' , { available_payment_methods: payment_methods }); From edb8b69dc4d75f73043474a9eac145c880c7f503 Mon Sep 17 00:00:00 2001 From: moon Date: Mon, 11 Apr 2022 07:22:36 -0700 Subject: [PATCH 154/206] Use wc.tracks.recordEvent if available --- .../woocommerce/legacy/js/admin/settings.js | 291 +++++++++++------- 1 file changed, 176 insertions(+), 115 deletions(-) diff --git a/plugins/woocommerce/legacy/js/admin/settings.js b/plugins/woocommerce/legacy/js/admin/settings.js index 421debd3d19..8464191f0e3 100644 --- a/plugins/woocommerce/legacy/js/admin/settings.js +++ b/plugins/woocommerce/legacy/js/admin/settings.js @@ -1,204 +1,265 @@ /* global woocommerce_settings_params, wp */ -( function( $, params, wp ) { - $( function() { +( function ( $, params, wp ) { + $( function () { // Sell Countries - $( 'select#woocommerce_allowed_countries' ).on( 'change', function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).show(); - } else if ( 'all_except' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - $( this ).closest('tr').next().next( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'select#woocommerce_allowed_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).show(); + } else if ( 'all_except' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + $( this ).closest( 'tr' ).next().next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); // Ship Countries - $( 'select#woocommerce_ship_to_countries' ).on( 'change', function() { - if ( 'specific' === $( this ).val() ) { - $( this ).closest('tr').next( 'tr' ).show(); - } else { - $( this ).closest('tr').next( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'select#woocommerce_ship_to_countries' ) + .on( 'change', function () { + if ( 'specific' === $( this ).val() ) { + $( this ).closest( 'tr' ).next( 'tr' ).show(); + } else { + $( this ).closest( 'tr' ).next( 'tr' ).hide(); + } + } ) + .trigger( 'change' ); // Stock management - $( 'input#woocommerce_manage_stock' ).on( 'change', function() { - if ( $( this ).is(':checked') ) { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).show(); - } else { - $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).hide(); - } - }).trigger( 'change' ); + $( 'input#woocommerce_manage_stock' ) + .on( 'change', function () { + if ( $( this ).is( ':checked' ) ) { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .show(); + } else { + $( this ) + .closest( 'tbody' ) + .find( '.manage_stock_field' ) + .closest( 'tr' ) + .hide(); + } + } ) + .trigger( 'change' ); // Color picker $( '.colorpick' ) - - .iris({ - change: function( event, ui ) { - $( this ).parent().find( '.colorpickpreview' ).css({ backgroundColor: ui.color.toString() }); + .iris( { + change: function ( event, ui ) { + $( this ) + .parent() + .find( '.colorpickpreview' ) + .css( { backgroundColor: ui.color.toString() } ); }, hide: true, - border: true - }) + border: true, + } ) - .on( 'click focus', function( event ) { + .on( 'click focus', function ( event ) { event.stopPropagation(); $( '.iris-picker' ).hide(); $( this ).closest( 'td' ).find( '.iris-picker' ).show(); $( this ).data( 'originalValue', $( this ).val() ); - }) + } ) - .on( 'change', function() { + .on( 'change', function () { if ( $( this ).is( '.iris-error' ) ) { var original_value = $( this ).data( 'originalValue' ); - if ( original_value.match( /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ ) ) { - $( this ).val( $( this ).data( 'originalValue' ) ).trigger( 'change' ); + if ( + original_value.match( + /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ + ) + ) { + $( this ) + .val( $( this ).data( 'originalValue' ) ) + .trigger( 'change' ); } else { $( this ).val( '' ).trigger( 'change' ); } } - }); + } ); - $( 'body' ).on( 'click', function() { + $( 'body' ).on( 'click', function () { $( '.iris-picker' ).hide(); - }); + } ); // Edit prompt - $( function() { + $( function () { var changed = false; let $check_column = $( '.wp-list-table .check-column' ); - $( 'input, textarea, select, checkbox' ).on( 'change', function( event ) { + $( 'input, textarea, select, checkbox' ).on( 'change', function ( + event + ) { // Toggling WP List Table checkboxes should not trigger navigation warnings. - if ( $check_column.length && $check_column.has( event.target ) ) { + if ( + $check_column.length && + $check_column.has( event.target ) + ) { return; } if ( ! changed ) { - window.onbeforeunload = function() { + window.onbeforeunload = function () { return params.i18n_nav_warning; }; changed = true; } - }); + } ); - $( '.submit :input, input#search-submit' ).on( 'click', function() { - window.onbeforeunload = ''; - }); - }); + $( '.submit :input, input#search-submit' ).on( + 'click', + function () { + window.onbeforeunload = ''; + } + ); + } ); // Sorting - $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable({ + $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable( { items: 'tr', cursor: 'move', axis: 'y', handle: 'td.sort', scrollSensitivity: 40, - helper: function( event, ui ) { - ui.children().each( function() { + helper: function ( event, ui ) { + ui.children().each( function () { $( this ).width( $( this ).width() ); - }); + } ); ui.css( 'left', '0' ); return ui; }, - start: function( event, ui ) { + start: function ( event, ui ) { ui.item.css( 'background-color', '#f6f6f6' ); }, - stop: function( event, ui ) { + stop: function ( event, ui ) { ui.item.removeAttr( 'style' ); ui.item.trigger( 'updateMoveButtons' ); - } - }); + }, + } ); // Select all/none - $( '.woocommerce' ).on( 'click', '.select_all', function() { - $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', true ); + $( '.woocommerce' ).on( 'click', '.select_all', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', true ); $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); return false; - }); + } ); - $( '.woocommerce' ).on( 'click', '.select_none', function() { - $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', false ); + $( '.woocommerce' ).on( 'click', '.select_none', function () { + $( this ) + .closest( 'td' ) + .find( 'select option' ) + .prop( 'selected', false ); $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); return false; - }); + } ); // Re-order buttons. - $( '.wc-item-reorder-nav').find( '.wc-move-up, .wc-move-down' ).on( 'click', function() { - var moveBtn = $( this ), - $row = moveBtn.closest( 'tr' ); + $( '.wc-item-reorder-nav' ) + .find( '.wc-move-up, .wc-move-down' ) + .on( 'click', function () { + var moveBtn = $( this ), + $row = moveBtn.closest( 'tr' ); - moveBtn.trigger( 'focus' ); + moveBtn.trigger( 'focus' ); - var isMoveUp = moveBtn.is( '.wc-move-up' ), - isMoveDown = moveBtn.is( '.wc-move-down' ); + var isMoveUp = moveBtn.is( '.wc-move-up' ), + isMoveDown = moveBtn.is( '.wc-move-down' ); - if ( isMoveUp ) { - var $previewRow = $row.prev( 'tr' ); + if ( isMoveUp ) { + var $previewRow = $row.prev( 'tr' ); - if ( $previewRow && $previewRow.length ) { - $previewRow.before( $row ); - wp.a11y.speak( params.i18n_moved_up ); + if ( $previewRow && $previewRow.length ) { + $previewRow.before( $row ); + wp.a11y.speak( params.i18n_moved_up ); + } + } else if ( isMoveDown ) { + var $nextRow = $row.next( 'tr' ); + + if ( $nextRow && $nextRow.length ) { + $nextRow.after( $row ); + wp.a11y.speak( params.i18n_moved_down ); + } } - } else if ( isMoveDown ) { - var $nextRow = $row.next( 'tr' ); - if ( $nextRow && $nextRow.length ) { - $nextRow.after( $row ); - wp.a11y.speak( params.i18n_moved_down ); - } - } + moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. + moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); + } ); - moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. - moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); - } ); + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .on( 'updateMoveButtons', function () { + var table = $( this ), + lastRow = $( this ).find( 'tbody tr:last' ), + firstRow = $( this ).find( 'tbody tr:first' ); - $( '.wc-item-reorder-nav').closest( 'table' ).on( 'updateMoveButtons', function() { - var table = $( this ), - lastRow = $( this ).find( 'tbody tr:last' ), - firstRow = $( this ).find( 'tbody tr:first' ); + table + .find( '.wc-item-reorder-nav .wc-move-disabled' ) + .removeClass( 'wc-move-disabled' ) + .attr( { tabindex: '0', 'aria-hidden': 'false' } ); + firstRow + .find( '.wc-item-reorder-nav .wc-move-up' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + lastRow + .find( '.wc-item-reorder-nav .wc-move-down' ) + .addClass( 'wc-move-disabled' ) + .attr( { tabindex: '-1', 'aria-hidden': 'true' } ); + } ); - table.find( '.wc-item-reorder-nav .wc-move-disabled' ).removeClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '0', 'aria-hidden': 'false' } ); - firstRow.find( '.wc-item-reorder-nav .wc-move-up' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - lastRow.find( '.wc-item-reorder-nav .wc-move-down' ).addClass( 'wc-move-disabled' ) - .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); - } ); + $( '.wc-item-reorder-nav' ) + .closest( 'table' ) + .trigger( 'updateMoveButtons' ); - $( '.wc-item-reorder-nav').closest( 'table' ).trigger( 'updateMoveButtons' ); - - - $( '.submit button' ).on( 'click', function() { + $( '.submit button' ).on( 'click', function () { if ( - $( 'select#woocommerce_allowed_countries' ).val() === 'specific' && + $( 'select#woocommerce_allowed_countries' ).val() === + 'specific' && ! $( '[name="woocommerce_specific_allowed_countries[]"]' ).val() ) { - if ( window.confirm( woocommerce_settings_params.i18n_no_specific_countries_selected ) ) { + if ( + window.confirm( + woocommerce_settings_params.i18n_no_specific_countries_selected + ) + ) { return true; } return false; } } ); - $('#settings-other-payment-methods').on('click', function( e ) { - if ( typeof window.wcTracks.recordEvent === 'undefined' ) { + $( '#settings-other-payment-methods' ).on( 'click', function ( e ) { + if ( + typeof window.wcTracks.recordEvent === 'undefined' && + typeof window.wc.tracks.recordEvent === 'undefined' + ) { return; } - var payment_methods = $.map( $( "td.wc_payment_gateways_wrapper tbody tr[data-gateway_id] "), function( tr ) { - return $( tr ).attr( 'data-gateway_id' ); - }); + var recordEvent = + window.wc.tracks.recordEvent || window.wcTracks.recordEvent; - window.wcTracks.recordEvent( 'settings_payments_recommendations_other_options' , { - available_payment_methods: payment_methods - }); + var payment_methods = $.map( + $( + 'td.wc_payment_gateways_wrapper tbody tr[data-gateway_id] ' + ), + function ( tr ) { + return $( tr ).attr( 'data-gateway_id' ); + } + ); - }); - - }); -})( jQuery, woocommerce_settings_params, wp ); + recordEvent( 'settings_payments_recommendations_other_options', { + available_payment_methods: payment_methods, + } ); + } ); + } ); +} )( jQuery, woocommerce_settings_params, wp ); From 33981358b69fd0d4d88f6234ce49f3dc25ba640d Mon Sep 17 00:00:00 2001 From: Jonathan Sadowski Date: Mon, 11 Apr 2022 10:49:45 -0500 Subject: [PATCH 155/206] Fetch more git history for changelogger comparison --- .github/workflows/pr-lint-monorepo.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-lint-monorepo.yml b/.github/workflows/pr-lint-monorepo.yml index f4a40ce8f31..7c952b3a641 100644 --- a/.github/workflows/pr-lint-monorepo.yml +++ b/.github/workflows/pr-lint-monorepo.yml @@ -1,4 +1,4 @@ -name: Run lint checks potentially affected projects across the monorepo +name: Run lint checks potentially affecting projects across the monorepo on: pull_request concurrency: group: changelogger-${{ github.event_name }}-${{ github.ref }} @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 10 + fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 From becc53d645c1c4c6331c7c41be40e68264d8ad16 Mon Sep 17 00:00:00 2001 From: moon Date: Mon, 11 Apr 2022 09:55:18 -0700 Subject: [PATCH 156/206] Make sure paymentGatewaySuggestions is defined --- .../client/payments/payment-recommendations.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx index 1bc484fcd9c..93d25671df2 100644 --- a/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx +++ b/plugins/woocommerce-admin/client/payments/payment-recommendations.tsx @@ -80,6 +80,7 @@ const PaymentRecommendations: React.FC = () => { ); const supportsWCPayments = + paymentGatewaySuggestions && paymentGatewaySuggestions.filter( ( paymentGatewaySuggestion ) => { return ( paymentGatewaySuggestion.id.indexOf( From fc7adc283ddcfeb4cf19d0d557bbde5f783e7862 Mon Sep 17 00:00:00 2001 From: moon Date: Mon, 11 Apr 2022 10:08:14 -0700 Subject: [PATCH 157/206] Add changelog --- ...update-32130-ui-changes-to-the-recommended-payment-options | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-32130-ui-changes-to-the-recommended-payment-options diff --git a/plugins/woocommerce/changelog/update-32130-ui-changes-to-the-recommended-payment-options b/plugins/woocommerce/changelog/update-32130-ui-changes-to-the-recommended-payment-options new file mode 100644 index 00000000000..ed8117ebe3c --- /dev/null +++ b/plugins/woocommerce/changelog/update-32130-ui-changes-to-the-recommended-payment-options @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Adds Other payment methods link to the payment setting page when the store is located in WC Payments eligible country. From 4729ae1f01b1a3140b8b2ec6c86ad58eb64c2fc4 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Thu, 7 Apr 2022 12:54:19 -0700 Subject: [PATCH 158/206] =?UTF-8?q?Product=20downloads=20=E2=86=92=20suppo?= =?UTF-8?q?rt=20a=20concept=20of=20active/inactive=20files.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../abstracts/abstract-wc-product.php | 94 ++++++++++++++----- .../views/html-product-data-general.php | 22 ++++- .../views/html-product-download.php | 17 +++- .../views/html-product-variation-download.php | 18 +++- .../meta-boxes/views/html-variation-admin.php | 25 ++++- .../includes/class-wc-product-download.php | 62 ++++++++---- .../includes/wc-user-functions.php | 5 + plugins/woocommerce/legacy/css/admin.scss | 13 +++ .../legacy/unit-tests/importer/product.php | 15 +-- .../class-wc-abstract-product-test.php | 36 +++---- .../class-wc-product-downloads-test.php | 44 --------- 11 files changed, 234 insertions(+), 117 deletions(-) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php index 68a85d7cbe4..3dd5352b356 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php @@ -1202,44 +1202,90 @@ class WC_Product extends WC_Abstract_Legacy_Product { /** * Set downloads. * - * @since 3.0.0 + * @throws WC_Data_Exception + * * @param array $downloads_array Array of WC_Product_Download objects or arrays. + * + * @since 3.0.0 */ public function set_downloads( $downloads_array ) { - $downloads = array(); - $errors = array(); + // When the object is first hydrated, only the previously persisted downloads will be passed in. + $existing_downloads = $this->get_object_read() ? (array) $this->get_prop( 'downloads' ) : $downloads_array; + $downloads = array(); + $errors = array(); + + $downloads_array = $this->build_downloads_map( $downloads_array ); + $existing_downloads = $this->build_downloads_map( $existing_downloads ); foreach ( $downloads_array as $download ) { - if ( is_a( $download, 'WC_Product_Download' ) ) { - $download_object = $download; - } else { - $download_object = new WC_Product_Download(); - - // If we don't have a previous hash, generate UUID for download. - if ( empty( $download['download_id'] ) ) { - $download['download_id'] = wp_generate_uuid4(); - } - - $download_object->set_id( $download['download_id'] ); - $download_object->set_name( $download['name'] ); - $download_object->set_file( $download['file'] ); - } + $download_id = $download->get_id(); + $is_new = ! isset( $existing_downloads[ $download_id ] ); try { - $download_object->check_is_valid(); - $downloads[ $download_object->get_id() ] = $download_object; + $download->check_is_valid( $this->get_object_read() ); + $downloads[ $download_id ] = $download; } catch ( Exception $e ) { - if ( $this->get_object_read() ) { + // We only add error messages for newly added downloads (let's not overwhelm the user if there are + // multiple existing files which are problematic). + if ( $is_new ) { $errors[] = $e->getMessage(); } - } - } - if ( $errors ) { - $this->error( 'product_invalid_download', $errors[0] ); + // If the problem is with an existing download, disable it. + if ( ! $is_new ) { + $download->set_enabled( false ); + $downloads[ $download_id ] = $download; + } + } } $this->set_prop( 'downloads', $downloads ); + + if ( $errors && $this->get_object_read() ) { + $this->error( 'product_invalid_download', $errors[0] ); + } + } + + /** + * Takes an array of downloadable file representations and converts it into an array of + * WC_Product_Download objects, indexed by download ID. + * + * @param array[]|WC_Product_Download[] $downloads Download data to be re-mapped. + * + * @return WC_Product_Download[] + */ + private function build_downloads_map( array $downloads ): array { + $downloads_map = array(); + + foreach ( $downloads as $download_data ) { + // If the item is already a WC_Product_Download we can add it to the map and move on. + if ( is_a( $download_data, 'WC_Product_Download' ) ) { + $downloads_map[ $download_data->get_id() ] = $download_data; + continue; + } + + // If the item is not an array, there is nothing else we can do (bad data). + if ( ! is_array( $download_data ) ) { + continue; + } + + // Otherwise, transform the array to a WC_Product_Download and add to the map. + $download_object = new WC_Product_Download(); + + // If we don't have a previous hash, generate UUID for download. + if ( empty( $download_data['download_id'] ) ) { + $download_data['download_id'] = wp_generate_uuid4(); + } + + $download_object->set_id( $download_data['download_id'] ); + $download_object->set_name( $download_data['name'] ); + $download_object->set_file( $download_data['file'] ); + $download_object->set_enabled( isset( $download_data['enabled'] ) ? $download_data['enabled'] : true ); + + $downloads_map[ $download_object->get_id() ] = $download_object; + } + + return $downloads_map; } /** diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php index db243413896..cff71f755d8 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-general.php @@ -88,9 +88,13 @@ defined( 'ABSPATH' ) || exit; get_downloads( 'edit' ); + $downloadable_files = $product_object->get_downloads( 'edit' ); + $disabled_downloads_count = 0; + if ( $downloadable_files ) { foreach ( $downloadable_files as $key => $file ) { + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-download.php'; } } @@ -98,7 +102,7 @@ defined( 'ABSPATH' ) || exit; - + + + + * + ', + '' + ); + ?> + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php index 621b785c4cd..437af94dc91 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-download.php @@ -1,4 +1,14 @@ " name="_wc_file_names[]" value="" /> - + + + + * + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php index 6c39752cc0f..f8eba6dd29d 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-variation-download.php @@ -1,4 +1,15 @@ " name="_wc_variation_file_names[][]" value="" /> - + + + + * + + diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php index 2bfdc85b8f3..5402c3576d1 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-variation-admin.php @@ -404,10 +404,13 @@ defined( 'ABSPATH' ) || exit; get_downloads( 'edit' ); + $downloadable_files = $variation_object->get_downloads( 'edit' ); + $disabled_downloads_count = 0; - if ( $downloads ) { - foreach ( $downloads as $key => $file ) { + if ( $downloadable_files ) { + foreach ( $downloadable_files as $key => $file ) { + $disabled_download = isset( $file['enabled'] ) && false === $file['enabled']; + $disabled_downloads_count += (int) $disabled_download; include __DIR__ . '/html-product-variation-download.php'; } } @@ -415,7 +418,7 @@ defined( 'ABSPATH' ) || exit;
- + + + + * + ', + '' + ); + ?> + +
diff --git a/plugins/woocommerce/includes/class-wc-product-download.php b/plugins/woocommerce/includes/class-wc-product-download.php index a3176b7d5ca..6f4f234e594 100644 --- a/plugins/woocommerce/includes/class-wc-product-download.php +++ b/plugins/woocommerce/includes/class-wc-product-download.php @@ -25,9 +25,10 @@ class WC_Product_Download implements ArrayAccess { * @var array */ protected $data = array( - 'id' => '', - 'name' => '', - 'file' => '', + 'id' => '', + 'name' => '', + 'file' => '', + 'enabled' => true, ); /** @@ -106,10 +107,20 @@ class WC_Product_Download implements ArrayAccess { public function check_is_valid( bool $auto_add_to_approved_directory_list = true ) { $download_file = $this->get_file(); + if ( ! $this->data['enabled'] ) { + throw new Exception( + sprintf( + /* translators: %s: Downloadable file. */ + __( 'The downloadable file %s cannot be used as it has been disabled.', 'woocommerce' ), + '' . basename( $download_file ) . '' + ) + ); + } + if ( ! $this->is_allowed_filetype() ) { throw new Exception( sprintf( - /* translators: %1$s: Downloadable file */ + /* translators: 1: Downloadable file, 2: List of allowed filetypes. */ __( 'The downloadable file %1$s cannot be used as it does not have an allowed file type. Allowed types include: %2$s', 'woocommerce' ), '' . basename( $download_file ) . '', '' . implode( ', ', array_keys( $this->get_allowed_mime_types() ) ) . '' @@ -121,7 +132,7 @@ class WC_Product_Download implements ArrayAccess { if ( ! $this->file_exists() ) { throw new Exception( sprintf( - /* translators: %s: Downloadable file */ + /* translators: %s: Downloadable file */ __( 'The downloadable file %s cannot be used as it does not exist on the server.', 'woocommerce' ), '' . $download_file . '' ) @@ -215,25 +226,22 @@ class WC_Product_Download implements ArrayAccess { $is_site_administrator = is_multisite() ? current_user_can( 'manage_sites' ) : current_user_can( 'manage_options' ); $valid_storage_directory = $download_directories->is_valid_path( $download_file ); - if ( ! $valid_storage_directory && $auto_add_to_approved_directory_list ) { + if ( $valid_storage_directory ) { + return; + } + + if ( $auto_add_to_approved_directory_list ) { try { // Add the parent URL to the approved directories list, but *do not enable it* unless the current user is a site admin. $download_directories->add_approved_directory( ( new URL( $download_file ) )->get_parent_url(), $is_site_administrator ); - } catch ( Exception $e ) { - /* translators: %s: Downloadable file */ - throw new Exception( - sprintf( - /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ - __( 'The downloadable file %1$s cannot be used: it is not located in an approved directory. Please contact a site administrator and request their approval. %2$sLearn more.%3$s', 'woocommerce' ), - '' . $download_file . '', - '', - '' - ) - ); + $valid_storage_directory = $download_directories->is_valid_path( $download_file ); + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // At this point, $valid_storage_directory will be false. Fall-through so the appropriate exception is + // triggered (same as if the storage directory was invalid and $auto_add_to_approved_directory_list was false. } } - if ( ! $valid_storage_directory && ! $is_site_administrator ) { + if ( ! $valid_storage_directory ) { throw new Exception( sprintf( /* translators: %1$s is the downloadable file path, %2$s is an opening link tag, %3%s is a closing link tag. */ @@ -302,6 +310,15 @@ class WC_Product_Download implements ArrayAccess { } } + /** + * Sets the status of the download to enabled (true) or disabled (false). + * + * @param bool $enabled True indicates the downloadable file is enabled, false indicates it is disabled. + */ + public function set_enabled( bool $enabled = true ) { + $this->data['enabled'] = $enabled; + } + /* |-------------------------------------------------------------------------- | Getters @@ -346,6 +363,15 @@ class WC_Product_Download implements ArrayAccess { return $this->data['file']; } + /** + * Get status of the download. + * + * @return bool + */ + public function get_enabled(): bool { + return $this->data['enabled']; + } + /* |-------------------------------------------------------------------------- | ArrayAccess/Backwards compatibility. diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php index 0c30c4f1aeb..a874c92a052 100644 --- a/plugins/woocommerce/includes/wc-user-functions.php +++ b/plugins/woocommerce/includes/wc-user-functions.php @@ -614,6 +614,11 @@ function wc_get_customer_available_downloads( $customer_id ) { $download_file = $_product->get_file( $result->download_id ); + // If the downloadable file has been disabled (it may be located in an untrusted location) then do not return it. + if ( ! $download_file->get_enabled() ) { + continue; + } + // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. $download_name = apply_filters( 'woocommerce_downloadable_product_name', diff --git a/plugins/woocommerce/legacy/css/admin.scss b/plugins/woocommerce/legacy/css/admin.scss index be221579281..b5fd23b2ae4 100644 --- a/plugins/woocommerce/legacy/css/admin.scss +++ b/plugins/woocommerce/legacy/css/admin.scss @@ -5110,6 +5110,14 @@ img.help_tip { margin: 1px 0; } + &.file_url { + /* Reduce the size of this field to make space for a warning asterisk. */ + input { + display: inline-block; + width: 96%; + } + } + .upload_file_button { width: auto; float: right; @@ -5160,6 +5168,11 @@ img.help_tip { color: #333; } } + + /* Warning asterisk (indicates if there is a problem with a downloadable file). */ + span.disabled { + color: var( --wc-red ); + } } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php index 637506ea631..3fbab5add05 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/importer/product.php @@ -35,11 +35,14 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { // Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response. $this->http_responder = array( $this, 'mock_http_responses' ); $this->csv_file = dirname( __FILE__ ) . '/sample.csv'; - $this->sut = new WC_Product_CSV_Importer( $this->csv_file, array( - 'mapping' => $this->get_csv_mapped_items(), - 'parse' => true, - 'prevent_timeouts' => false, - ) ); + $this->sut = new WC_Product_CSV_Importer( + $this->csv_file, + array( + 'mapping' => $this->get_csv_mapped_items(), + 'parse' => true, + 'prevent_timeouts' => false, + ) + ); } /** @@ -112,7 +115,7 @@ class WC_Tests_Product_CSV_Importer extends WC_Unit_Test_Case { $this->assertEquals( 0, count( $results['skipped'] ) ); $this->assertEquals( 7, - count( $results['imported'] ) , + count( $results['imported'] ), 'One import item references a downloadable file stored in an unapproved location: if the import is triggered by an admin user, that location will be automatically approved.' ); } diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php index aaf81b31e7c..98f97525e9e 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php @@ -18,16 +18,18 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { $download_directories->add_approved_directory( 'https://always.trusted/' ); $problematic_file_source_id = $download_directories->add_approved_directory( 'https://new.supplier/' ); - $product = WC_Helper_Product::create_downloadable_product( array( + $product = WC_Helper_Product::create_downloadable_product( array( - 'name' => 'Book 1', - 'file' => 'https://always.trusted/123.pdf' - ), - array( - 'name' => 'Book 2', - 'file' => 'https://new.supplier/456.pdf' - ), - ) ); + array( + 'name' => 'Book 1', + 'file' => 'https://always.trusted/123.pdf', + ), + array( + 'name' => 'Book 2', + 'file' => 'https://new.supplier/456.pdf', + ), + ) + ); $this->assertCount( 2, @@ -36,17 +38,17 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { ); $download_directories->disable_by_id( $problematic_file_source_id ); + $product_downloads = wc_get_product( $product->get_id() )->get_downloads(); $this->assertCount( - 1, - wc_get_product( $product->get_id() )->get_downloads(), - 'If a trusted download directory is disabled, we expect any individual download files from that location will not be listed.' + 2, + $product_downloads, + 'If a trusted download directory rule is disabled, we still expect it to be fetched.' ); - $this->assertEquals( - 'Book 1', - current( wc_get_product( $product->get_id() )->get_downloads() )->get_name(), - 'Only individual download files that are stored in trusted locations will be fetched.' + $this->assertFalse( + next( $product_downloads )->get_enabled(), + 'If a trusted download directory rule is disabled, corresponding product downloads will also be marked as disabled.' ); $download_directories->set_mode( Download_Directories::MODE_DISABLED ); @@ -54,7 +56,7 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case { $this->assertCount( 2, wc_get_product( $product->get_id() )->get_downloads(), - 'If the Approved Download Directories system is completely disabled, we expect all product downloads to be fetched irrespective of where they are stored.' + 'Disabling the Approved Download Directories system entirely does not impact our ability to fetch product downloads.' ); } } diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php index ba3d9eb484e..0cb0ad4dc15 100644 --- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php +++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php @@ -63,48 +63,4 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $this->expectExceptionMessage( 'cannot be used: it is not located in an approved directory' ); $download->check_is_valid(); } - - /** - * Test handling of filepaths described via shortcodes in relation to the Approved Download Directory - * feature. This is to simulate scenarios such as encountered when using the S3 Downloads extension. - */ - public function test_shortcode_resolution_for_approved_directory_rules() { - /** @var Download_Directories $download_directories */ - $download_directories = wc_get_container()->get( Download_Directories::class ); - $download_directories->set_mode( Download_Directories::MODE_ENABLED ); - $dynamic_filepath = 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; - - // We select an admin user because we wish to automatically add Approved Directory rules. - $admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'administrator', 'user_pass' => 'x' ) ); - wp_set_current_user( $admin_user ); - - add_shortcode( 'dynamic-download', function () { - return 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; - } ); - - $this->assertFalse( - $download_directories->is_valid_path( $dynamic_filepath ), - 'Confirm the filepath returned by the test URL is not yet valid.' - ); - - $download = new WC_Product_Download(); - $download->set_file( '[dynamic-download]' ); - - $this->assertNull( - $download->check_is_valid(), - 'The downloadable file successfully validates (if it did not, an exception would be thrown).' - ); - - $this->assertTrue( - $download_directories->is_valid_path( $dynamic_filepath ), - 'Confirm the filepath returned by the test URL is now considered valid.' - ); - - remove_shortcode( 'dynamic-download' ); - - // Now the shortcode is removed (perhaps the parent plugin has been removed/disabled) it will not resolve - // and so the filepath will not validate. - $this->expectException( 'Error' ); - $download_directories->check_is_valid(); - } } From 10b8eef1f233b8106bffaaf6d916a57ca4700bc1 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:44:10 -0700 Subject: [PATCH 159/206] Add changelog. --- plugins/woocommerce/changelog/fix-downloadable-file-data | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-downloadable-file-data diff --git a/plugins/woocommerce/changelog/fix-downloadable-file-data b/plugins/woocommerce/changelog/fix-downloadable-file-data new file mode 100644 index 00000000000..f99ba74d55b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-downloadable-file-data @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Make it possible for downloadable files to be in an enabled or disabled state. From 49b3a328ce587fa5a40a2b8f55d34c80cc64a0c2 Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:51:55 -0700 Subject: [PATCH 160/206] PHPCS/formatting fixes; restore inadvertently removed test. --- .../abstracts/abstract-wc-product.php | 2 +- .../class-wc-product-downloads-test.php | 73 ++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php index 3dd5352b356..71ffba8a229 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php @@ -1202,7 +1202,7 @@ class WC_Product extends WC_Abstract_Legacy_Product { /** * Set downloads. * - * @throws WC_Data_Exception + * @throws WC_Data_Exception If an error relating to one of the downloads is encountered. * * @param array $downloads_array Array of WC_Product_Download objects or arrays. * diff --git a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php index 0cb0ad4dc15..8192a44a069 100644 --- a/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php +++ b/plugins/woocommerce/tests/php/includes/class-wc-product-downloads-test.php @@ -9,7 +9,7 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { * Test for file without extension. */ public function test_is_allowed_filetype_with_no_extension() { - $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); + $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); $file_path_with_no_extension = $upload_dir . 'upload_file'; if ( ! file_exists( $file_path_with_no_extension ) ) { // Copy an existing file without extension. @@ -24,7 +24,7 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { * Simulates test condition for windows when filename ends with a period. */ public function test_is_allowed_filetype_on_windows_with_period_at_end() { - $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); + $upload_dir = trailingslashit( wp_upload_dir()['basedir'] ); $file_path_with_period_at_end = $upload_dir . 'upload_file.'; if ( ! file_exists( $file_path_with_period_at_end ) ) { // Copy an existing file without extension. @@ -45,8 +45,20 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $download_directories = wc_get_container()->get( Download_Directories::class ); $download_directories->set_mode( Download_Directories::MODE_ENABLED ); - $non_admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'editor', 'user_pass' => 'x' ) ); - $admin_user = wp_insert_user( array( 'user_login' => uniqid(), 'role' => 'administrator', 'user_pass' => 'x' ) ); + $non_admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'editor', + 'user_pass' => 'x', + ) + ); + $admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'administrator', + 'user_pass' => 'x', + ) + ); $ebook_url = 'https://external.site/books/ultimate-guide-to-stuff.pdf'; $podcast_url = 'https://external.site/podcasts/ultimate-guide-to-stuff.mp3'; @@ -63,4 +75,57 @@ class WC_Product_Download_Test extends WC_Unit_Test_Case { $this->expectExceptionMessage( 'cannot be used: it is not located in an approved directory' ); $download->check_is_valid(); } + + /** + * Test handling of filepaths described via shortcodes in relation to the Approved Download Directory + * feature. This is to simulate scenarios such as encountered when using the S3 Downloads extension. + */ + public function test_shortcode_resolution_for_approved_directory_rules() { + /** @var Download_Directories $download_directories */ + $download_directories = wc_get_container()->get( Download_Directories::class ); + $download_directories->set_mode( Download_Directories::MODE_ENABLED ); + $dynamic_filepath = 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + + // We select an admin user because we wish to automatically add Approved Directory rules. + $admin_user = wp_insert_user( + array( + 'user_login' => uniqid(), + 'role' => 'administrator', + 'user_pass' => 'x', + ) + ); + wp_set_current_user( $admin_user ); + + add_shortcode( + 'dynamic-download', + function () { + return 'https://fast.reliable.external.fileserver.com/bucket-123/textbook.pdf'; + } + ); + + $this->assertFalse( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is not yet valid.' + ); + + $download = new WC_Product_Download(); + $download->set_file( '[dynamic-download]' ); + + $this->assertNull( + $download->check_is_valid(), + 'The downloadable file successfully validates (if it did not, an exception would be thrown).' + ); + + $this->assertTrue( + $download_directories->is_valid_path( $dynamic_filepath ), + 'Confirm the filepath returned by the test URL is now considered valid.' + ); + + remove_shortcode( 'dynamic-download' ); + + // Now the shortcode is removed (perhaps the parent plugin has been removed/disabled) it will not resolve + // and so the filepath will not validate. + $this->expectException( 'Error' ); + $download_directories->check_is_valid(); + } } From ac1ad607b4bd780942e4f79518c75b2c4ed15548 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Mon, 11 Apr 2022 16:29:43 -0700 Subject: [PATCH 161/206] Normalize auto-draft as a status in the abstract order class Ensures that an existing order with auto-draft status won't be interpreted as pending when determining if the status has changed. This also fixes an unrelated phpcs formatting error in order to pass the precommit checks. Fixes #32539 --- .../woocommerce/includes/abstracts/abstract-wc-order.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php index abb2e63fbe9..83d59992a42 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php @@ -201,7 +201,8 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { } catch ( Exception $e ) { $message_id = $this->get_id() ? $this->get_id() : __( '(no ID)', 'woocommerce' ); - $this->handle_exception( $e, + $this->handle_exception( + $e, wp_kses_post( sprintf( /* translators: 1: Order ID or "(no ID)" if not known. */ @@ -551,15 +552,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order { $old_status = $this->get_status(); $new_status = 'wc-' === substr( $new_status, 0, 3 ) ? substr( $new_status, 3 ) : $new_status; + $status_exceptions = array( 'auto-draft', 'trash' ); + // If setting the status, ensure it's set to a valid status. if ( true === $this->object_read ) { // Only allow valid new status. - if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && 'trash' !== $new_status && 'auto-draft' !== $new_status ) { + if ( ! in_array( 'wc-' . $new_status, $this->get_valid_statuses(), true ) && ! in_array( $new_status, $status_exceptions, true ) ) { $new_status = 'pending'; } // If the old status is set but unknown (e.g. draft) assume its pending for action usage. - if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && 'trash' !== $old_status ) { + if ( $old_status && ! in_array( 'wc-' . $old_status, $this->get_valid_statuses(), true ) && ! in_array( $old_status, $status_exceptions, true ) ) { $old_status = 'pending'; } } From e2da686c31eb3609c862515c596a9d10e4f42cd8 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 09:49:23 +0800 Subject: [PATCH 162/206] Add eway --- .../assets/images/payment_methods/72x72/eway.png | Bin 0 -> 790 bytes .../DefaultPaymentGateways.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 plugins/woocommerce/assets/images/payment_methods/72x72/eway.png diff --git a/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png b/plugins/woocommerce/assets/images/payment_methods/72x72/eway.png new file mode 100644 index 0000000000000000000000000000000000000000..74884fa1b03f35a5e95b9869e28477812d2975f3 GIT binary patch literal 790 zcmV+x1L^#UP)tcqc9MLX&{8Q zyE8r?Y9J(e|94z_Kx#^mScq>b<-f6JVm{A5f>?I(t+F{CEBJ3!$J3v(c+{b;V7pF- z8K(Szo%;A$&KvC3+g(wg#Mw>yoYnJmO(k)jB9#Q1^?7&450F90+o zS=5lT3_2CZJfLGyTI`b}HF{KvI@sDa&XTJj*@59nJ(@V*B}e4(9AjV_qjXwj z4TK#89CeNl`{al`_Q+D_I_?Kgi51XS0LMlF+@vN)H`EL}029F^j_n#q#3|pp;DClY z=hG;tffNHs4J5S;vN-+jIYo$U92IfxG)W{FEopEW1RRwGXEpZY(j8?b8DXeQ3yez{ z>DL&5^KuzPB+|uEU1+AHrR5bSscQ<(6sSdOB*umCZVIkHM`i{wSeOEzCWesd>&JdO zNb$#NB_DGF*>(^|Gf&Zrx*2qnlFi*B+W~?z1~)Ut_x)#c40jbcfdck`h@?21 zFh&t)ABbLVJ$S=~MCqT}j!Z8xspIbHeT`Xrb-!P&}$^-Zx$!Hh0zh1Iox5 Uq9MrH&;S4c07*qoM6N<$f@U{VqW}N^ literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php index e648fa9722f..4b64d63b5ac 100644 --- a/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php +++ b/plugins/woocommerce/src/Admin/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php @@ -354,7 +354,7 @@ class DefaultPaymentGateways { 'id' => 'eway', 'title' => __( 'Eway', 'woocommerce' ), 'content' => __( 'The Eway extension for WooCommerce allows you to take credit card payments directly on your store without redirecting your customers to a third party site to make payment.', 'woocommerce' ), - 'image' => WC_ADMIN_IMAGES_FOLDER_URL . '/onboarding/eway.png', + 'image' => WC()->plugin_url() . '/assets/images/payment_methods/72x72/eway.png', 'plugins' => array( 'woocommerce-gateway-eway' ), 'is_visible' => array( self::get_rules_for_countries( array( 'AU', 'NZ' ) ), From 0ae4f8e484662a848c2c766d0ab487d08f771a4d Mon Sep 17 00:00:00 2001 From: Jacob Sewell Date: Mon, 11 Apr 2022 21:08:26 -0500 Subject: [PATCH 163/206] Add context param to Notes\DataStore::get_notes() and associated methods/filters. --- .../woocommerce/src/Admin/Notes/DataStore.php | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Notes/DataStore.php b/plugins/woocommerce/src/Admin/Notes/DataStore.php index 0a7abdf954b..ea1949979b8 100644 --- a/plugins/woocommerce/src/Admin/Notes/DataStore.php +++ b/plugins/woocommerce/src/Admin/Notes/DataStore.php @@ -11,6 +11,9 @@ defined( 'ABSPATH' ) || exit; * WC Admin Note Data Store (Custom Tables) */ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Interface { + // Extensions should define their own contexts and use them to avoid applying woocommerce_note_where_clauses when not needed. + const WC_ADMIN_NOTE_OPER_GLOBAL = 'global'; + /** * Method to create a new note in the database. * @@ -324,9 +327,10 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * Return an ordered list of notes. * * @param array $args Query arguments. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return array An array of objects containing a note id. */ - public function get_notes( $args = array() ) { + public function get_notes( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { global $wpdb; $defaults = array( @@ -338,7 +342,7 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter $args = wp_parse_args( $args, $defaults ); $offset = $args['per_page'] * ( $args['page'] - 1 ); - $where_clauses = $this->get_notes_where_clauses( $args ); + $where_clauses = $this->get_notes_where_clauses( $args, $context ); $query = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared @@ -378,16 +382,18 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * * @param string $type Comma separated list of note types. * @param string $status Comma separated list of statuses. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return array An array of objects containing a note id. */ - public function get_notes_count( $type = array(), $status = array() ) { + public function get_notes_count( $type = array(), $status = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { global $wpdb; $where_clauses = $this->get_notes_where_clauses( array( 'type' => $type, 'status' => $status, - ) + ), + $context ); if ( ! empty( $where_clauses ) ) { @@ -426,9 +432,10 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * * @uses args_to_where_clauses * @param array $args Array of args to pass. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return string Where clauses for the query. */ - public function get_notes_where_clauses( $args = array() ) { + public function get_notes_where_clauses( $args = array(), $context = self::WC_ADMIN_NOTE_OPER_GLOBAL ) { $where_clauses = $this->args_to_where_clauses( $args ); /** @@ -438,8 +445,9 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * * @param string $where_clauses The generated WHERE clause. * @param array $args The original arguments for the request. + * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. */ - return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args ); + return apply_filters( 'woocommerce_note_where_clauses', $where_clauses, $args, $context ); } /** From e83631983a6339f0ae28036b680a20d33f4ad027 Mon Sep 17 00:00:00 2001 From: Jacob Sewell Date: Mon, 11 Apr 2022 21:09:04 -0500 Subject: [PATCH 164/206] Add unit tests for new context param in Notes\DataStore::get_notes(). --- .../notes/class-wc-tests-notes-data-store.php | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php index c36be43720d..b4445dab708 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/notes/class-wc-tests-notes-data-store.php @@ -7,6 +7,7 @@ use \Automattic\WooCommerce\Admin\Notes\Notes; use \Automattic\WooCommerce\Admin\Notes\Note; +use \Automattic\WooCommerce\Admin\Notes\DataStore; /** * Class WC_Admin_Tests_Notes_Data_Store @@ -344,6 +345,92 @@ class WC_Admin_Tests_Notes_Data_Store extends WC_Unit_Test_Case { $this->assertEquals( $lookup_note_zero->get_id(), $get_note_zero->get_id() ); } + /** + * Test that get_notes properly handles the $context parameter + */ + public function test_get_notes_context_param() { + $data_store = WC_Data_Store::load( 'admin-note' ); + + // Create two Notes: one in context and one out of it. + $test_context = self::class; + $global_context = DataStore::WC_ADMIN_NOTE_OPER_GLOBAL; + $context_name = 'PHP_UNIT_IN_CONTEXT_TEST_NOTE'; + $out_of_context_name = 'PHP_UNIT_OUT_OF_CONTEXT_TEST_NOTE'; + + foreach ( array( $context_name, $out_of_context_name ) as $note_name ) { + $note = new Note(); + $note->set_name( $note_name ); + $note->set_title( 'PHPUNIT_CONTEXT_TEST_NOTE' ); + $note->set_content( 'PHPUNIT_CONTEXT_TEST_NOTE_CONTENT' ); + $note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note->set_source( 'PHPUNIT_TEST' ); + $note->set_is_snoozable( false ); + $note->set_layout( 'plain' ); + $note->set_image( '' ); + $note->add_action( + 'PHPUNIT_TEST_ACTION_SLUG', + 'PHPUNIT_TEST_ACTION_LABEL', + '?s=PHPUNIT_TEST_ACTION_URL' + ); + $note->set_is_deleted( false ); + $note->save(); + } + + // Add filter for 'woocommerce_note_where_clauses' that applies only in context. + $context_filter_hit_count = 0; + $context_filter_callback = function( $where_clauses, $args, $context ) use ( $test_context, $global_context, $context_name, &$context_filter_hit_count ) { + if ( $context === $test_context ) { + $context_filter_hit_count++; + $where_clauses .= ' AND name = "' . $context_name . '"'; + } + return $where_clauses; + }; + add_filter( 'woocommerce_note_where_clauses', $context_filter_callback, 10, 3 ); + + // Add filter for 'woocommerce_note_where_clauses' that applies in any context. + $no_context_filter_hit_count = 0; + $global_context_received = null; + $no_context_filter_callback = function( $where_clauses, $args, $context ) use ( $test_context, &$global_context_received, &$no_context_filter_hit_count ) { + // Record the context we get passed in. + if ( $test_context !== $context ) { + $global_context_received = $context; + } + + // Record that we're here. + $no_context_filter_hit_count++; + $where_clauses .= ' AND source = "PHPUNIT_TEST"'; + return $where_clauses; + }; + add_filter( 'woocommerce_note_where_clauses', $no_context_filter_callback, 10, 3 ); + + // Get note counts. + $no_context_note_count = $data_store->get_notes_count( array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ), array() ); + $test_context_note_count = $data_store->get_notes_count( array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ), array(), $test_context ); + + // The no context filter should have been hit twice, the context filter once. + $this->assertEquals( 2, $no_context_filter_hit_count ); + $this->assertEquals( 1, $context_filter_hit_count ); + + // There should be only one note that satisfies the context filter. + $this->assertEquals( 1, $test_context_note_count ); + + // There should be more than one note that satisfies the no-context filter. + $this->assertGreaterThan( 1, $no_context_note_count ); + + // Same tests with get_notes(). + $no_context_get_notes = $data_store->get_notes( array( 'type' => array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ) ) ); + $test_context_get_notes = $data_store->get_notes( array( 'type' => array( Note::E_WC_ADMIN_NOTE_INFORMATIONAL ) ), $test_context ); + + // There should only be one note in the context result set. + $this->assertEquals( 1, count( $test_context_get_notes ) ); + + // There should be more than one note in the no-context result set. + $this->assertGreaterThan( 1, count( $no_context_get_notes ) ); + + // When not explicitly passed, context should default to WC_ADMIN_NOTE_OPER_GLOBAL. + $this->assertEquals( $global_context, $global_context_received ); + } + /** * Delete notes created by this class's tests. */ From 7cca9642f45d4453b46d1d541d78d7afddbe0fac Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 10:09:07 +0800 Subject: [PATCH 165/206] Remove enabled section --- .../fills/PaymentGatewaySuggestions/index.js | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js index 084a4711efa..032b85c327a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/index.js @@ -185,12 +185,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { return gateway; }, [ isResolving, query, paymentGateways ] ); - const [ - wcPayGateway, - enabledGateways, - offlineGateways, - additionalGateways, - ] = useMemo( + const [ wcPayGateway, offlineGateways, additionalGateways ] = useMemo( () => Array.from( paymentGateways.values() ) .sort( ( a, b ) => { @@ -207,7 +202,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { } ) .reduce( ( all, gateway ) => { - const [ wcPay, enabled, offline, additional ] = all; + const [ wcPay, offline, additional ] = all; // WCPay is handled separately when not installed and configured if ( @@ -216,8 +211,6 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { ! ( gateway.installed && ! gateway.needsSetup ) ) { wcPay.push( gateway ); - } else if ( gateway.enabled ) { - enabled.push( gateway ); } else if ( gateway.is_offline ) { offline.push( gateway ); } else { @@ -226,7 +219,7 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { return all; }, - [ [], [], [], [] ] + [ [], [], [] ] ), [ paymentGateways ] ); @@ -258,14 +251,6 @@ export const PaymentGatewaySuggestions = ( { onComplete, query } ) => { ); } - const enabledSection = !! enabledGateways.length && ( - - ); - const additionalSection = !! additionalGateways.length && ( { > { additionalSection } { offlineSection } - { enabledSection } ) : ( <> { additionalSection } { offlineSection } - { enabledSection } ) }
From e1db7ab0f2f1a54de7169c382f5dac88ab989960 Mon Sep 17 00:00:00 2001 From: Jacob Sewell Date: Mon, 11 Apr 2022 21:17:59 -0500 Subject: [PATCH 166/206] Changelog for 32573/32574. --- .../woocommerce/changelog/add-32573-context-arg-for-get-notes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-32573-context-arg-for-get-notes diff --git a/plugins/woocommerce/changelog/add-32573-context-arg-for-get-notes b/plugins/woocommerce/changelog/add-32573-context-arg-for-get-notes new file mode 100644 index 00000000000..d576cf03ea3 --- /dev/null +++ b/plugins/woocommerce/changelog/add-32573-context-arg-for-get-notes @@ -0,0 +1,4 @@ +Significance: minor +Type: performance + +Add a context param with a default value of global to Admin\Notes\DataStore::get_notes(), get_notes_count(), and get_notes_where_clauses(). #32574 From 0b2df9a95657456c2e03dece9e91cb2bc3d7c390 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 10:52:11 +0800 Subject: [PATCH 167/206] Fix text domain, standardize component conditional, remove unnecessary comment --- .../fills/PaymentGatewaySuggestions/components/Action.js | 2 +- .../fills/PaymentGatewaySuggestions/components/List/List.js | 6 ++---- .../PaymentGatewaySuggestions/components/Setup/Configure.js | 2 +- .../PaymentGatewaySuggestions/components/Toggle/Toggle.js | 2 -- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js index 90c0486ecbc..ae4c9ad1f2a 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Action.js @@ -24,7 +24,7 @@ export const Action = ( { markConfigured, onSetUp = () => {}, onSetupCallback, - setupButtonText = __( 'Get started', 'woocommerce-admin' ), + setupButtonText = __( 'Get started', 'woocommerce' ), } ) => { const [ isBusy, setIsBusy ] = useState( false ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js index d9442b284f7..289243ed3c9 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/List/List.js @@ -19,7 +19,7 @@ export const List = ( { } ) => { return ( - { heading ? { heading } : null } + { heading && { heading } } { paymentGateways.map( ( paymentGateway ) => { const { id } = paymentGateway; return ( @@ -31,10 +31,8 @@ export const List = ( { /> ); } ) } - { footerLink ? ( + { footerLink && ( { footerLink } - ) : ( - '' ) } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js index 5b5e45b5f92..b80430d58e9 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Setup/Configure.js @@ -156,7 +156,7 @@ export const Configure = ( { markConfigured, paymentGateway } ) => {

) } ); diff --git a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js index d096360b166..95ad0f2b078 100644 --- a/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js +++ b/plugins/woocommerce-admin/client/tasks/fills/PaymentGatewaySuggestions/components/Toggle/Toggle.js @@ -38,5 +38,3 @@ export const Toggle = ( { children, heading, onToggle } ) => {
); }; - -// export default Toggle; From b7e156cdfa1801a6416889d1877629251b589786 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 11:19:09 +0800 Subject: [PATCH 168/206] Add changelog for data package --- packages/js/data/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/data/CHANGELOG.md b/packages/js/data/CHANGELOG.md index cb955c6b7e2..f33e3ab6327 100644 --- a/packages/js/data/CHANGELOG.md +++ b/packages/js/data/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - Update dependency `@wordpress/hooks` to ^3.5.0 +- Add `is_offline` attribute for `Plugin` type # 3.1.0 From 1fb2418e57d553a3f607b904e9ddf35da1c8d85e Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 11:40:02 +0800 Subject: [PATCH 169/206] Add changelog for data package --- packages/js/data/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/data/CHANGELOG.md b/packages/js/data/CHANGELOG.md index f33e3ab6327..a69294d2ca7 100644 --- a/packages/js/data/CHANGELOG.md +++ b/packages/js/data/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased - Update dependency `@wordpress/hooks` to ^3.5.0 -- Add `is_offline` attribute for `Plugin` type +- Add `is_offline` attribute for `Plugin` type. #32467 # 3.1.0 From 29e27fff35c967b5fa683625c8cc66024d71dee8 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 11:47:22 +0800 Subject: [PATCH 170/206] Update e2e --- packages/js/admin-e2e-tests/CHANGELOG.md | 2 ++ packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts | 4 ++++ packages/js/admin-e2e-tests/src/specs/tasks/payment.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/packages/js/admin-e2e-tests/CHANGELOG.md b/packages/js/admin-e2e-tests/CHANGELOG.md index d764841fc75..db24c246ffd 100644 --- a/packages/js/admin-e2e-tests/CHANGELOG.md +++ b/packages/js/admin-e2e-tests/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update test for payment task. #32467 + # 1.0.0 - Add returned type annotations and remove unused vars. #8020 diff --git a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts index 5224a8825a6..4b0720454ca 100644 --- a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts +++ b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts @@ -25,6 +25,10 @@ export class PaymentsSetup extends BasePage { await this.clickButtonWithText( 'Got it' ); } + async toggleOtherPaymentMethods(): Promise< void > { + await this.clickButtonWithText( 'Other payment methods' ); + } + async goToPaymentMethodSetup( method: PaymentMethodWithSetupButton ): Promise< void > { diff --git a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts index 0515dce47f3..b017e0ac837 100644 --- a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts +++ b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts @@ -67,6 +67,7 @@ const testAdminPaymentSetupTask = () => { await waitForTimeout( 1000 ); await homeScreen.clickOnTaskList( 'Set up payments' ); await paymentsSetup.isDisplayed(); + await paymentsSetup.toggleOtherPaymentMethods(); await paymentsSetup.methodHasBeenSetup( 'bacs' ); } ); @@ -76,6 +77,8 @@ const testAdminPaymentSetupTask = () => { await homeScreen.isDisplayed(); await waitForTimeout( 1000 ); await homeScreen.clickOnTaskList( 'Set up payments' ); + await paymentsSetup.isDisplayed(); + await paymentsSetup.toggleOtherPaymentMethods(); await paymentsSetup.enableCashOnDelivery(); await homeScreen.navigate(); await homeScreen.isDisplayed(); From 13720a663e679ba94644b5a32981708e5b1eee9c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Tue, 12 Apr 2022 15:47:03 +0800 Subject: [PATCH 171/206] Fix admin clean command --- plugins/woocommerce-admin/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index d8270041af0..6be60f4ffc8 100644 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -15,7 +15,7 @@ "build-storybook": "build-storybook -c ./storybook/.storybook", "build:feature-config": "php ../woocommerce/bin/generate-feature-config.php", "build:packages": "cross-env NODE_ENV=production pnpm run:packages -- build", - "clean": "rimraf ./dist && pnpm run:packages -- clean --parallel", + "clean": "rimraf ../woocommerce/assets/client/admin/* && pnpm run:packages -- clean --parallel", "client:watch": "cross-env WC_ADMIN_PHASE=development pnpm run build:feature-config && cross-env WC_ADMIN_PHASE=development webpack --watch", "create-hook-reference": "node ./bin/hook-reference/index.js", "create-wc-extension": "node ./bin/starter-pack/starter-pack.js", From e13c30cfc4d8624c5e904f20bf53874cdde56575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Tue, 12 Apr 2022 10:32:56 +0200 Subject: [PATCH 172/206] Add changelog file for #32460. --- plugins/woocommerce/changelog/changelog-32460 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/changelog-32460 diff --git a/plugins/woocommerce/changelog/changelog-32460 b/plugins/woocommerce/changelog/changelog-32460 new file mode 100644 index 00000000000..b4c3c5bb5ae --- /dev/null +++ b/plugins/woocommerce/changelog/changelog-32460 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +WC_Product_CSV_Importer_Controller::is_file_valid_csv now just invokes wc_is_file_valid_csv. #32460 From 4624ab7ddebdc3e17331ea11db877a0b1751db1a Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 12 Apr 2022 11:33:12 +0300 Subject: [PATCH 173/206] adding filter comments --- .../includes/class-wc-download-handler.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index 789bc19ef97..e45084ad969 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -277,6 +277,13 @@ class WC_Download_Handler { // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { $file_path = ( is_ssl() ? 'https:' : 'http:' ) . $file_path; + + /** + * Filter the remote filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + */ return array( 'remote_file' => true, 'file_path' => apply_filters( 'woocommerce_download_parse_remote_file_path', $file_path ), @@ -297,7 +304,14 @@ class WC_Download_Handler { $remote_file = false; $file_path = $parsed_file_path['path']; } - + + /** + * Filter the filepath for download. + * + * @since 6.5.0 + * @param string $file_path File path. + * @param bool $remote_file Remote File Indicator. + */ return array( 'remote_file' => $remote_file, 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ) From 9c65d3bbdc51293ba457268ffe270386343e535e Mon Sep 17 00:00:00 2001 From: Ovidiu Liuta Date: Tue, 12 Apr 2022 12:24:12 +0300 Subject: [PATCH 174/206] phpcs fixes --- plugins/woocommerce/includes/class-wc-download-handler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/includes/class-wc-download-handler.php b/plugins/woocommerce/includes/class-wc-download-handler.php index e45084ad969..93e0cebad01 100644 --- a/plugins/woocommerce/includes/class-wc-download-handler.php +++ b/plugins/woocommerce/includes/class-wc-download-handler.php @@ -277,7 +277,7 @@ class WC_Download_Handler { // Paths that begin with '//' are always remote URLs. if ( '//' === substr( $file_path, 0, 2 ) ) { $file_path = ( is_ssl() ? 'https:' : 'http:' ) . $file_path; - + /** * Filter the remote filepath for download. * @@ -304,7 +304,7 @@ class WC_Download_Handler { $remote_file = false; $file_path = $parsed_file_path['path']; } - + /** * Filter the filepath for download. * @@ -314,7 +314,7 @@ class WC_Download_Handler { */ return array( 'remote_file' => $remote_file, - 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ) + 'file_path' => apply_filters( 'woocommerce_download_parse_file_path', $file_path, $remote_file ), ); } From 29a08dc51520fe952d2f9bf728038c94961e78b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=A9stor=20Soriano?= Date: Tue, 12 Apr 2022 11:38:59 +0200 Subject: [PATCH 175/206] Add changelog file for #32317. --- plugins/woocommerce/changelog/changelog-32317 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/changelog-32317 diff --git a/plugins/woocommerce/changelog/changelog-32317 b/plugins/woocommerce/changelog/changelog-32317 new file mode 100644 index 00000000000..3ab27b4b08d --- /dev/null +++ b/plugins/woocommerce/changelog/changelog-32317 @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Add woocommerce_download_parse_remote_file_path and woocommerce_download_parse_file_path filters to modify the parse_file_path method file_path return. #32317 \ No newline at end of file From 859ab8884b4b69d98e1a06927e4c88b313b14fb7 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Tue, 12 Apr 2022 18:06:09 +0800 Subject: [PATCH 176/206] Fix e2e tests --- .../src/pages/PaymentsSetup.ts | 17 +++++++-------- .../admin-e2e-tests/src/pages/WcSettings.ts | 16 +++++++++++++- .../src/specs/tasks/payment.ts | 21 ++++++++----------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts index 4b0720454ca..1dae7adc683 100644 --- a/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts +++ b/packages/js/admin-e2e-tests/src/pages/PaymentsSetup.ts @@ -25,8 +25,13 @@ export class PaymentsSetup extends BasePage { await this.clickButtonWithText( 'Got it' ); } - async toggleOtherPaymentMethods(): Promise< void > { - await this.clickButtonWithText( 'Other payment methods' ); + async showOtherPaymentMethods(): Promise< void > { + const selector = '.woocommerce-task-payments button.toggle-button'; + await this.page.waitForSelector( selector ); + const toggleButton = await this.page.$( + `${ selector }[aria-expanded=false]` + ); + await toggleButton?.click(); } async goToPaymentMethodSetup( @@ -45,14 +50,6 @@ export class PaymentsSetup extends BasePage { } } - async methodHasBeenSetup( method: PaymentMethod ): Promise< void > { - const selector = `.woocommerce-task-payment-${ method }`; - await this.page.waitForSelector( selector ); - expect( - await getElementByText( '*', 'Manage', selector ) - ).toBeDefined(); - } - async enableCashOnDelivery(): Promise< void > { await this.page.waitForSelector( '.woocommerce-task-payment-cod' ); await this.clickButtonWithText( 'Enable' ); diff --git a/packages/js/admin-e2e-tests/src/pages/WcSettings.ts b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts index ebf95a46033..0b5f78cf0a7 100644 --- a/packages/js/admin-e2e-tests/src/pages/WcSettings.ts +++ b/packages/js/admin-e2e-tests/src/pages/WcSettings.ts @@ -42,8 +42,22 @@ export class WcSettings extends BasePage { ); } + async paymentMethodIsEnabled( method = '' ): Promise< boolean > { + await this.navigate( 'checkout' ); + await waitForElementByText( 'h2', 'Payment methods' ); + const className = await getAttribute( + `tr[data-gateway_id=${ method }] .woocommerce-input-toggle`, + 'className' + ); + return ( + ( className as string ).indexOf( + 'woocommerce-input-toggle--disabled' + ) === -1 + ); + } + async cleanPaymentMethods(): Promise< void > { - this.navigate( 'checkout' ); + await this.navigate( 'checkout' ); await waitForElementByText( 'h2', 'Payment methods' ); const paymentMethods = await page.$$( 'span.woocommerce-input-toggle' ); for ( const method of paymentMethods ) { diff --git a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts index b017e0ac837..27052d002fc 100644 --- a/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts +++ b/packages/js/admin-e2e-tests/src/specs/tasks/payment.ts @@ -53,6 +53,7 @@ const testAdminPaymentSetupTask = () => { } ); it( 'Saving valid bank account transfer details enables the payment method', async () => { + await paymentsSetup.showOtherPaymentMethods(); await paymentsSetup.goToPaymentMethodSetup( 'bacs' ); await bankTransferSetup.saveAccountDetails( { accountNumber: '1234', @@ -62,13 +63,11 @@ const testAdminPaymentSetupTask = () => { iban: '12 3456 7890', swiftCode: 'ABBA', } ); - - await homeScreen.isDisplayed(); await waitForTimeout( 1000 ); - await homeScreen.clickOnTaskList( 'Set up payments' ); - await paymentsSetup.isDisplayed(); - await paymentsSetup.toggleOtherPaymentMethods(); - await paymentsSetup.methodHasBeenSetup( 'bacs' ); + expect( await settings.paymentMethodIsEnabled( 'bacs' ) ).toBe( + true + ); + await homeScreen.navigate(); } ); it( 'Enabling cash on delivery enables the payment method', async () => { @@ -78,14 +77,12 @@ const testAdminPaymentSetupTask = () => { await waitForTimeout( 1000 ); await homeScreen.clickOnTaskList( 'Set up payments' ); await paymentsSetup.isDisplayed(); - await paymentsSetup.toggleOtherPaymentMethods(); + await paymentsSetup.showOtherPaymentMethods(); await paymentsSetup.enableCashOnDelivery(); - await homeScreen.navigate(); - await homeScreen.isDisplayed(); await waitForTimeout( 1000 ); - await homeScreen.clickOnTaskList( 'Set up payments' ); - await paymentsSetup.isDisplayed(); - await paymentsSetup.methodHasBeenSetup( 'cod' ); + expect( await settings.paymentMethodIsEnabled( 'cod' ) ).toBe( + true + ); } ); } ); }; From db7b1ba035d55b3582ccebf4fa17f332384817c0 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 09:26:54 -0300 Subject: [PATCH 177/206] Add Pinterest to help panel --- .../woocommerce-admin/client/activity-panel/panels/help.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/woocommerce-admin/client/activity-panel/panels/help.js b/plugins/woocommerce-admin/client/activity-panel/panels/help.js index b13474e4f0b..a27469c37c1 100644 --- a/plugins/woocommerce-admin/client/activity-panel/panels/help.js +++ b/plugins/woocommerce-admin/client/activity-panel/panels/help.js @@ -100,6 +100,10 @@ function getMarketingItems( props ) { link: 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product#get-started', }, + activePlugins.includes( 'pinterest-for-woocommerce' ) && { + title: __( 'Set up Pinterest for WooCommerce', 'woocommerce' ), + link: 'https://woocommerce.com/products/pinterest-for-woocommerce/', + }, activePlugins.includes( 'mailchimp-for-woocommerce' ) && { title: __( 'Connect Mailchimp for WooCommerce', 'woocommerce' ), link: From 17ae456868af7e82b8a7ad67a627d865fa4db6ab Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 09:33:57 -0300 Subject: [PATCH 178/206] Fix text domain --- .../Admin/RemoteFreeExtensions/DefaultFreeExtensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index d5139a05118..5c80e61d5b5 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -125,7 +125,7 @@ class DefaultFreeExtensions { ], 'pinterest-for-woocommerce:alt' => [ 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), - 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce-admin' ), + 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ), 'image_url' => plugins_url( 'images/onboarding/pinterest.png', WC_ADMIN_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => false, From d4d70acfd103641d48bf36c43f2279fe36b926c4 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Tue, 12 Apr 2022 09:50:01 -0300 Subject: [PATCH 179/206] =?UTF-8?q?Enable=20=E2=80=9CSave=20changes?= =?UTF-8?q?=E2=80=9D=20in=20the=20admin=20variations=20form=20when=20textf?= =?UTF-8?q?ields=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legacy/js/admin/meta-boxes-product-variation.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js index f21f41a3066..79d3e7b322b 100644 --- a/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/legacy/js/admin/meta-boxes-product-variation.js @@ -350,7 +350,7 @@ jQuery( function( $ ) { .on( 'click','.downloadable_files a.delete', this.input_changed ); $( document.body ) - .on( 'change', '#variable_product_options .woocommerce_variations :input', this.input_changed ) + .on( 'change input', '#variable_product_options .woocommerce_variations :input', this.input_changed ) .on( 'change', '.variations-defaults select', this.defaults_changed ); var postForm = $( 'form#post' ); @@ -705,13 +705,18 @@ jQuery( function( $ ) { /** * Add new class when have changes in some input */ - input_changed: function() { + input_changed: function( event ) { $( this ) .closest( '.woocommerce_variation' ) .addClass( 'variation-needs-update' ); $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + // Do not trigger 'woocommerce_variations_input_changed' for 'input' events for backwards compat. + if ( 'input' === event.type && $( this ).is( ':text' ) ) { + return; + } + $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); }, From 566076de9b736b82bc95ab0aa610ae7a4fcfb959 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Tue, 12 Apr 2022 10:23:12 -0300 Subject: [PATCH 180/206] Add changelog --- plugins/woocommerce/changelog/issue-31347 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/issue-31347 diff --git a/plugins/woocommerce/changelog/issue-31347 b/plugins/woocommerce/changelog/issue-31347 new file mode 100644 index 00000000000..c732dbfa7c8 --- /dev/null +++ b/plugins/woocommerce/changelog/issue-31347 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Enable "Save changes" for variations on the admin when a textfield receives input. From d6c216fc7464da71843efc02c23cecccc90b84e9 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 10:37:49 -0300 Subject: [PATCH 181/206] Remove `welcome-modal.test.js --- .../tests/e2e/specs/admin-homescreen/welcome-modal.test.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js diff --git a/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js b/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js deleted file mode 100644 index c85d7381958..00000000000 --- a/plugins/woocommerce/tests/e2e/specs/admin-homescreen/welcome-modal.test.js +++ /dev/null @@ -1,5 +0,0 @@ -const { - testAdminHomescreenWelcomeModal, -} = require( '@woocommerce/admin-e2e-tests' ); - -testAdminHomescreenWelcomeModal(); From e219a19229ea1501020e2fa099e5d332743831b1 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 10:38:27 -0300 Subject: [PATCH 182/206] Renamed `testAdminHomescreenWelcomeModal` --- .../specs/activate-and-setup/complete-onboarding-wizard.ts | 4 ++-- .../activate-and-setup/complete-onboarding-wizard.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts index 5a8e48dd0b9..717cacf4186 100644 --- a/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts +++ b/packages/js/admin-e2e-tests/src/specs/activate-and-setup/complete-onboarding-wizard.ts @@ -597,7 +597,7 @@ const testBusinessDetailsForm = () => { } ); }; -const testAdminHomescreenWelcomeModal = () => { +const testAdminHomescreen = () => { describe( 'Homescreen', () => { const profileWizard = new OnboardingWizard( page ); const homeScreen = new WcHomescreen( page ); @@ -629,5 +629,5 @@ module.exports = { testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, - testAdminHomescreenWelcomeModal, + testAdminHomescreen, }; diff --git a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js index d69d0eb855a..28ca08ce731 100644 --- a/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js +++ b/plugins/woocommerce/tests/e2e/specs/activate-and-setup/complete-onboarding-wizard.test.js @@ -4,7 +4,7 @@ const { testDifferentStoreCurrenciesWCPay, testSubscriptionsInclusion, testBusinessDetailsForm, - testAdminHomescreenWelcomeModal, + testAdminHomescreen, } = require( '@woocommerce/admin-e2e-tests' ); const { withRestApi, IS_RETEST_MODE } = require( '@woocommerce/e2e-utils' ); @@ -18,4 +18,4 @@ testSelectiveBundleWCPay(); testDifferentStoreCurrenciesWCPay(); testSubscriptionsInclusion(); testBusinessDetailsForm(); -testAdminHomescreenWelcomeModal(); +testAdminHomescreen(); From 23b3250d1068fec57cd591ea075ce9e0276bd225 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 10:38:50 -0300 Subject: [PATCH 183/206] Add package changelog --- packages/js/admin-e2e-tests/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/js/admin-e2e-tests/CHANGELOG.md b/packages/js/admin-e2e-tests/CHANGELOG.md index d764841fc75..14fdbbcb34b 100644 --- a/packages/js/admin-e2e-tests/CHANGELOG.md +++ b/packages/js/admin-e2e-tests/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add E2E tests to disabled welcome modal #32505 + # 1.0.0 - Add returned type annotations and remove unused vars. #8020 From 9f025db18a3a8c1a7af2df3a4b83aacd8b2c774e Mon Sep 17 00:00:00 2001 From: barryhughes <3594411+barryhughes@users.noreply.github.com> Date: Tue, 12 Apr 2022 08:05:00 -0700 Subject: [PATCH 184/206] Adjust changelog entry. --- plugins/woocommerce/changelog/issue-31347 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/changelog/issue-31347 b/plugins/woocommerce/changelog/issue-31347 index c732dbfa7c8..2a5bacef909 100644 --- a/plugins/woocommerce/changelog/issue-31347 +++ b/plugins/woocommerce/changelog/issue-31347 @@ -1,4 +1,4 @@ Significance: patch Type: fix -Enable "Save changes" for variations on the admin when a textfield receives input. +Enable the "Save changes" button within the variations panel when a textfield receives input. From 97fca30b394d0075fc040920fc0d660e7e17c726 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 12:07:10 -0300 Subject: [PATCH 185/206] Fix fallback text --- .../Admin/RemoteFreeExtensions/DefaultFreeExtensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index 5c80e61d5b5..84fc8cc9351 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -104,7 +104,7 @@ class DefaultFreeExtensions { 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => sprintf( /* translators: 1: opening product link tag. 2: closing link tag */ - __( 'aaaa Inspire shoppers with %1$sPinterest for WooCommerce%2$s', 'woocommerce' ), + __( 'Inspire shoppers with %1$sPinterest for WooCommerce%2$s', 'woocommerce' ), '', '' ), From b85648a11744f491bb2f238adc694334c45c0789 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 12 Apr 2022 14:08:04 -0300 Subject: [PATCH 186/206] Fix fallback images URL --- .../DefaultFreeExtensions.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index 84fc8cc9351..202dc7f5982 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -78,7 +78,7 @@ class DefaultFreeExtensions { '', '' ), - 'image_url' => plugins_url( 'images/onboarding/google-listings-and-ads.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, 'is_visible' => [ @@ -96,7 +96,7 @@ class DefaultFreeExtensions { 'google-listings-and-ads:alt' => [ 'name' => __( 'Google Listings & Ads', 'woocommerce' ), 'description' => __( 'Reach more shoppers and drive sales for your store. Integrate with Google to list your products for free and launch paid ad campaigns.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/google-listings-and-ads.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/google-listings-and-ads.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fgoogle%2Fstart', 'is_built_by_wc' => true, ], @@ -108,7 +108,7 @@ class DefaultFreeExtensions { '', '' ), - 'image_url' => plugins_url( 'images/onboarding/pinterest.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=pinterest-for-woocommerce', 'is_visible' => [ [ @@ -126,28 +126,28 @@ class DefaultFreeExtensions { 'pinterest-for-woocommerce:alt' => [ 'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ), 'description' => __( 'Get your products in front of Pinterest users searching for ideas and things to buy. Get started with Pinterest and make your entire product catalog browsable.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/pinterest.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding', 'is_built_by_wc' => false, ], 'mailpoet' => [ 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailpoet.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ], 'mailchimp-for-woocommerce' => [ 'name' => __( 'Mailchimp', 'woocommerce' ), 'description' => __( 'Send targeted campaigns, recover abandoned carts and much more with Mailchimp.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailchimp-for-woocommerce.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailchimp-for-woocommerce.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailchimp-woocommerce', 'is_built_by_wc' => false, ], 'creative-mail-by-constant-contact' => [ 'name' => __( 'Creative Mail for WooCommerce', 'woocommerce' ), 'description' => __( 'Create on-brand store campaigns, fast email promotions and customer retargeting with Creative Mail.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/creative-mail-by-constant-contact.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/creative-mail-by-constant-contact.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=creativemail', 'is_built_by_wc' => false, ], @@ -481,7 +481,7 @@ class DefaultFreeExtensions { 'mailpoet:alt' => [ 'name' => __( 'MailPoet', 'woocommerce' ), 'description' => __( 'Create and send purchase follow-up emails, newsletters, and promotional campaigns straight from your dashboard.', 'woocommerce' ), - 'image_url' => plugins_url( 'images/onboarding/mailpoet.png', WC_ADMIN_PLUGIN_FILE ), + 'image_url' => plugins_url( '/assets/images/onboarding/mailpoet.png', WC_PLUGIN_FILE ), 'manage_url' => 'admin.php?page=mailpoet-newsletters', 'is_built_by_wc' => true, ], From 4b9c44ebb37e1b53519f048156ddb7dff53e8d27 Mon Sep 17 00:00:00 2001 From: Jacob Sewell Date: Tue, 12 Apr 2022 15:46:02 -0500 Subject: [PATCH 187/206] Docblock linter fixes. --- plugins/woocommerce/src/Admin/Notes/DataStore.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Notes/DataStore.php b/plugins/woocommerce/src/Admin/Notes/DataStore.php index ea1949979b8..49b14e500b3 100644 --- a/plugins/woocommerce/src/Admin/Notes/DataStore.php +++ b/plugins/woocommerce/src/Admin/Notes/DataStore.php @@ -326,7 +326,7 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter /** * Return an ordered list of notes. * - * @param array $args Query arguments. + * @param array $args Query arguments. * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return array An array of objects containing a note id. */ @@ -431,7 +431,7 @@ class DataStore extends \WC_Data_Store_WP implements \WC_Object_Data_Store_Inter * Applies woocommerce_note_where_clauses filter. * * @uses args_to_where_clauses - * @param array $args Array of args to pass. + * @param array $args Array of args to pass. * @param string $context Optional argument that the woocommerce_note_where_clauses filter can use to determine whether to apply extra conditions. Extensions should define their own contexts and use them to avoid adding to notes where clauses when not needed. * @return string Where clauses for the query. */ From 667d2a0be150d956606524b75e3ec5215fff0b57 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 12 Apr 2022 14:15:21 -0700 Subject: [PATCH 188/206] Add changelog file --- .../changelog/fix-32539-order-autodraft-validation | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-32539-order-autodraft-validation diff --git a/plugins/woocommerce/changelog/fix-32539-order-autodraft-validation b/plugins/woocommerce/changelog/fix-32539-order-autodraft-validation new file mode 100644 index 00000000000..d283595b90c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-32539-order-autodraft-validation @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Ensure that an existing order with auto-draft status won't be interpreted as pending when determining if the status has changed. From 42838bc2dab60969184bfaf28e09f297fa438dfa Mon Sep 17 00:00:00 2001 From: Christopher Allford Date: Tue, 12 Apr 2022 15:36:24 -0700 Subject: [PATCH 189/206] Added 6.4.0 Changelog --- changelog.txt | 98 ++++++++++++++++++++++++++++++++++ plugins/woocommerce/readme.txt | 2 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 2d055307fe0..ee3248e0e05 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,103 @@ == Changelog == += 6.4.0 2022-04-12 = + +**WooCommerce** + +- Add - Scaffolding for the custom orders table feature. ([#31692](https://github.com/woocommerce/woocommerce/pull/31692)) +- Add - Add DB table structure for custom order tables. ([#31811](https://github.com/woocommerce/woocommerce/pull/31811)) +- Add - Primary key for the product attributes lookup table. ([#32067](https://github.com/woocommerce/woocommerce/pull/32067)) +- Add - Tracks to the dashboard status widget and setup widget. ([#31857](https://github.com/woocommerce/woocommerce/pull/31857)) +- Add - Check around setup widget display when features are disabled. ([#31884](https://github.com/woocommerce/woocommerce/pull/31884)) +- Add - 'woocommerce_get_formatted_meta_data_include_all_meta_lines' filter hook. This can be used to control whether metadata lines are shown in the order meta box. ([#30948](https://github.com/woocommerce/woocommerce/pull/30948)) +- Enhancement - Introduce rate_limit_remaining column in the wc_rate_limits table. ([#32041](https://github.com/woocommerce/woocommerce/pull/32041)) +- Tweak - Update PayPal Standard JS used in the admin environment to avoid deprecated functionality. ([#32076](https://github.com/woocommerce/woocommerce/pull/32076)) +- Tweak - Change level of escaping used to render the CSV import error log. ([#32000](https://github.com/woocommerce/woocommerce/pull/32000)) +- Tweak - Make the payment_url field available via the REST API's orders endpoint. ([#31826](https://github.com/woocommerce/woocommerce/pull/31826)) +- Tweak - Rename WC_API_Exception code woocommerce_api_cannot_edit_product_catgory into woocommerce_api_cannot_edit_product_category ([#31785](https://github.com/woocommerce/woocommerce/pull/31785)) +- Tweak - Updated default email color to new Woo purple ([#30586](https://github.com/woocommerce/woocommerce/pull/30586)) +- Fix - Avoid depending on the presence of a theme header template to clear the cart after payment is made. ([#31877](https://github.com/woocommerce/woocommerce/pull/31877)) +- Fix - Payments tab tracking. ([#31844](https://github.com/woocommerce/woocommerce/pull/31844)) +- Fix - Remove unnecessary duplicate style in email-styles template. ([#31860](https://github.com/woocommerce/woocommerce/pull/31860)) +- Fix - incorrect position value for registering menu pages. ([#31779](https://github.com/woocommerce/woocommerce/pull/31779)) +- Fix - SZL currency symbol. Updated from 'L' to 'E'. ([#30602](https://github.com/woocommerce/woocommerce/pull/30602)) +- Fix - Removed execution of at least one hook ignoring the `woocommerce_load_webhooks_limit` filter value. ([#29002](https://github.com/woocommerce/woocommerce/pull/29002)) +- Dev - Added has_options() to REST API v3 product endpoint response. ([#32031](https://github.com/woocommerce/woocommerce/pull/32031)) +- Dev - Added woocommerce_admin_order_should_render_refunds hook to allow control over the refunds UI within the order editor. ([#31414](https://github.com/woocommerce/woocommerce/pull/31414)) + +**WooCommerce Admin - 3.3.0 & 3.3.1 & 3.3.2** + +- Add - Add asynchronous plugin install and activation endpoints ([#8079](https://github.com/woocommerce/woocommerce-admin/pull/8079)) +- Performance - Avoid expensive get_notes() queries in CouponPageMoved admin_init actions by using new Notes::get_note_by_name() helper method. ([#8202](https://github.com/woocommerce/woocommerce-admin/pull/8202)) +- Enhancement - Add chart color filter for overriding default chart colors. ([#8258](https://github.com/woocommerce/woocommerce-admin/pull/8258)) +- Enhancement - Added Typescript type declarations to build for @woocommerce/components ([#8282](https://github.com/woocommerce/woocommerce-admin/pull/8282)) +- Enhancement - Increase color selection limit to ten and add additional colors. ([#8258](https://github.com/woocommerce/woocommerce-admin/pull/8258)) +- Enhancement - Made @woocommerce/components/Stepper a Typescript file ([#8286](https://github.com/woocommerce/woocommerce-admin/pull/8286)) +- Enhancement - Prompts a modal to save any unsaved changes when the users try to move to a different step ([#8278](https://github.com/woocommerce/woocommerce-admin/pull/8278)) +- Tweak - OBW: Override Country/Region label line-height style to avoid truncated descenders. ([#8186](https://github.com/woocommerce/woocommerce-admin/pull/8186)) +- Tweak - Show single success message for theme install and activation ([#8236](https://github.com/woocommerce/woocommerce-admin/pull/8236)) +- Tweak - Use WC_VERSION as cache buster for assets ([#8308](https://github.com/woocommerce/woocommerce-admin/pull/8308)) +- Update - Adjust time range and add an image for the Jetpack Backup note. ([#8293](https://github.com/woocommerce/woocommerce-admin/pull/8293)) +- Update - Implement MailChimp API request threshold for MailchimpScheduler. ([#8342](https://github.com/woocommerce/woocommerce-admin/pull/8342)) +- Update - Reintroduce CES on product add, product update, and order update. ([#8238](https://github.com/woocommerce/woocommerce-admin/pull/8238)) +- Update - Replace mysql image with mariadb ([#8220](https://github.com/woocommerce/woocommerce-admin/pull/8220)) +- Update - Update country support list for WooCommerce Payments Task. ([#8517](https://github.com/woocommerce/woocommerce-admin/pull/8517)) +- Fix - Fix handling of paid themes in purchase task. ([#8493](https://github.com/woocommerce/woocommerce-admin/pull/8493)) +- Fix - Make sure the paid extension task is also shown for themes. ([#8412](https://github.com/woocommerce/woocommerce-admin/pull/8412)) +- Fix - Reintroduce emphasis on inbox note action button. ([#8411](https://github.com/woocommerce/woocommerce-admin/pull/8411)) +- Fix - Remove class ExtendedPayments. ([#8461](https://github.com/woocommerce/woocommerce-admin/pull/8461)) +- Fix - Added random IDs to SVG checkmarks in stepper component ([#8222](https://github.com/woocommerce/woocommerce-admin/pull/8222)) +- Fix - Fix Google Listings plugin is always shown in free features despite already activated. ([#8330](https://github.com/woocommerce/woocommerce-admin/pull/8330)) +- Fix - Fix hidden notes in `admin/notes` endpoint when the user is not in the tasklist experiment. ([#8328](https://github.com/woocommerce/woocommerce-admin/pull/8328)) +- Fix - Fix missing product name in variation analytic page for the deleted products. ([#8255](https://github.com/woocommerce/woocommerce-admin/pull/8255)) +- Fix - Fix payments extensions displayed below the offline payments options. ([#8232](https://github.com/woocommerce/woocommerce-admin/pull/8232)) +- Fix - Fix setup wizard title and flash of content ([#8201](https://github.com/woocommerce/woocommerce-admin/pull/8201)) +- Fix - Fix too many pending run_remote_notifications actions. ([#8285](https://github.com/woocommerce/woocommerce-admin/pull/8285)) +- Fix - Fix view logic for Setup additional payment providers task. ([#8391](https://github.com/woocommerce/woocommerce-admin/pull/8391)) +- Fix - OBW: fix copy on Business Details when "WooCommerce Shipping" is not listed ([#8324](https://github.com/woocommerce/woocommerce-admin/pull/8324)) +- Fix - Only add product data on REST requests and task list ([#8235](https://github.com/woocommerce/woocommerce-admin/pull/8235)) +- Fix - Stop showing actioned inbox items ([#8394](https://github.com/woocommerce/woocommerce-admin/pull/8394)) +- Fix - WC Payments task is not visible after installing the plugin ([#8514](https://github.com/woocommerce/woocommerce-admin/pull/8514)) +- Fix - PHP warning when default param is missing in payments spec. ([#8519](https://github.com/woocommerce/woocommerce-admin/pull/8519)) +- Dev - Added a test for tracks event recording for PaymentGatewaySuggestions ([#8306](https://github.com/woocommerce/woocommerce-admin/pull/8306)) +- Dev - Add README to hook reference generation script ([#8004](https://github.com/woocommerce/woocommerce-admin/pull/8004)) +- Dev - Add reset WooCommerce functionality to E2E tests, so tests have a fresh state. ([#8219](https://github.com/woocommerce/woocommerce-admin/pull/8219)) +- Dev - Enabled optional typescript checking on ./client subfolder ([#8372](https://github.com/woocommerce/woocommerce-admin/pull/8372)) +- Dev - Fix formatting and add filter param for changelog types for the testing instructions script. ([#8256](https://github.com/woocommerce/woocommerce-admin/pull/8256)) +- Dev - Refactor MerchantEmailNotifications ([#8304](https://github.com/woocommerce/woocommerce-admin/pull/8304)) +- Dev - Remove preloaded countries from data endpoints and use data store instead. ([#8380](https://github.com/woocommerce/woocommerce-admin/pull/8380)) +- Dev - Remove unused pre loaded setting data displaying all the routes. ([#8379](https://github.com/woocommerce/woocommerce-admin/pull/8379)) +- Dev - Remove unused task styling classes ([#8234](https://github.com/woocommerce/woocommerce-admin/pull/8234)) +- Dev - Update dependencies to support react 17 and drop support for IE11. ([#8305](https://github.com/woocommerce/woocommerce-admin/pull/8305)) +- Dev - Update task list data structure to better handle new designs. ([#8332](https://github.com/woocommerce/woocommerce-admin/pull/8332)) + +**WooCommerce Blocks - 7.2.0 & 7.2.1** + +- Enhancement - Add Global Styles support to the Product Price block. ([5950](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5950)) +- Enhancement - Add Global Styles support to the Add To Cart Button block. ([5816](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5816)) +- Enhancement - Store API - Introduced `wc/store/v1` namespace. ([5911](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5911)) +- Enhancement - Renamed WooCommerce block templates to more e-commerce related names. ([5935](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5935)) +- Enhancement - Featured Product block: Add the ability to reset to a previously set custom background image. ([5886](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5886)) +- Enhancement - Add a remove image button to the WooCommerce Feature Category block. ([5719](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5719)) +- Enhancement - Add support for the global style for the On-Sale Badge block. ([5565](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5565)) +- Enhancement - Add support for the global style for the Attribute Filter block. ([5557](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5557)) +- Enhancement - Category List block: Add support for global style. ([5516](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5516)) +- Fix - Fixed typo in `wooocommerce_store_api_validate_add_to_cart` and `wooocommerce_store_api_validate_cart_item` hook names. ([5926](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5926)) +- Fix - Fix loading WC core translations in locales where WC Blocks is not localized for some strings. ([5910](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5910)) +- Fix - Fixed an issue where clear customizations functionality was not working for WooCommerce templates. ([5746](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5746)) +- Fix - Fixed hover and focus states for button components. ([5712](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5712)) +- Fix - Add to Cart button on Products listing blocks will respect the "Redirect to the cart page after successful addition" setting. ([5708](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5708)) +- Fix - Fixes Twenty Twenty Two issues with sales price and added to cart "View Cart" call out styling in the "Products by Category" block. ([5684](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5684)) +- Fix - StoreAPI: Clear all wc notice types in the cart validation context [#5983](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5983) +- Fix - Don't trigger class deprecations notices if headers are already sent [#6074](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/6074) +- Various - Remove v1 string from Store Keys. ([5987](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5987)) +- Various - Introduce the `InvalidCartException` for handling cart validation. ([5904](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5904)) +- Various - Renamed Store API custom headers to remove `X-WC-Store-API` prefixes. [#5983](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5983) +- Various - Normalised Store API error codes [#5992](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/5992) +- Various - Deprecated `woocommerce_blocks_checkout_order_processed` in favour of `woocommerce_store_api_checkout_order_processed` +- Various - Deprecated `woocommerce_blocks_checkout_update_order_meta` in favour of `woocommerce_store_api_checkout_update_order_meta` +- Various - Deprecated `woocommerce_blocks_checkout_update_order_from_request` in favour of `woocommerce_store_api_checkout_update_order_from_request` + = 6.3.1 2022-03-10 = **WooCommerce** diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index 5fe323beb36..f6abd5a8f09 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -160,6 +160,6 @@ WooCommerce comes with some sample data you can use to see how products look; im == Changelog == -= 6.4.0 2022-XX-XX = += 6.5.0 2022-XX-XX = [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). From 372ad23cc82f79de5a8f8518f1735e752ac53eb0 Mon Sep 17 00:00:00 2001 From: Christopher Allford Date: Tue, 12 Apr 2022 15:37:05 -0700 Subject: [PATCH 190/206] Updated Version & Stable Tag --- plugins/woocommerce/includes/class-woocommerce.php | 2 +- plugins/woocommerce/package.json | 2 +- plugins/woocommerce/readme.txt | 2 +- plugins/woocommerce/woocommerce.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index 5970ed38b90..92a51a6bf57 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -29,7 +29,7 @@ final class WooCommerce { * * @var string */ - public $version = '6.4.0'; + public $version = '6.5.0'; /** * WooCommerce Schema version. diff --git a/plugins/woocommerce/package.json b/plugins/woocommerce/package.json index c619fef5231..ea6e35665c2 100644 --- a/plugins/woocommerce/package.json +++ b/plugins/woocommerce/package.json @@ -1,7 +1,7 @@ { "name": "woocommerce", "title": "WooCommerce", - "version": "6.4.0", + "version": "6.5.0", "homepage": "https://woocommerce.com/", "repository": { "type": "git", diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index f6abd5a8f09..63cfa9ee1dc 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d Requires at least: 5.7 Tested up to: 5.9 Requires PHP: 7.0 -Stable tag: 6.3.0 +Stable tag: 6.4.0 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html diff --git a/plugins/woocommerce/woocommerce.php b/plugins/woocommerce/woocommerce.php index 62470bec195..e80a3779ba3 100644 --- a/plugins/woocommerce/woocommerce.php +++ b/plugins/woocommerce/woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. - * Version: 6.4.0-dev + * Version: 6.5.0-dev * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce From 68efbafcb5f8f85fc4e78f1dc9fb892b8b517a60 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 21 Mar 2022 14:04:15 -0300 Subject: [PATCH 191/206] Add sectioned task list component --- packages/js/data/src/onboarding/types.ts | 11 + .../woocommerce-admin/client/tasks/tasks.tsx | 3 + .../headers/section-header.scss | 12 + .../headers/section-header.tsx | 28 ++ .../two-column-tasks/sectioned-task-list.scss | 77 +++++ .../two-column-tasks/sectioned-task-list.tsx | 273 ++++++++++++++++++ .../task_list/basics-section-illustration.png | Bin 0 -> 63845 bytes .../task_list/expand-section-illustration.png | Bin 0 -> 48357 bytes .../task_list/sales-section-illustration.png | Bin 0 -> 105347 bytes .../woocommerce/client/admin/config/core.json | 3 +- .../client/admin/config/development.json | 3 +- .../client/admin/config/plugin.json | 3 +- .../Admin/Features/OnboardingTasks/Task.php | 10 + .../Features/OnboardingTasks/TaskList.php | 40 ++- .../OnboardingTasks/TaskListSection.php | 122 ++++++++ .../Features/OnboardingTasks/TaskLists.php | 58 +++- .../OnboardingTasks/Tasks/Appearance.php | 6 + .../OnboardingTasks/Tasks/Marketing.php | 6 + .../OnboardingTasks/Tasks/Payments.php | 6 + .../OnboardingTasks/Tasks/Products.php | 6 + .../OnboardingTasks/Tasks/Shipping.php | 6 + .../OnboardingTasks/Tasks/StoreCreation.php | 79 +++++ .../Features/OnboardingTasks/Tasks/Tax.php | 6 + 23 files changed, 753 insertions(+), 5 deletions(-) create mode 100644 plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss create mode 100644 plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx create mode 100644 plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss create mode 100644 plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx create mode 100644 plugins/woocommerce/assets/images/task_list/basics-section-illustration.png create mode 100644 plugins/woocommerce/assets/images/task_list/expand-section-illustration.png create mode 100644 plugins/woocommerce/assets/images/task_list/sales-section-illustration.png create mode 100644 plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php create mode 100644 plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php diff --git a/packages/js/data/src/onboarding/types.ts b/packages/js/data/src/onboarding/types.ts index 39e0387ae6a..8685d211fbd 100644 --- a/packages/js/data/src/onboarding/types.ts +++ b/packages/js/data/src/onboarding/types.ts @@ -9,12 +9,22 @@ export type TaskType = { isSnoozed: boolean; isVisible: boolean; isSnoozable: boolean; + isDisabled: boolean; snoozedUntil: number; time: string; title: string; isVisited: boolean; }; +export type TaskListSection = { + id: string; + title: string; + description: string; + image: string; + tasks: string[]; + isComplete: boolean; +}; + export type TaskListType = { id: string; isCollapsible?: boolean; @@ -25,4 +35,5 @@ export type TaskListType = { title: string; eventPrefix: string; displayProgressHeader: boolean; + sections?: TaskListSection[]; }; diff --git a/plugins/woocommerce-admin/client/tasks/tasks.tsx b/plugins/woocommerce-admin/client/tasks/tasks.tsx index 6d450e63a43..5ca3fba71fc 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.tsx +++ b/plugins/woocommerce-admin/client/tasks/tasks.tsx @@ -19,6 +19,7 @@ import { TasksPlaceholder, TasksPlaceholderProps } from './placeholder'; import './tasks.scss'; import { TaskListProps, TaskList } from './task-list'; import { TaskList as TwoColumnTaskList } from '../two-column-tasks/task-list'; +import { SectionedTaskList } from '../two-column-tasks/sectioned-task-list'; import TwoColumnTaskListPlaceholder from '../two-column-tasks/placeholder'; import '../two-column-tasks/style.scss'; import { getAdminSetting } from '~/utils/admin-settings'; @@ -31,6 +32,8 @@ function getTaskListComponent( taskListId: string ): React.FC< TaskListProps > { switch ( taskListId ) { case 'setup_experiment_1': return TwoColumnTaskList; + case 'setup_experiment_2': + return SectionedTaskList; default: return TaskList; } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss new file mode 100644 index 00000000000..9de47b67c61 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -0,0 +1,12 @@ +.woocommerce-task-section-header__container { + display: flex; + + .illustration-background { + max-width: 150px; + margin-left: $gap; + } + + .woocommerce-task-header__contents p { + font-size: 16px; + } +} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx new file mode 100644 index 00000000000..99960e4c2d1 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import './section-header.scss'; + +type Props = { + title: string; + description: string; + image: string; +}; + +const SectionHeader: React.FC< Props > = ( { title, description, image } ) => { + return ( +
+
+

{ title }

+

{ description }

+
+ { +
+ ); +}; + +export default SectionHeader; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss new file mode 100644 index 00000000000..98cc835c70c --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -0,0 +1,77 @@ +.woocommerce-sectioned-task-list { + .components-panel { + width: 100%; + background: transparent; + border: 0; + } + + .components-panel__body { + padding-bottom: 0; + margin-bottom: $gap-smaller; + background: #fff; + border: 1px solid $gray-200; + + .components-panel__body-title { + margin-bottom: 0; + border-bottom: 1px solid #e0e0e0; + + > .components-button { + font-size: 20px; + font-weight: 400; + padding-top: 20px; + padding-bottom: 20px; + } + } + .wooocommerce-task-card__header-container { + width: 100%; + border-bottom: none; + } + .components-panel__body-toggle { + box-shadow: none; + } + &.is-opened .components-panel__body-toggle { + width: 100%; + padding: 0; + > span { + position: absolute; + right: 0; + top: $gap-large; + } + } + + .woocommerce-experimental-list { + width: calc(100% + 32px); + margin: 0 -16px; + } + } + ul li.woocommerce-task-list__item { + padding-top: $gap; + padding-bottom: $gap; + + &.task-disabled { + pointer-events: none; + } + } + + .woocommerce-task-list__item.complete .woocommerce-task__icon { + background-color: $alert-green; + } + + .components-panel__body-title { + .woocommerce-badge { + width: 28px; + height: 28px; + } + .woocommerce-task__icon { + margin-left: $gap-smaller; + background-color: $alert-green; + border-radius: 50%; + width: 24px; + height: 24px; + svg { + fill: #fff; + position: relative; + } + } + } +} diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx new file mode 100644 index 00000000000..ad1a608b98b --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx @@ -0,0 +1,273 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef, useState } from '@wordpress/element'; +import { Panel, PanelBody, PanelRow } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Icon, check } from '@wordpress/icons'; +import { updateQueryString } from '@woocommerce/navigation'; +import { Badge } from '@woocommerce/components'; +import { + OPTIONS_STORE_NAME, + ONBOARDING_STORE_NAME, + TaskType, + TaskListSection, +} from '@woocommerce/data'; +import { recordEvent } from '@woocommerce/tracks'; +import { List, TaskItem, Text } from '@woocommerce/experimental'; +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import '../tasks/task-list.scss'; +import './sectioned-task-list.scss'; +import TaskListCompleted from './completed'; +import { TaskListProps } from '~/tasks/task-list'; +import SectionHeader from './headers/section-header'; + +type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { + title: string | React.ReactNode | undefined; + onToggle?: ( isOpen: boolean ) => void; +}; +const PanelBodyWithUpdatedType = PanelBody as React.ComponentType< PanelBodyProps >; + +export const SectionedTaskList: React.FC< TaskListProps > = ( { + query, + id, + eventName, + tasks, + keepCompletedTaskList, + isComplete, + sections, +} ) => { + const { createNotice } = useDispatch( 'core/notices' ); + const { updateOptions, dismissTask, undoDismissTask } = useDispatch( + OPTIONS_STORE_NAME + ); + const { profileItems } = useSelect( ( select ) => { + const { getProfileItems } = select( ONBOARDING_STORE_NAME ); + return { + profileItems: getProfileItems(), + }; + } ); + const { hideTaskList } = useDispatch( ONBOARDING_STORE_NAME ); + const [ openPanel, setOpenPanel ] = useState< string | null >( + sections?.find( ( section ) => ! section.isComplete )?.id || null + ); + + const prevQueryRef = useRef( query ); + + const nowTimestamp = Date.now(); + const visibleTasks = tasks.filter( + ( task ) => + ! task.isDismissed && + ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) + ); + + const recordTaskListView = () => { + if ( query.task ) { + return; + } + + recordEvent( `${ eventName }_view`, { + number_tasks: visibleTasks.length, + store_connected: profileItems.wccom_connected, + } ); + }; + + useEffect( () => { + recordTaskListView(); + }, [] ); + + useEffect( () => { + const { task: prevTask } = prevQueryRef.current; + const { task } = query; + + if ( prevTask !== task ) { + window.document.documentElement.scrollTop = 0; + prevQueryRef.current = query; + } + }, [ query ] ); + + const onDismissTask = ( taskId: string, onDismiss?: () => void ) => { + dismissTask( taskId ); + createNotice( 'success', __( 'Task dismissed' ), { + actions: [ + { + label: __( 'Undo', 'woocommerce-admin' ), + onClick: () => undoDismissTask( taskId ), + }, + ], + } ); + + if ( onDismiss ) { + onDismiss(); + } + }; + + const hideTasks = () => { + hideTaskList( id ); + }; + + const keepTasks = () => { + const updateOptionsParams = { + woocommerce_task_list_keep_completed: 'yes', + }; + + updateOptions( { + ...updateOptionsParams, + } ); + }; + + let selectedHeaderCard = visibleTasks.find( + ( listTask ) => listTask.isComplete === false + ); + + // If nothing is selected, default to the last task since everything is completed. + if ( ! selectedHeaderCard ) { + selectedHeaderCard = visibleTasks[ visibleTasks.length - 1 ]; + } + + const trackClick = ( task: TaskType ) => { + recordEvent( `${ eventName }_click`, { + task_name: task.id, + } ); + }; + + const goToTask = ( task: TaskType ) => { + trackClick( task ); + updateQueryString( { task: task.id } ); + }; + + const onTaskSelected = ( task: TaskType ) => { + goToTask( task ); + }; + + const getSectionTasks = ( sectionTaskIds: string[] ) => { + return visibleTasks.filter( ( task ) => + sectionTaskIds.includes( task.id ) + ); + }; + + const getPanelTitle = ( section: TaskListSection ) => { + return openPanel === section.id ? ( +
+
+ +
+
+ ) : ( + <> + + { section.title } + + + { section.isComplete && ( +
+ +
+ ) } + + ); + }; + + if ( ! visibleTasks.length ) { + return
; + } + + if ( isComplete && ! keepCompletedTaskList ) { + return ( + <> + + + ); + } + + return ( + <> +
+ + { ( sections || [] ).map( ( section ) => ( + { + if ( ! isOpen && openPanel === section.id ) { + setOpenPanel( null ); + } else { + setOpenPanel( section.id ); + } + } } + initialOpen={ false } + > + + + { getSectionTasks( section.tasks ).map( + ( task ) => { + const className = classnames( + 'woocommerce-task-list__item', + { + complete: task.isComplete, + 'task-disabled': + task.isDisabled, + } + ); + return ( + { + if ( + ! task.isDisabled + ) { + onTaskSelected( + task + ); + } + } } + onDismiss={ + task.isDismissable + ? () => + onDismissTask( + task.id + ) + : undefined + } + action={ () => {} } + actionLabel={ + task.actionLabel + } + /> + ); + } + ) } + + + + ) ) } + +
+ + ); +}; + +export default SectionedTaskList; diff --git a/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..f598d83ba4306c27ecb1bb2d4b0c1fb13dbf8199 GIT binary patch literal 63845 zcmbqa1y`F*)5codDHOL*+$rwv?p~x6cPA8Rk+ir|thg0-3q^{%yA%l!2ohXB`n*5k z+jH)dlRGCnGrQN$UOO`j(o|Q##vsE$KtRA&Qk2z3KtMcyd5Y0qzr2Zzea-#yf$pYg z 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 JS4*0N{2wq<5N-ef literal 0 HcmV?d00001 diff --git a/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..816e87a7f595d7efcbbde1b7d0716c1050a14ea1 GIT binary patch literal 105347 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 $this->is_snoozed(), 'isSnoozeable' => $this->is_snoozeable(), 'isVisited' => $this->is_visited(), + 'isDisabled' => $this->is_disabled(), 'snoozedUntil' => $this->get_snoozed_until(), 'additionalData' => self::convert_object_to_camelcase( $this->get_additional_data() ), 'eventPrefix' => $this->prefix_event( '' ), diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php index 1e016981412..4053546fc9a 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskList.php @@ -96,6 +96,20 @@ class TaskList { */ public $options = array(); + /** + * Array of TaskListSection. + * + * @var array + */ + private $sections = array(); + + /** + * Key value map of task class and id used for sections. + * + * @var array + */ + public $task_class_id_map = array(); + /** * Constructor * @@ -112,6 +126,7 @@ class TaskList { 'options' => array(), 'visible' => true, 'display_progress_header' => false, + 'sections' => array(), ); $data = wp_parse_args( $data, $defaults ); @@ -132,6 +147,12 @@ class TaskList { } $this->possibly_remove_reminder_bar(); + $this->sections = array_map( + function( $section ) { + return new TaskListSection( $section, $this ); + }, + $data['sections'] + ); } /** @@ -243,7 +264,9 @@ class TaskList { return; } - $this->tasks[] = $task; + $task_class_name = substr( get_class( $task ), strrpos( get_class( $task ), '\\' ) + 1 ); + $this->task_class_id_map[ $task_class_name ] = $task->get_id(); + $this->tasks[] = $task; } /** @@ -279,6 +302,15 @@ class TaskList { ); } + /** + * Get task list sections. + * + * @return array + */ + public function get_sections() { + return $this->sections; + } + /** * Track list completion of viewable tasks. */ @@ -377,6 +409,12 @@ class TaskList { 'eventPrefix' => $this->prefix_event( '' ), 'displayProgressHeader' => $this->display_progress_header, 'keepCompletedTaskList' => $this->get_keep_completed_task_list(), + 'sections' => array_map( + function( $section ) { + return $section->get_json(); + }, + $this->sections + ), ); } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php new file mode 100644 index 00000000000..daeca101342 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskListSection.php @@ -0,0 +1,122 @@ + '', + 'title' => '', + 'description' => '', + 'image' => '', + 'tasks' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + $this->task_list = $task_list; + $this->id = $data['id']; + $this->title = $data['title']; + $this->description = $data['description']; + $this->image = $data['image']; + $this->task_names = $data['task_names']; + } + + /** + * Returns if section is complete. + * + * @return boolean; + */ + private function is_complete() { + $complete = true; + foreach ( $this->task_names as $task_name ) { + if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) { + $task = $this->task_list->get_task( $this->task_list->task_class_id_map[ $task_name ] ); + if ( ! $task->is_complete() ) { + $complete = false; + break; + } + } + } + return $complete; + } + + /** + * Get the list for use in JSON. + * + * @return array + */ + public function get_json() { + return array( + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'image' => $this->image, + 'tasks' => array_map( + function( $task_name ) { + if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) { + return $this->task_list->task_class_id_map[ $task_name ]; + } + return ''; + }, + $this->task_names + ), + 'isComplete' => $this->is_complete(), + ); + } +} diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 8363250cd36..d9448c4d237 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -93,7 +93,7 @@ class TaskLists { 'Appearance', ), 'event_prefix' => 'tasklist_', - 'visible' => ! Features::is_enabled( 'tasklist-setup-experiment-1' ), + 'visible' => ! Features::is_enabled( 'tasklist-setup-experiment-1' ) && ! Features::is_enabled( 'tasklist-setup-experiment-2' ), ) ); @@ -121,6 +121,62 @@ class TaskLists { ) ); + self::add_list( + array( + 'id' => 'setup_experiment_2', + 'hidden_id' => 'setup', + 'title' => __( 'Get ready to start selling', 'woocommerce-admin' ), + 'tasks' => array( + 'StoreCreation', + 'StoreDetails', + 'Products', + 'WooCommercePayments', + 'Payments', + 'Tax', + 'Shipping', + 'Marketing', + 'Appearance', + ), + 'event_prefix' => 'tasklist_', + 'visible' => Features::is_enabled( 'tasklist-setup-experiment-2' ), + 'options' => array( + 'use_completed_title' => true, + ), + 'sections' => array( + array( + 'id' => 'basics', + 'title' => __( 'Cover the basics', 'woocommerce-admin' ), + 'description' => __( 'Make sure you’ve got everything you need to start selling—from business details to products.', 'woocommerce-admin' ), + 'image' => plugins_url( + '/images/task_list/basics-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'StoreCreation', 'StoreDetails', 'Products', 'Payments', 'WooCommercePayments' ), + ), + array( + 'id' => 'sales', + 'title' => __( 'Get ready to sell', 'woocommerce-admin' ), + 'description' => __( 'Easily set up the backbone of your store’s operations and get ready to accept first orders.', 'woocommerce-admin' ), + 'image' => plugins_url( + '/images/task_list/sales-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'Shipping', 'Tax' ), + ), + array( + 'id' => 'expand', + 'title' => __( 'Customize & expand', 'woocommerce-admin' ), + 'description' => __( 'Personalize your store’s design and grow your business by enabling new sales channels.', 'woocommerce-admin' ), + 'image' => plugins_url( + '/images/task_list/expand-section-illustration.png', + WC_ADMIN_PLUGIN_FILE + ), + 'task_names' => array( 'Appearance', 'Marketing' ), + ), + ), + ) + ); + self::add_list( array( 'id' => 'extended', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index 21db5d4aa30..65833b4ec06 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -39,6 +39,9 @@ class Appearance extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Make your store stand out with unique design', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You personalized your store', 'woocommerce' ); @@ -54,6 +57,9 @@ class Appearance extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Upload your logo to adapt the store to your brand’s personality.', 'woocommerce-admin' ); + } return __( 'Add your logo, create a homepage, and start designing your store.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php index 59dc41f51c0..3df6744bdc2 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php @@ -25,6 +25,9 @@ class Marketing extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Grow your business with marketing tools', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added sales channels', 'woocommerce' ); @@ -40,6 +43,9 @@ class Marketing extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce-admin' ); + } return __( 'Add recommended marketing tools to reach new customers and grow your business', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 9d7029f74f0..360dcf58dd4 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -25,6 +25,9 @@ class Payments extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Add a way to get paid', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You set up payments', 'woocommerce' ); @@ -40,6 +43,9 @@ class Payments extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Let your customers pay the way they like.', 'woocommerce-admin' ); + } return __( 'Choose payment providers and enable payment methods at checkout.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index d3e657d5ba6..3e05b231d5d 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -36,6 +36,9 @@ class Products extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Create or upload your first products', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added products', 'woocommerce' ); @@ -51,6 +54,9 @@ class Products extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Add products to sell and build your catalog.', 'woocommerce-admin' ); + } return __( 'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.', 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php index 1a3adb553a6..32ed4085dac 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php @@ -24,6 +24,9 @@ class Shipping extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Select how to ship your products', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added shipping costs', 'woocommerce' ); @@ -39,6 +42,9 @@ class Shipping extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce-admin' ); + } return __( "Set your store location and where you'll ship to.", 'woocommerce' diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php new file mode 100644 index 00000000000..f7afbc4b830 --- /dev/null +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php @@ -0,0 +1,79 @@ +is_complete() ) { + /* translators: Store name */ + return sprintf( __( 'You created %s', 'woocommerce-admin' ), get_bloginfo( 'name' ) ); + } + } + + /** + * Content. + * + * @return string + */ + public function get_content() { + return ''; + } + + /** + * Time. + * + * @return string + */ + public function get_time() { + return ''; + } + + /** + * Time. + * + * @return string + */ + public function get_action_url() { + return ''; + } + + /** + * Task completion. + * + * @return bool + */ + public function is_complete() { + return true; + } + + /** + * Check if task is disabled. + * + * @return bool + */ + public function is_disabled() { + return true; + } +} + diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index 2f23894023f..7368b147da7 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -65,6 +65,9 @@ class Tax extends Task { * @return string */ public function get_title() { + if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { + return __( 'Get taxes out of your mind', 'woocommerce-admin' ); + } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { return __( 'You added tax rates', 'woocommerce' ); @@ -80,6 +83,9 @@ class Tax extends Task { * @return string */ public function get_content() { + if ( count( $this->task_list->get_sections() ) > 0 ) { + return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce-admin' ); + } return self::can_use_automated_taxes() ? __( 'Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.', From dd8380820bbb6aeefc88e5b22097b924401b454e Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 21 Mar 2022 14:41:46 -0300 Subject: [PATCH 192/206] Updated image urls --- .../src/Admin/Features/OnboardingTasks/TaskLists.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index d9448c4d237..5a407a2b25a 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -148,7 +148,7 @@ class TaskLists { 'title' => __( 'Cover the basics', 'woocommerce-admin' ), 'description' => __( 'Make sure you’ve got everything you need to start selling—from business details to products.', 'woocommerce-admin' ), 'image' => plugins_url( - '/images/task_list/basics-section-illustration.png', + '/assets/images/task_list/basics-section-illustration.png', WC_ADMIN_PLUGIN_FILE ), 'task_names' => array( 'StoreCreation', 'StoreDetails', 'Products', 'Payments', 'WooCommercePayments' ), @@ -158,7 +158,7 @@ class TaskLists { 'title' => __( 'Get ready to sell', 'woocommerce-admin' ), 'description' => __( 'Easily set up the backbone of your store’s operations and get ready to accept first orders.', 'woocommerce-admin' ), 'image' => plugins_url( - '/images/task_list/sales-section-illustration.png', + '/assets/images/task_list/sales-section-illustration.png', WC_ADMIN_PLUGIN_FILE ), 'task_names' => array( 'Shipping', 'Tax' ), @@ -168,7 +168,7 @@ class TaskLists { 'title' => __( 'Customize & expand', 'woocommerce-admin' ), 'description' => __( 'Personalize your store’s design and grow your business by enabling new sales channels.', 'woocommerce-admin' ), 'image' => plugins_url( - '/images/task_list/expand-section-illustration.png', + '/assets/images/task_list/expand-section-illustration.png', WC_ADMIN_PLUGIN_FILE ), 'task_names' => array( 'Appearance', 'Marketing' ), From df0acb92864222b246cf2e6923a8750956209778 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 21 Mar 2022 16:20:58 -0300 Subject: [PATCH 193/206] Add changelog --- .../changelogs/feature-32164_new_task_list_version_2 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 diff --git a/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 b/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 new file mode 100644 index 00000000000..77b6ac86e61 --- /dev/null +++ b/plugins/woocommerce-admin/changelogs/feature-32164_new_task_list_version_2 @@ -0,0 +1,4 @@ +Significance: minor +Type: Add + +Add support for sections in our TaskList class and create a new sectional task list component. #32302 From 7bdaea6195a1bbc7b61e427e1474261d6531491b Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 24 Mar 2022 16:56:27 -0300 Subject: [PATCH 194/206] Add progress header to sectioned task list and made some additional little changes --- .../progress-header/progress-header.tsx | 60 ++++++++++--------- .../two-column-tasks/sectioned-task-list.tsx | 5 ++ .../OnboardingTasks/TaskListSection.php | 2 +- .../Features/OnboardingTasks/TaskLists.php | 1 + 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx index aa541e27db4..fa97af5ba9d 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx +++ b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx @@ -20,37 +20,43 @@ type ProgressHeaderProps = { export const ProgressHeader: React.FC< ProgressHeaderProps > = ( { taskListId, } ) => { - const { loading, tasksCount, completedCount, hasVisitedTasks } = useSelect( - ( select ) => { - const taskList: TaskListType = select( - ONBOARDING_STORE_NAME - ).getTaskList( taskListId ); - const finishedResolution = select( - ONBOARDING_STORE_NAME - ).hasFinishedResolution( 'getTaskList', [ taskListId ] ); - const nowTimestamp = Date.now(); - const visibleTasks = taskList?.tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const { + loading, + tasksCount, + completedCount, + hasVisitedTasks, + disabledCompletedCount, + } = useSelect( ( select ) => { + const taskList: TaskListType = select( + ONBOARDING_STORE_NAME + ).getTaskList( taskListId ); + const finishedResolution = select( + ONBOARDING_STORE_NAME + ).hasFinishedResolution( 'getTaskList', [ taskListId ] ); + const nowTimestamp = Date.now(); + const visibleTasks = taskList?.tasks.filter( + ( task ) => + ! task.isDismissed && + ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) + ); - return { - loading: ! finishedResolution, - tasksCount: visibleTasks?.length, - completedCount: visibleTasks?.filter( - ( task ) => task.isComplete - ).length, - hasVisitedTasks: - visibleTasks?.filter( ( task ) => task.isVisited ).length > - 0, - }; - } - ); + return { + loading: ! finishedResolution, + tasksCount: visibleTasks?.length, + completedCount: visibleTasks?.filter( ( task ) => task.isComplete ) + .length, + disabledCompletedCount: visibleTasks?.filter( + ( task ) => task.isComplete && task.isDisabled + ).length, + hasVisitedTasks: + visibleTasks?.filter( ( task ) => task.isVisited ).length > 0, + }; + } ); const progressTitle = useMemo( () => { if ( - ( ! hasVisitedTasks && completedCount < 2 ) || + ( ! hasVisitedTasks && + completedCount < 2 + disabledCompletedCount ) || completedCount === tasksCount ) { const siteTitle = getSetting( 'siteTitle' ); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx index ad1a608b98b..2d5f354191d 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx @@ -26,6 +26,7 @@ import './sectioned-task-list.scss'; import TaskListCompleted from './completed'; import { TaskListProps } from '~/tasks/task-list'; import SectionHeader from './headers/section-header'; +import { ProgressHeader } from '~/task-lists/progress-header'; type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { title: string | React.ReactNode | undefined; @@ -41,6 +42,7 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { keepCompletedTaskList, isComplete, sections, + displayProgressHeader, } ) => { const { createNotice } = useDispatch( 'core/notices' ); const { updateOptions, dismissTask, undoDismissTask } = useDispatch( @@ -191,6 +193,9 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { return ( <> + { displayProgressHeader ? ( + + ) : null }

task_names as $task_name ) { if ( null !== $this->task_list && isset( $this->task_list->task_class_id_map[ $task_name ] ) ) { $task = $this->task_list->get_task( $this->task_list->task_class_id_map[ $task_name ] ); - if ( ! $task->is_complete() ) { + if ( $task->can_view() && ! $task->is_complete() ) { $complete = false; break; } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 5a407a2b25a..82834c773b4 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -142,6 +142,7 @@ class TaskLists { 'options' => array( 'use_completed_title' => true, ), + 'display_progress_header' => true, 'sections' => array( array( 'id' => 'basics', From fd303232888c9ca39309a22064f03687afcc27a8 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Fri, 1 Apr 2022 10:53:17 -0300 Subject: [PATCH 195/206] Fix section title and list alignment --- .../two-column-tasks/headers/section-header.scss | 6 ++++++ .../client/two-column-tasks/sectioned-task-list.scss | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss index 9de47b67c61..48b5dad3996 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -9,4 +9,10 @@ .woocommerce-task-header__contents p { font-size: 16px; } + + .woocommerce-task-header__contents h1 { + font-size: 20px; + line-height: 28px; + padding: 0px; + } } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss index 98cc835c70c..376a1a060ea 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -21,6 +21,10 @@ padding-top: 20px; padding-bottom: 20px; } + + .components-panel__arrow { + right: $gap-large; + } } .wooocommerce-task-card__header-container { width: 100%; @@ -28,14 +32,13 @@ } .components-panel__body-toggle { box-shadow: none; + padding-left: $gap-large; } &.is-opened .components-panel__body-toggle { width: 100%; padding: 0; - > span { - position: absolute; - right: 0; - top: $gap-large; + .components-panel__arrow { + top: 32px; } } From 954be3997dea304cff7c92e0c3ce205ab52250f8 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Fri, 1 Apr 2022 11:05:47 -0300 Subject: [PATCH 196/206] Realign header image --- .../client/two-column-tasks/headers/section-header.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss index 48b5dad3996..19c8c274cc5 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -3,7 +3,8 @@ .illustration-background { max-width: 150px; - margin-left: $gap; + margin-left: auto; + margin-right: 44px; } .woocommerce-task-header__contents p { From dfa342f400ac5c918fbef47b940393fb9ced27a9 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Fri, 1 Apr 2022 11:12:51 -0300 Subject: [PATCH 197/206] Update unchecked checkbox border for task list --- .../client/two-column-tasks/sectioned-task-list.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss index 376a1a060ea..d7e5365598b 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -54,6 +54,9 @@ &.task-disabled { pointer-events: none; } + &:not(.complete) .woocommerce-task-list__item-before .woocommerce-task__icon { + border-color: $gray-300; + } } .woocommerce-task-list__item.complete .woocommerce-task__icon { From 7547901e7de59f05c3fa2ad9f228d8cca1670193 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Fri, 1 Apr 2022 11:22:10 -0300 Subject: [PATCH 198/206] Update number of task list badge and adjust styling --- .../two-column-tasks/sectioned-task-list.scss | 2 +- .../two-column-tasks/sectioned-task-list.tsx | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss index d7e5365598b..3430940bc10 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -69,7 +69,7 @@ height: 28px; } .woocommerce-task__icon { - margin-left: $gap-smaller; + margin-left: $gap; background-color: $alert-green; border-radius: 50%; width: 24px; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx index 2d5f354191d..32866665f12 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx @@ -154,18 +154,26 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { }; const getPanelTitle = ( section: TaskListSection ) => { - return openPanel === section.id ? ( -
-
- + if ( openPanel === section.id ) { + return ( +
+
+ +
-
- ) : ( + ); + } + const completedTasksCount = tasks.filter( + ( task ) => ! task.isComplete && section.tasks.includes( task.id ) + ).length; + return ( <> { section.title } - + { ! section.isComplete && ( + + ) } { section.isComplete && (
From 56cb47e05905b4b2363abbd03b99828935a5dbf5 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 10:15:39 -0300 Subject: [PATCH 199/206] Fix css lint errors --- .../client/two-column-tasks/headers/section-header.scss | 2 +- .../client/two-column-tasks/sectioned-task-list.scss | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss index 19c8c274cc5..f8fc10b71ee 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -14,6 +14,6 @@ .woocommerce-task-header__contents h1 { font-size: 20px; line-height: 28px; - padding: 0px; + padding: 0; } } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss index 3430940bc10..ce3501bdc21 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -54,7 +54,8 @@ &.task-disabled { pointer-events: none; } - &:not(.complete) .woocommerce-task-list__item-before .woocommerce-task__icon { + + &:not(.complete) .woocommerce-task-list__item-before .woocommerce-task__icon { border-color: $gray-300; } } From 387dc777e620b58fa1879a065b076bf8cbc34916 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 4 Apr 2022 10:20:59 -0300 Subject: [PATCH 200/206] Fix PHP I18n domain issues --- .../Admin/Features/OnboardingTasks/TaskLists.php | 14 +++++++------- .../Features/OnboardingTasks/Tasks/Appearance.php | 4 ++-- .../Features/OnboardingTasks/Tasks/Marketing.php | 4 ++-- .../Features/OnboardingTasks/Tasks/Payments.php | 4 ++-- .../Features/OnboardingTasks/Tasks/Products.php | 4 ++-- .../Features/OnboardingTasks/Tasks/Shipping.php | 4 ++-- .../OnboardingTasks/Tasks/StoreCreation.php | 2 +- .../Admin/Features/OnboardingTasks/Tasks/Tax.php | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 82834c773b4..f8365812239 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -125,7 +125,7 @@ class TaskLists { array( 'id' => 'setup_experiment_2', 'hidden_id' => 'setup', - 'title' => __( 'Get ready to start selling', 'woocommerce-admin' ), + 'title' => __( 'Get ready to start selling', 'woocommerce' ), 'tasks' => array( 'StoreCreation', 'StoreDetails', @@ -146,8 +146,8 @@ class TaskLists { 'sections' => array( array( 'id' => 'basics', - 'title' => __( 'Cover the basics', 'woocommerce-admin' ), - 'description' => __( 'Make sure you’ve got everything you need to start selling—from business details to products.', 'woocommerce-admin' ), + 'title' => __( 'Cover the basics', 'woocommerce' ), + 'description' => __( 'Make sure you’ve got everything you need to start selling—from business details to products.', 'woocommerce' ), 'image' => plugins_url( '/assets/images/task_list/basics-section-illustration.png', WC_ADMIN_PLUGIN_FILE @@ -156,8 +156,8 @@ class TaskLists { ), array( 'id' => 'sales', - 'title' => __( 'Get ready to sell', 'woocommerce-admin' ), - 'description' => __( 'Easily set up the backbone of your store’s operations and get ready to accept first orders.', 'woocommerce-admin' ), + 'title' => __( 'Get ready to sell', 'woocommerce' ), + 'description' => __( 'Easily set up the backbone of your store’s operations and get ready to accept first orders.', 'woocommerce' ), 'image' => plugins_url( '/assets/images/task_list/sales-section-illustration.png', WC_ADMIN_PLUGIN_FILE @@ -166,8 +166,8 @@ class TaskLists { ), array( 'id' => 'expand', - 'title' => __( 'Customize & expand', 'woocommerce-admin' ), - 'description' => __( 'Personalize your store’s design and grow your business by enabling new sales channels.', 'woocommerce-admin' ), + 'title' => __( 'Customize & expand', 'woocommerce' ), + 'description' => __( 'Personalize your store’s design and grow your business by enabling new sales channels.', 'woocommerce' ), 'image' => plugins_url( '/assets/images/task_list/expand-section-illustration.png', WC_ADMIN_PLUGIN_FILE diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php index 65833b4ec06..36f4fd1b752 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Appearance.php @@ -40,7 +40,7 @@ class Appearance extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Make your store stand out with unique design', 'woocommerce-admin' ); + return __( 'Make your store stand out with unique design', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -58,7 +58,7 @@ class Appearance extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Upload your logo to adapt the store to your brand’s personality.', 'woocommerce-admin' ); + return __( 'Upload your logo to adapt the store to your brand’s personality.', 'woocommerce' ); } return __( 'Add your logo, create a homepage, and start designing your store.', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php index 3df6744bdc2..81a35205140 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Marketing.php @@ -26,7 +26,7 @@ class Marketing extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Grow your business with marketing tools', 'woocommerce-admin' ); + return __( 'Grow your business with marketing tools', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -44,7 +44,7 @@ class Marketing extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce-admin' ); + return __( 'Promote your store in other sales channels, like email, Google, and Facebook.', 'woocommerce' ); } return __( 'Add recommended marketing tools to reach new customers and grow your business', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php index 360dcf58dd4..6cd523c6f63 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Payments.php @@ -26,7 +26,7 @@ class Payments extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Add a way to get paid', 'woocommerce-admin' ); + return __( 'Add a way to get paid', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -44,7 +44,7 @@ class Payments extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Let your customers pay the way they like.', 'woocommerce-admin' ); + return __( 'Let your customers pay the way they like.', 'woocommerce' ); } return __( 'Choose payment providers and enable payment methods at checkout.', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php index 3e05b231d5d..2394ef9b288 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php @@ -37,7 +37,7 @@ class Products extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Create or upload your first products', 'woocommerce-admin' ); + return __( 'Create or upload your first products', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -55,7 +55,7 @@ class Products extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Add products to sell and build your catalog.', 'woocommerce-admin' ); + return __( 'Add products to sell and build your catalog.', 'woocommerce' ); } return __( 'Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.', diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php index 32ed4085dac..4af52c248b2 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Shipping.php @@ -25,7 +25,7 @@ class Shipping extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Select how to ship your products', 'woocommerce-admin' ); + return __( 'Select how to ship your products', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -43,7 +43,7 @@ class Shipping extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce-admin' ); + return __( 'Set delivery costs and enable extra features, like shipping label printing.', 'woocommerce' ); } return __( "Set your store location and where you'll ship to.", diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php index f7afbc4b830..93cc08b1e33 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php @@ -27,7 +27,7 @@ class StoreCreation extends Task { public function get_title() { if ( $this->is_complete() ) { /* translators: Store name */ - return sprintf( __( 'You created %s', 'woocommerce-admin' ), get_bloginfo( 'name' ) ); + return sprintf( __( 'You created %s', 'woocommerce' ), get_bloginfo( 'name' ) ); } } diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php index 7368b147da7..cc6e8356e34 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Tax.php @@ -66,7 +66,7 @@ class Tax extends Task { */ public function get_title() { if ( count( $this->task_list->get_sections() ) > 0 && ! $this->is_complete() ) { - return __( 'Get taxes out of your mind', 'woocommerce-admin' ); + return __( 'Get taxes out of your mind', 'woocommerce' ); } if ( true === $this->get_parent_option( 'use_completed_title' ) ) { if ( $this->is_complete() ) { @@ -84,7 +84,7 @@ class Tax extends Task { */ public function get_content() { if ( count( $this->task_list->get_sections() ) > 0 ) { - return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce-admin' ); + return __( 'Have sales tax calculated automatically, or add the rates manually.', 'woocommerce' ); } return self::can_use_automated_taxes() ? __( From 3bb9c18a2de21e2f951cd4ea897cb38f2076ba29 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Tue, 5 Apr 2022 09:57:46 -0300 Subject: [PATCH 201/206] Update hover color --- .../client/stylesheets/shared/_global.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss index e4376a5e49c..10c1235e642 100644 --- a/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss +++ b/plugins/woocommerce-admin/client/stylesheets/shared/_global.scss @@ -1,3 +1,4 @@ +@import 'node_modules/@wordpress/base-styles/colors.native'; // By using CSS variables, we can switch the spacing rhythm using a single media query. :root { --large-gap: 40px; @@ -37,6 +38,14 @@ max-width: 100%; line-height: 1; } + + .components-panel__body > .components-panel__body-title, + .woocommerce-experimental-list__item, + .woocommerce-inbox-message { + &:hover { + background: $gray-0; + } + } } body.woocommerce-page { From 52922c92195702e271829e58207576637d38b859 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Mon, 11 Apr 2022 10:07:08 -0300 Subject: [PATCH 202/206] Add changelogs --- packages/js/onboarding/CHANGELOG.md | 2 ++ .../changelog/feature-32164_new_task_list_version_2 | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 plugins/woocommerce/changelog/feature-32164_new_task_list_version_2 diff --git a/packages/js/onboarding/CHANGELOG.md b/packages/js/onboarding/CHANGELOG.md index e6cb194609c..5054025a93e 100644 --- a/packages/js/onboarding/CHANGELOG.md +++ b/packages/js/onboarding/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Update TaskList types. + # 3.0.1 - Add missing dependency. diff --git a/plugins/woocommerce/changelog/feature-32164_new_task_list_version_2 b/plugins/woocommerce/changelog/feature-32164_new_task_list_version_2 new file mode 100644 index 00000000000..502e6a1268d --- /dev/null +++ b/plugins/woocommerce/changelog/feature-32164_new_task_list_version_2 @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Add new sectioned task list component. #32302 From 0c91a56f1f6af19db974bea6a6cb1dc448d89162 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 13 Apr 2022 11:20:26 -0300 Subject: [PATCH 203/206] Address minor PR feedback and UI issues --- packages/js/data/src/index.ts | 1 + packages/js/data/src/onboarding/utils.ts | 16 +++++ .../progress-header/progress-header.tsx | 13 ++-- .../client/tasks/task-list.tsx | 13 ++-- .../headers/section-header.scss | 11 +++- .../headers/section-header.tsx | 12 ++-- .../two-column-tasks/section-panel-title.tsx | 54 ++++++++++++++++ .../two-column-tasks/sectioned-task-list.scss | 14 +++- .../two-column-tasks/sectioned-task-list.tsx | 64 +++++-------------- .../client/two-column-tasks/task-list.tsx | 8 +-- .../OnboardingTasks/Tasks/StoreCreation.php | 6 +- 11 files changed, 129 insertions(+), 83 deletions(-) create mode 100644 packages/js/data/src/onboarding/utils.ts create mode 100644 plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts index 9e32bacc9c4..469218ce89f 100644 --- a/packages/js/data/src/index.ts +++ b/packages/js/data/src/index.ts @@ -34,6 +34,7 @@ export { withPluginsHydration } from './plugins/with-plugins-hydration'; export { ONBOARDING_STORE_NAME } from './onboarding'; export { withOnboardingHydration } from './onboarding/with-onboarding-hydration'; +export { getVisibleTasks } from './onboarding/utils'; export type { TaskType, TaskListType } from './onboarding/types'; export { USER_STORE_NAME } from './user'; diff --git a/packages/js/data/src/onboarding/utils.ts b/packages/js/data/src/onboarding/utils.ts new file mode 100644 index 00000000000..11f44817f72 --- /dev/null +++ b/packages/js/data/src/onboarding/utils.ts @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import { TaskType } from './types'; + +/** + * Filters tasks to only visible tasks, taking in account snoozed tasks. + */ +export function getVisibleTasks( tasks: TaskType[] ) { + const nowTimestamp = Date.now(); + return tasks.filter( + ( task ) => + ! task.isDismissed && + ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) + ); +} diff --git a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx index fa97af5ba9d..0c537ed207c 100644 --- a/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx +++ b/plugins/woocommerce-admin/client/task-lists/progress-header/progress-header.tsx @@ -4,7 +4,11 @@ import { __, sprintf } from '@wordpress/i18n'; import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { ONBOARDING_STORE_NAME, TaskListType } from '@woocommerce/data'; +import { + getVisibleTasks, + ONBOARDING_STORE_NAME, + TaskListType, +} from '@woocommerce/data'; import { getSetting } from '@woocommerce/settings'; /** @@ -33,12 +37,7 @@ export const ProgressHeader: React.FC< ProgressHeaderProps > = ( { const finishedResolution = select( ONBOARDING_STORE_NAME ).hasFinishedResolution( 'getTaskList', [ taskListId ] ); - const nowTimestamp = Date.now(); - const visibleTasks = taskList?.tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( taskList?.tasks ); return { loading: ! finishedResolution, diff --git a/plugins/woocommerce-admin/client/tasks/task-list.tsx b/plugins/woocommerce-admin/client/tasks/task-list.tsx index 43601642724..25168c76142 100644 --- a/plugins/woocommerce-admin/client/tasks/task-list.tsx +++ b/plugins/woocommerce-admin/client/tasks/task-list.tsx @@ -6,7 +6,11 @@ import { useEffect, useRef, useState } from '@wordpress/element'; import { Card, CardHeader } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { Badge } from '@woocommerce/components'; -import { ONBOARDING_STORE_NAME, TaskListType } from '@woocommerce/data'; +import { + getVisibleTasks, + ONBOARDING_STORE_NAME, + TaskListType, +} from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { Text, List, CollapsibleList } from '@woocommerce/experimental'; @@ -45,12 +49,7 @@ export const TaskList: React.FC< TaskListProps > = ( { }; } ); const prevQueryRef = useRef( query ); - const nowTimestamp = Date.now(); - const visibleTasks = tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( tasks ); const incompleteTasks = tasks.filter( ( task ) => ! task.isComplete && ! task.isDismissed diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss index f8fc10b71ee..067d50e0266 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.scss @@ -1,10 +1,17 @@ .woocommerce-task-section-header__container { display: flex; - .illustration-background { + .woocommerce-task-header__illustration { max-width: 150px; + width: 34%; margin-left: auto; - margin-right: 44px; + margin-right: 7%; + display: flex; + align-items: center; + + .illustration-background { + max-width: 100%; + } } .woocommerce-task-header__contents p { diff --git a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx index 99960e4c2d1..c84a892084e 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/headers/section-header.tsx @@ -16,11 +16,13 @@ const SectionHeader: React.FC< Props > = ( { title, description, image } ) => {

{ title }

{ description }

- { +
+ { +
); }; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx new file mode 100644 index 00000000000..793a69a3cc8 --- /dev/null +++ b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { Badge } from '@woocommerce/components'; +import { TaskListSection, TaskType } from '@woocommerce/data'; +import { Icon, check } from '@wordpress/icons'; +import { Text } from '@woocommerce/experimental'; + +/** + * Internal dependencies + */ +import SectionHeader from './headers/section-header'; + +type SectionPanelTitleProps = { + section: TaskListSection; + active: boolean; + tasks: TaskType[]; +}; + +export const SectionPanelTitle: React.FC< SectionPanelTitleProps > = ( { + section, + active, + tasks, +} ) => { + if ( active ) { + return ( +
+
+ +
+
+ ); + } + + const uncompletedTasksCount = tasks.filter( + ( task ) => ! task.isComplete && section.tasks.includes( task.id ) + ).length; + + return ( + <> + + { section.title } + + { ! section.isComplete && ( + + ) } + { section.isComplete && ( +
+ +
+ ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss index ce3501bdc21..b7352b0b022 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.scss @@ -11,10 +11,18 @@ background: #fff; border: 1px solid $gray-200; + &.is-opened { + padding-bottom: 0; + } + .components-panel__body-title { margin-bottom: 0; border-bottom: 1px solid #e0e0e0; + &:hover { + border-bottom: 1px solid #e0e0e0; + } + > .components-button { font-size: 20px; font-weight: 400; @@ -51,16 +59,16 @@ padding-top: $gap; padding-bottom: $gap; - &.task-disabled { + &.is-disabled { pointer-events: none; } - &:not(.complete) .woocommerce-task-list__item-before .woocommerce-task__icon { + &:not(.is-complete) .woocommerce-task-list__item-before .woocommerce-task__icon { border-color: $gray-300; } } - .woocommerce-task-list__item.complete .woocommerce-task__icon { + .woocommerce-task-list__item.is-complete .woocommerce-task__icon { background-color: $alert-green; } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx index 32866665f12..9fb472401da 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/sectioned-task-list.tsx @@ -5,17 +5,15 @@ import { __ } from '@wordpress/i18n'; import { useEffect, useRef, useState } from '@wordpress/element'; import { Panel, PanelBody, PanelRow } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { Icon, check } from '@wordpress/icons'; import { updateQueryString } from '@woocommerce/navigation'; -import { Badge } from '@woocommerce/components'; import { OPTIONS_STORE_NAME, ONBOARDING_STORE_NAME, TaskType, - TaskListSection, + getVisibleTasks, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; -import { List, TaskItem, Text } from '@woocommerce/experimental'; +import { List, TaskItem } from '@woocommerce/experimental'; import classnames from 'classnames'; /** @@ -25,8 +23,8 @@ import '../tasks/task-list.scss'; import './sectioned-task-list.scss'; import TaskListCompleted from './completed'; import { TaskListProps } from '~/tasks/task-list'; -import SectionHeader from './headers/section-header'; import { ProgressHeader } from '~/task-lists/progress-header'; +import { SectionPanelTitle } from './section-panel-title'; type PanelBodyProps = Omit< PanelBody.Props, 'title' | 'onToggle' > & { title: string | React.ReactNode | undefined; @@ -61,12 +59,7 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { const prevQueryRef = useRef( query ); - const nowTimestamp = Date.now(); - const visibleTasks = tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( tasks ); const recordTaskListView = () => { if ( query.task ) { @@ -93,7 +86,7 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { } }, [ query ] ); - const onDismissTask = ( taskId: string, onDismiss?: () => void ) => { + const onDismissTask = ( taskId: string ) => { dismissTask( taskId ); createNotice( 'success', __( 'Task dismissed' ), { actions: [ @@ -103,10 +96,6 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { }, ], } ); - - if ( onDismiss ) { - onDismiss(); - } }; const hideTasks = () => { @@ -153,36 +142,6 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { ); }; - const getPanelTitle = ( section: TaskListSection ) => { - if ( openPanel === section.id ) { - return ( -
-
- -
-
- ); - } - const completedTasksCount = tasks.filter( - ( task ) => ! task.isComplete && section.tasks.includes( task.id ) - ).length; - return ( - <> - - { section.title } - - { ! section.isComplete && ( - - ) } - { section.isComplete && ( -
- -
- ) } - - ); - }; - if ( ! visibleTasks.length ) { return
; } @@ -213,7 +172,13 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { { ( sections || [] ).map( ( section ) => ( + } opened={ openPanel === section.id } onToggle={ ( isOpen: boolean ) => { if ( ! isOpen && openPanel === section.id ) { @@ -231,8 +196,9 @@ export const SectionedTaskList: React.FC< TaskListProps > = ( { const className = classnames( 'woocommerce-task-list__item', { - complete: task.isComplete, - 'task-disabled': + 'is-complete': + task.isComplete, + 'is-disabled': task.isDisabled, } ); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx index 0bb00f0751a..a0ed8cdd85d 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx @@ -16,6 +16,7 @@ import { ONBOARDING_STORE_NAME, TaskType, useUserPreferences, + getVisibleTasks, } from '@woocommerce/data'; import { recordEvent } from '@woocommerce/tracks'; import { List, TaskItem } from '@woocommerce/experimental'; @@ -63,12 +64,7 @@ export const TaskList: React.FC< TaskListProps > = ( { const prevQueryRef = useRef( query ); - const nowTimestamp = Date.now(); - const visibleTasks = tasks.filter( - ( task ) => - ! task.isDismissed && - ( ! task.isSnoozed || task.snoozedUntil < nowTimestamp ) - ); + const visibleTasks = getVisibleTasks( tasks ); const recordTaskListView = () => { if ( query.task ) { diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php index 93cc08b1e33..03a178779a5 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/StoreCreation.php @@ -25,10 +25,8 @@ class StoreCreation extends Task { * @return string */ public function get_title() { - if ( $this->is_complete() ) { - /* translators: Store name */ - return sprintf( __( 'You created %s', 'woocommerce' ), get_bloginfo( 'name' ) ); - } + /* translators: Store name */ + return sprintf( __( 'You created %s', 'woocommerce' ), get_bloginfo( 'name' ) ); } /** From 60e288c84920cd2201f7d6934c70b2a75685af95 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 13 Apr 2022 12:15:05 -0300 Subject: [PATCH 204/206] Update illustrations and fix potential update delay --- .../two-column-tasks/section-panel-title.tsx | 7 +++---- .../client/two-column-tasks/style.scss | 9 ++++++--- .../client/two-column-tasks/task-list.tsx | 2 +- .../task_list/basics-section-illustration.png | Bin 63845 -> 16174 bytes .../task_list/expand-section-illustration.png | Bin 48357 -> 16097 bytes .../task_list/sales-section-illustration.png | Bin 105347 -> 29679 bytes 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx index 793a69a3cc8..54c0b8dec77 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/section-panel-title.tsx @@ -35,16 +35,15 @@ export const SectionPanelTitle: React.FC< SectionPanelTitleProps > = ( { const uncompletedTasksCount = tasks.filter( ( task ) => ! task.isComplete && section.tasks.includes( task.id ) ).length; + const isComplete = section.isComplete || uncompletedTasksCount === 0; return ( <> { section.title } - { ! section.isComplete && ( - - ) } - { section.isComplete && ( + { ! isComplete && } + { isComplete && (
diff --git a/plugins/woocommerce-admin/client/two-column-tasks/style.scss b/plugins/woocommerce-admin/client/two-column-tasks/style.scss index a6f1dc686a1..16aa121bdcd 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/style.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/style.scss @@ -103,7 +103,7 @@ margin: 0 auto; justify-content: space-between; - ul li.complete .woocommerce-task-list__item-title { + ul li.is-complete .woocommerce-task-list__item-title { font-weight: 600; color: $gray-600; } @@ -178,10 +178,13 @@ height: 100%; } } - .woocommerce-task-list__item:not(.complete) .woocommerce-task__icon { + .woocommerce-task-list__item:not(.is-complete) .woocommerce-task__icon { border: 1px solid var(--wp-admin-theme-color); background: transparent; } + .woocommerce-task-list__item.is-complete:not(.complete) .woocommerce-task__icon { + border: none; + } .woocommerce-task-list__item-before { display: block; @@ -203,7 +206,7 @@ } @for $i from 1 through 10 { - .woocommerce-task-list__item:not(.complete).index-#{$i} .woocommerce-task__icon::after { + .woocommerce-task-list__item:not(.is-complete).index-#{$i} .woocommerce-task__icon::after { content: '#{$i}'; @extend .numbered-circle; color: var(--wp-admin-theme-color); diff --git a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx index a0ed8cdd85d..9f9c4f468e2 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/task-list.tsx @@ -291,7 +291,7 @@ export const TaskList: React.FC< TaskListProps > = ( { const className = classnames( 'woocommerce-task-list__item index-' + index, { - complete: task.isComplete, + 'is-complete': task.isComplete, 'is-active': task.id === activeTaskId, } ); diff --git a/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png b/plugins/woocommerce/assets/images/task_list/basics-section-illustration.png index f598d83ba4306c27ecb1bb2d4b0c1fb13dbf8199..988a281048f1e0f4f41435d605bf6cc027d41c17 100644 GIT binary patch literal 16174 zcmV+}KheO6P)nr@bK{O@bL8Y_3iKP^Yrx4(a|U`HSO;1>FewE`1s=E z?E-J35ET~o_xJVp_Yf2o@9^;CWqCnu%w^LPI;nutwA8}%} zz0$#+_4fAE#iP=~q21KF0duOjxw>YzzuDQ_0CA_;%CFbX!Rh7S-O#e**uUuH-u>#q zJ)EQ4+uhsL%Rrx|+0n(^&92SCvE<#z?P)B~zMIp_zSYE^xuuKy`S!@UsLH*p=HlGs z;M(Ke*WT94zresrID4F-Pg}*vd+!9l^-7<;M&yw?Wjaa zPSD4>;MvaN+|#t5gjuh)>EqcoB^VJ42LJix{N}h2S$##Ksl~LP{p6MY*Fx0Exh^m< zKQ1Bv`|@hIzPqcIPEl9a*xCN_%1NcHUtnQXKQF<;#8j-aOHEP1v7SjbD7vMBO{cCZ z9TCB=m1}Kq^1-6_&90!JppcM|wxD^Cl$QC{zRI+XfPsKfs<8dkG#wQUvDDrE;&|Sr ze*WEEzptCs=R$S{@PE;%iYk>(4V5C zMoWzT;A^CZXzAtQtdw}eh+)^9U3XMK|Mu6ry}t3qf$O(vw2x^ulDrRltr1#&D=>WB zqH9-LmuW{asHw=8nZ7wagfTfge}S*UtAgK8Nd5BOmTpq$^!54R-(;w)^4O|zb*EQw zZ}s53T7re-b#eUh?(gZ`e89%+fi%P6<(tmZjmFOEmy=(YnoMM3^18XCsi;q&qtBLx zcYJ^Bl0QR@k@WEAouH#ZbayL_xZc2_NA$ff0000bbW%=J00ajM4}_qgz@_11fgz)t zN$~!moZ!I3b2Nt0=fY^PHs3_ZpbpBz&j0{B#7RU!RCwC#+`&%UKoEdok42H15k|nfCF1fG+Vz2t0r;J3ON!owvQaWAjJr&lT_!J-P!l zy3lm!zv#ZF+q!w9bX>VB-~H{hKS$m9KtHeZJ~!Lm9#`y8Sw@|C=;`^HUg775^Lns@ z2aNUCbRG100qC-~kFofF2u0TmyL`YadK(N5=W%;ax9~I4kL&W{{C2U5$6Ks#uYdG= z0Daju2k_FxdKE`_peTy3&X&;9#eEKFz$y5Gdmku@Pbs?IoRbf>mTLZbD|%Qk_2Gt?K+FjskPpck%WTqx<0i9 zfZlAEq+QhMsc|0R)Z!)Ru3Z)y^4=OJ7hmWGP8ccyEq{fRi>`J1CJj%y@5i5z{0ji+ zi*rf1_-Aa`+xzVLI!zex4oe;+n>x?S`tfO0@~@!#H%Vx%J!Hy*O&X?UT~(X9$XuR` zm~vkic2R)&{@&z4?aCy;o~@j#_D;F)>z-Ybmt(#+`Nq`&;zLyyS(X-cGvbNM?7rlB zA(sWnnB+E1Ee2I;wp?xzE4LXn<@(`AA;24(+`1a^(V5qBOWznyt~+))064inoZPw! zgTlPg{Bu?hZr^&!dCM`}KlTsLr1bnmH*+OcQ#j^bH>h=EIn z7k0fJBLs||@;c|@uUTv)E*MD8(e(q)|5h_^%pXW@o$Z2Wp)qL@bSLInf{g_0a%jr`yW}VL zm)~6|U$!H8mE#jOQbTIwlN1{z24V1i^55ZG-!E@Yyd!q}7kx{j(6CvXRNzrFV?xRD4(Ry2gUa^2*N3JoUH6JOt*@(u%!%GsZL6 z!Y7QRUmlptmvba5b6FJeA>_KWh7OXw3X{Fu?`x($-4%I^%pZwGX&AYpJsZOO*eeb+ zC`Ta|Z#I!>IXk!iA`g>YGZaOe>lS$-M<9P4*}nZHzO)~=%67kxzip?EKjwJjb{V

|Z;UyL`Dp;TsVKq)KZm(u2FYHr13d`@AA|h) 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 literal 48357 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 JS4*0N{2wq<5N-ef diff --git a/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png b/plugins/woocommerce/assets/images/task_list/sales-section-illustration.png index 816e87a7f595d7efcbbde1b7d0716c1050a14ea1..133800215bc9a736177a58c1e3600872850275e7 100644 GIT binary patch literal 29679 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 Date: Wed, 13 Apr 2022 10:16:33 -0500 Subject: [PATCH 205/206] Update PR template and post-merge comment for changelogger --- .github/PULL_REQUEST_TEMPLATE.md | 5 +---- .github/workflows/scripts/add-post-merge-comment.php | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b5140fcd7f0..dd08989d3c3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,13 +25,10 @@ Closes # . * [ ] Have you added an explanation of what your changes do and why you'd like us to include them? * [ ] Have you written new tests for your changes, as applicable? * [ ] Have you successfully run tests with your changes locally? +* [ ] Have you created a changelog file by running `pnpm nx affected --target=changelog`? -### Changelog entry - -> Enter a summary of all changes on this Pull Request. This will appear in the changelog if accepted. - ### FOR PR REVIEWER ONLY: * [ ] I have reviewed that everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities. I made sure Linting is not ignored or disabled. diff --git a/.github/workflows/scripts/add-post-merge-comment.php b/.github/workflows/scripts/add-post-merge-comment.php index ba2e13baf2d..afd6ec803f4 100644 --- a/.github/workflows/scripts/add-post-merge-comment.php +++ b/.github/workflows/scripts/add-post-merge-comment.php @@ -57,7 +57,6 @@ echo "The pull request was merged by: $merger_user_name\n"; $comment_body = "Hi @$merger_user_name, thanks for merging this pull request. Please take a look at these follow-up tasks you may need to perform: -- [ ] Add the `release: add changelog` label - [ ] Add the `release: add testing instructions` label"; $add_comment_mutation = " From f15e38f55e7500b68a2e16c9546726c769e4640c Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 13 Apr 2022 16:01:31 -0300 Subject: [PATCH 206/206] Fix minor css issues with complete task list item --- .../js/experimental/src/experimental-list/task-item/index.tsx | 2 +- .../experimental/src/experimental-list/task-item/style.scss | 4 ++-- plugins/woocommerce-admin/client/tasks/tasks.scss | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/js/experimental/src/experimental-list/task-item/index.tsx b/packages/js/experimental/src/experimental-list/task-item/index.tsx index 8aa2864d296..59b8ecd2ffd 100644 --- a/packages/js/experimental/src/experimental-list/task-item/index.tsx +++ b/packages/js/experimental/src/experimental-list/task-item/index.tsx @@ -130,7 +130,7 @@ export const TaskItem: React.FC< TaskItemProps > = ( { }, [ expanded ] ); const className = classnames( 'woocommerce-task-list__item', { - complete: completed, + 'is-complete': completed, expanded: isTaskExpanded, 'level-2': level === 2 && ! completed, 'level-1': level === 1 && ! completed, diff --git a/packages/js/experimental/src/experimental-list/task-item/style.scss b/packages/js/experimental/src/experimental-list/task-item/style.scss index f7fb5eb81df..7a95dc775f0 100644 --- a/packages/js/experimental/src/experimental-list/task-item/style.scss +++ b/packages/js/experimental/src/experimental-list/task-item/style.scss @@ -121,7 +121,7 @@ $task-alert-yellow: #f0b849; left: 5px; } - &.complete { + &.is-complete { .woocommerce-task__icon { background-color: var(--wp-admin-theme-color); } @@ -136,7 +136,7 @@ $task-alert-yellow: #f0b849; } } - &:not(.complete) { + &:not(.is-complete) { .woocommerce-task__icon { border: 1px solid $gray-100; background: $white; diff --git a/plugins/woocommerce-admin/client/tasks/tasks.scss b/plugins/woocommerce-admin/client/tasks/tasks.scss index 07719e15b8a..1ba8c76f427 100644 --- a/plugins/woocommerce-admin/client/tasks/tasks.scss +++ b/plugins/woocommerce-admin/client/tasks/tasks.scss @@ -236,7 +236,7 @@ } .woocommerce-task-list__setup_experiment_1 { - .woocommerce-experimental-list .woocommerce-experimental-list__item.complete { + .woocommerce-experimental-list .woocommerce-experimental-list__item.is-complete { text-decoration: line-through; .woocommerce-task-list__item-title {