From 33b2bdc0cd31824a62acdacd724820a9ae66c2f5 Mon Sep 17 00:00:00 2001 From: Florian DANIEL aka Facyla Date: Fri, 23 Sep 2022 12:47:44 +0200 Subject: [PATCH 001/370] Error message in logs on CSV export error Provide a useful feedback message when CSV export fails due to wrong permissions on wp-content/upload/ folder (or any other folder set by WC CSV Exporter module). This helps understand why CSV export fails under some conditions, by providing a hint on the error cause, instead of silently failing. --- .../includes/export/abstract-wc-csv-batch-exporter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php index d65f36bed17..e6f518d269d 100644 --- a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php +++ b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php @@ -127,6 +127,7 @@ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter { protected function write_csv_data( $data ) { if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) { + error_log(__("ERROR : Cannot create temporary CSV export file : please check permissions on upload directory.", 'woocommerce')); return false; } From 29c9dfce165ea3a18a67f77e31e080b0e1fe1b37 Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 10 Oct 2022 09:45:50 +1000 Subject: [PATCH 002/370] Get the first array item for the alt_text. Props galbaras --- plugins/woocommerce/includes/wc-product-functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php index 188c8096d44..222ad109eb2 100644 --- a/plugins/woocommerce/includes/wc-product-functions.php +++ b/plugins/woocommerce/includes/wc-product-functions.php @@ -819,7 +819,7 @@ function wc_get_product_attachment_props( $attachment_id = null, $product = fals } $alt_text = array_filter( $alt_text ); - $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; + $props['alt'] = $alt_text ? reset( $alt_text ) : ''; // Large version. $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); From 2a034c0df4108e7e954a71d3a8917b2b54f79485 Mon Sep 17 00:00:00 2001 From: Phill <38789408+SavPhill@users.noreply.github.com> Date: Sat, 15 Oct 2022 15:43:52 +0700 Subject: [PATCH 003/370] Update tooltip text The tooltip for the Header Image field currently reads: "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media)." I believe my small edit to the description text makes it more clearer for how the user should do this. --- .../includes/admin/settings/class-wc-settings-emails.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php index 9086d7fd8b8..36705f13c50 100644 --- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php +++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php @@ -120,7 +120,7 @@ class WC_Settings_Emails extends WC_Settings_Page { array( 'title' => __( 'Header image', 'woocommerce' ), - 'desc' => __( 'URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).', 'woocommerce' ), + 'desc' => __( 'Paste the URL of an image you want to show in the email header. Upload images using the media uploader (Media > Add New).', 'woocommerce' ), 'id' => 'woocommerce_email_header_image', 'type' => 'text', 'css' => 'min-width:400px;', From 1b5bc44c60810413d01e1430c7c8dca4fb990375 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 2 Nov 2022 20:14:55 +0530 Subject: [PATCH 004/370] Add changelog. --- plugins/woocommerce/changelog/pr-35107 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/pr-35107 diff --git a/plugins/woocommerce/changelog/pr-35107 b/plugins/woocommerce/changelog/pr-35107 new file mode 100644 index 00000000000..4a03f11bfc9 --- /dev/null +++ b/plugins/woocommerce/changelog/pr-35107 @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Only a minor text change. + + From 6acd69e404349bee923b8640dc70960b9f67bcf6 Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Wed, 9 Nov 2022 10:41:18 +0000 Subject: [PATCH 005/370] Multichannel Marketing - Core Library (#35099) * Create channel interface and campaign value class * Create MarketingChannels class * Register MarketingChannels class in DI container * Use the new MarketingChannels class to get the installed marketing extensions' data * Use DI container to access InstalledExtensions class * Add InstalledExtensions to the $provides array * Hint that campaign cost should also indicate the currency * Initialize the channels array * Add unit tests for MarketingCampaign * Add unit tests for MarketingChannels * Add Price class to represent a price with currency * Use Price class for marketing campaign's cost * Define a constant to indicate the MCM classes exist This constant will be checked by third-party extensions before utilizing any of the classes/interfaces defined for this feature. * Create MarketingSpecs class to include WC.com API calls * Remove WC.com API calls from Marketing class And replace them with calls from MarketingSpecs class. * Use the const from MarketingSpecs * Fix MarketingChannels unit tests * Add missing settings URL to the channel data Co-authored-by: Nima --- .../includes/wc-update-functions.php | 4 +- .../woocommerce/src/Admin/API/Marketing.php | 20 +- .../src/Admin/API/MarketingOverview.php | 9 +- .../Admin/Marketing/InstalledExtensions.php | 618 +----------------- .../src/Admin/Marketing/MarketingCampaign.php | 110 ++++ .../Marketing/MarketingChannelInterface.php | 82 +++ .../src/Admin/Marketing/MarketingChannels.php | 131 ++++ .../woocommerce/src/Admin/Marketing/Price.php | 70 ++ plugins/woocommerce/src/Container.php | 2 + .../src/Internal/Admin/Marketing.php | 140 +--- .../Admin/Marketing/MarketingSpecs.php | 145 ++++ .../MarketingServiceProvider.php | 44 ++ .../Admin/Marketing/MarketingCampaignTest.php | 58 ++ .../Admin/Marketing/MarketingChannelsTest.php | 120 ++++ 14 files changed, 830 insertions(+), 723 deletions(-) create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/MarketingChannels.php create mode 100644 plugins/woocommerce/src/Admin/Marketing/Price.php create mode 100644 plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php create mode 100644 plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php create mode 100644 plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php index 20d966b2e86..2d0c94a14fd 100644 --- a/plugins/woocommerce/includes/wc-update-functions.php +++ b/plugins/woocommerce/includes/wc-update-functions.php @@ -18,7 +18,7 @@ defined( 'ABSPATH' ) || exit; -use Automattic\WooCommerce\Internal\Admin\Marketing; +use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; use Automattic\WooCommerce\Internal\AssignDefaultCategory; use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator; use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore; @@ -2469,5 +2469,5 @@ function wc_update_700_remove_download_log_fk() { * Remove the transient data for recommended marketing extensions. */ function wc_update_700_remove_recommended_marketing_plugins_transient() { - delete_transient( Marketing::RECOMMENDED_PLUGINS_TRANSIENT ); + delete_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT ); } diff --git a/plugins/woocommerce/src/Admin/API/Marketing.php b/plugins/woocommerce/src/Admin/API/Marketing.php index de06c4b7071..a417170f00a 100644 --- a/plugins/woocommerce/src/Admin/API/Marketing.php +++ b/plugins/woocommerce/src/Admin/API/Marketing.php @@ -7,8 +7,8 @@ namespace Automattic\WooCommerce\Admin\API; -use Automattic\WooCommerce\Internal\Admin\Marketing as MarketingFeature; use Automattic\WooCommerce\Admin\PluginsHelper; +use Automattic\WooCommerce\Internal\Admin\Marketing\MarketingSpecs; defined( 'ABSPATH' ) || exit; @@ -103,9 +103,16 @@ class Marketing extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_recommended_plugins( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + // Default to marketing category (if no category set). $category = ( ! empty( $request->get_param( 'category' ) ) ) ? $request->get_param( 'category' ) : 'marketing'; - $all_plugins = MarketingFeature::get_instance()->get_recommended_plugins(); + $all_plugins = $marketing_specs->get_recommended_plugins(); $valid_plugins = []; $per_page = $request->get_param( 'per_page' ); @@ -130,7 +137,14 @@ class Marketing extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_knowledge_base_posts( $request ) { + /** + * MarketingSpecs class. + * + * @var MarketingSpecs $marketing_specs + */ + $marketing_specs = wc_get_container()->get( MarketingSpecs::class ); + $category = $request->get_param( 'category' ); - return rest_ensure_response( MarketingFeature::get_instance()->get_knowledge_base_posts( $category ) ); + return rest_ensure_response( $marketing_specs->get_knowledge_base_posts( $category ) ); } } diff --git a/plugins/woocommerce/src/Admin/API/MarketingOverview.php b/plugins/woocommerce/src/Admin/API/MarketingOverview.php index 930dcb4c0fc..883ce04c1bb 100644 --- a/plugins/woocommerce/src/Admin/API/MarketingOverview.php +++ b/plugins/woocommerce/src/Admin/API/MarketingOverview.php @@ -125,7 +125,14 @@ class MarketingOverview extends \WC_REST_Data_Controller { * @return \WP_Error|\WP_REST_Response */ public function get_installed_plugins( $request ) { - return rest_ensure_response( InstalledExtensions::get_data() ); + /** + * InstalledExtensions + * + * @var InstalledExtensions $installed_extensions + */ + $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); + + return rest_ensure_response( $installed_extensions->get_data() ); } } diff --git a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php index 3f90c964d28..9669b9014c6 100644 --- a/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php +++ b/plugins/woocommerce/src/Admin/Marketing/InstalledExtensions.php @@ -5,598 +5,46 @@ namespace Automattic\WooCommerce\Admin\Marketing; -use Automattic\WooCommerce\Admin\PluginsHelper; - /** * Installed Marketing Extensions class. */ class InstalledExtensions { + /** + * MarketingChannels repository + * + * @var MarketingChannels + */ + protected $marketing_channels; + + /** + * Class initialization, invoked by the DI container. + * + * @param MarketingChannels $marketing_channels The MarketingChannels repository. + * + * @internal + */ + final public function init( MarketingChannels $marketing_channels ) { + $this->marketing_channels = $marketing_channels; + } /** * Gets an array of plugin data for the "Installed marketing extensions" card. - * - * Valid extensions statuses are: installed, activated, configured */ - public static function get_data() { - $data = []; - - $automatewoo = self::get_automatewoo_extension_data(); - $aw_referral = self::get_aw_referral_extension_data(); - $aw_birthdays = self::get_aw_birthdays_extension_data(); - $mailchimp = self::get_mailchimp_extension_data(); - $facebook = self::get_facebook_extension_data(); - $pinterest = self::get_pinterest_extension_data(); - $google = self::get_google_extension_data(); - $amazon_ebay = self::get_amazon_ebay_extension_data(); - $mailpoet = self::get_mailpoet_extension_data(); - $creative_mail = self::get_creative_mail_extension_data(); - $tiktok = self::get_tiktok_extension_data(); - $jetpack_crm = self::get_jetpack_crm_extension_data(); - $zapier = self::get_zapier_extension_data(); - $salesforce = self::get_salesforce_extension_data(); - $vimeo = self::get_vimeo_extension_data(); - $trustpilot = self::get_trustpilot_extension_data(); - - if ( $automatewoo ) { - $data[] = $automatewoo; - } - - if ( $aw_referral ) { - $data[] = $aw_referral; - } - - if ( $aw_birthdays ) { - $data[] = $aw_birthdays; - } - - if ( $mailchimp ) { - $data[] = $mailchimp; - } - - if ( $facebook ) { - $data[] = $facebook; - } - - if ( $pinterest ) { - $data[] = $pinterest; - } - - if ( $google ) { - $data[] = $google; - } - - if ( $amazon_ebay ) { - $data[] = $amazon_ebay; - } - - if ( $mailpoet ) { - $data[] = $mailpoet; - } - - if ( $creative_mail ) { - $data[] = $creative_mail; - } - - if ( $tiktok ) { - $data[] = $tiktok; - } - - if ( $jetpack_crm ) { - $data[] = $jetpack_crm; - } - - if ( $zapier ) { - $data[] = $zapier; - } - - if ( $salesforce ) { - $data[] = $salesforce; - } - - if ( $vimeo ) { - $data[] = $vimeo; - } - - if ( $trustpilot ) { - $data[] = $trustpilot; - } - - return $data; + public function get_data(): array { + return array_map( + function ( MarketingChannelInterface $channel ) { + return [ + 'slug' => $channel->get_slug(), + 'status' => $channel->is_setup_completed() ? 'configured' : 'activated', + 'settingsUrl' => $channel->get_setup_url(), + 'name' => $channel->get_name(), + 'description' => $channel->get_description(), + 'product_listings_status' => $channel->get_product_listings_status(), + 'errors_no' => $channel->get_errors_no(), + 'icon' => $channel->get_icon_url(), + ]; + }, + $this->marketing_channels->get_registered_channels() + ); } - - /** - * Get allowed plugins. - * - * @return array - */ - public static function get_allowed_plugins() { - return [ - 'automatewoo', - 'mailchimp-for-woocommerce', - 'creative-mail-by-constant-contact', - 'facebook-for-woocommerce', - 'pinterest-for-woocommerce', - 'google-listings-and-ads', - 'hubspot-for-woocommerce', - 'woocommerce-amazon-ebay-integration', - 'mailpoet', - ]; - } - - /** - * Get AutomateWoo extension data. - * - * @return array|bool - */ - protected static function get_automatewoo_extension_data() { - $slug = 'automatewoo'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'AW' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings' ); - $data['docsUrl'] = 'https://automatewoo.com/docs/'; - $data['status'] = 'configured'; // Currently no configuration step. - } - - return $data; - } - - /** - * Get AutomateWoo Refer a Friend extension data. - * - * @return array|bool - */ - protected static function get_aw_referral_extension_data() { - $slug = 'automatewoo-referrals'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] ) { - $data['docsUrl'] = 'https://automatewoo.com/docs/refer-a-friend/'; - $data['status'] = 'configured'; - if ( function_exists( 'AW_Referrals' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=referrals' ); - } - } - - return $data; - } - - /** - * Get AutomateWoo Birthdays extension data. - * - * @return array|bool - */ - protected static function get_aw_birthdays_extension_data() { - $slug = 'automatewoo-birthdays'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/automatewoo.svg'; - - if ( 'activated' === $data['status'] ) { - $data['docsUrl'] = 'https://automatewoo.com/docs/getting-started-with-birthdays/'; - $data['status'] = 'configured'; - if ( function_exists( 'AW_Birthdays' ) ) { - $data['settingsUrl'] = admin_url( 'admin.php?page=automatewoo-settings&tab=birthdays' ); - } - } - - return $data; - } - - /** - * Get MailChimp extension data. - * - * @return array|bool - */ - protected static function get_mailchimp_extension_data() { - $slug = 'mailchimp-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailchimp.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'mailchimp_is_configured' ) ) { - $data['docsUrl'] = 'https://mailchimp.com/help/connect-or-disconnect-mailchimp-for-woocommerce/'; - $data['settingsUrl'] = admin_url( 'admin.php?page=mailchimp-woocommerce' ); - - if ( mailchimp_is_configured() ) { - $data['status'] = 'configured'; - } - } - - return $data; - } - - /** - * Get Facebook extension data. - * - * @return array|bool - */ - protected static function get_facebook_extension_data() { - $slug = 'facebook-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/facebook-icon.svg'; - - if ( $data['status'] === 'activated' && function_exists( 'facebook_for_woocommerce' ) ) { - $integration = facebook_for_woocommerce()->get_integration(); - - if ( $integration->is_configured() ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = facebook_for_woocommerce()->get_settings_url(); - $data['docsUrl'] = facebook_for_woocommerce()->get_documentation_url(); - } - - return $data; - } - - /** - * Get Pinterest extension data. - * - * @return array|bool - */ - protected static function get_pinterest_extension_data() { - $slug = 'pinterest-for-woocommerce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/pinterest.svg'; - - // TODO: Finalise docs url. - $data['docsUrl'] = 'https://woocommerce.com/document/pinterest-for-woocommerce/?utm_medium=product'; - - if ( 'activated' === $data['status'] && class_exists( 'Pinterest_For_Woocommerce' ) ) { - $pinterest_onboarding_completed = Pinterest_For_Woocommerce()::is_setup_complete(); - if ( $pinterest_onboarding_completed ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/pinterest/landing' ); - } - } - - return $data; - } - - /** - * Get Google extension data. - * - * @return array|bool - */ - protected static function get_google_extension_data() { - $slug = 'google-listings-and-ads'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/google.svg'; - - if ( 'activated' === $data['status'] && function_exists( 'woogle_get_container' ) && class_exists( '\Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService' ) ) { - - $merchant_center = woogle_get_container()->get( \Automattic\WooCommerce\GoogleListingsAndAds\MerchantCenter\MerchantCenterService::class ); - - if ( $merchant_center->is_setup_complete() ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-admin&path=/google/start' ); - } - - $data['docsUrl'] = 'https://woocommerce.com/document/google-listings-and-ads/?utm_medium=product'; - } - - return $data; - } - - /** - * Get Amazon / Ebay extension data. - * - * @return array|bool - */ - protected static function get_amazon_ebay_extension_data() { - $slug = 'woocommerce-amazon-ebay-integration'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/amazon-ebay.svg'; - - if ( 'activated' === $data['status'] && class_exists( '\CodistoConnect' ) ) { - - $codisto_merchantid = get_option( 'codisto_merchantid' ); - - // Use same check as codisto admin tabs. - if ( is_numeric( $codisto_merchantid ) ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=codisto-settings' ); - $data['docsUrl'] = 'https://woocommerce.com/document/multichannel-for-woocommerce-google-amazon-ebay-walmart-integration/?utm_medium=product'; - } - - return $data; - } - - /** - * Get MailPoet extension data. - * - * @return array|bool - */ - protected static function get_mailpoet_extension_data() { - $slug = 'mailpoet'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/mailpoet.svg'; - - if ( 'activated' === $data['status'] && class_exists( '\MailPoet\API\API' ) ) { - $mailpoet_api = \MailPoet\API\API::MP( 'v1' ); - - if ( ! method_exists( $mailpoet_api, 'isSetupComplete' ) || $mailpoet_api->isSetupComplete() ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=mailpoet-newsletters' ); - } - - $data['docsUrl'] = 'https://kb.mailpoet.com/'; - $data['supportUrl'] = 'https://www.mailpoet.com/support/'; - } - - return $data; - } - - /** - * Get Creative Mail for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_creative_mail_extension_data() { - $slug = 'creative-mail-by-constant-contact'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/creative-mail-by-constant-contact.png'; - - if ( 'activated' === $data['status'] && class_exists( '\CreativeMail\Helpers\OptionsHelper' ) ) { - if ( ! method_exists( '\CreativeMail\Helpers\OptionsHelper', 'get_instance_id' ) || \CreativeMail\Helpers\OptionsHelper::get_instance_id() !== null ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail_settings' ); - } else { - $data['settingsUrl'] = admin_url( 'admin.php?page=creativemail' ); - } - - $data['docsUrl'] = 'https://app.creativemail.com/kb/help/WooCommerce'; - $data['supportUrl'] = 'https://app.creativemail.com/kb/help/'; - } - - return $data; - } - - /** - * Get TikTok for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_tiktok_extension_data() { - $slug = 'tiktok-for-business'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/tiktok.jpg'; - - if ( 'activated' === $data['status'] ) { - if ( false !== get_option( 'tt4b_access_token' ) ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=tiktok' ); - $data['docsUrl'] = 'https://woocommerce.com/document/tiktok-for-woocommerce/'; - $data['supportUrl'] = 'https://ads.tiktok.com/athena/user-feedback/?identify_key=6a1e079024806640c5e1e695d13db80949525168a052299b4970f9c99cb5ac78'; - } - - return $data; - } - - /** - * Get Jetpack CRM for WooCommerce extension data. - * - * @return array|bool - */ - protected static function get_jetpack_crm_extension_data() { - $slug = 'zero-bs-crm'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/jetpack-crm.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=zerobscrm-plugin-settings' ); - $data['docsUrl'] = 'https://kb.jetpackcrm.com/'; - $data['supportUrl'] = 'https://kb.jetpackcrm.com/crm-support/'; - } - - return $data; - } - - /** - * Get WooCommerce Zapier extension data. - * - * @return array|bool - */ - protected static function get_zapier_extension_data() { - $slug = 'woocommerce-zapier'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/zapier.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=wc-settings&tab=wc_zapier' ); - $data['docsUrl'] = 'https://docs.om4.io/woocommerce-zapier/'; - } - - return $data; - } - - /** - * Get Salesforce extension data. - * - * @return array|bool - */ - protected static function get_salesforce_extension_data() { - $slug = 'integration-with-salesforce'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/salesforce.jpg'; - - if ( 'activated' === $data['status'] && class_exists( '\Integration_With_Salesforce_Admin' ) ) { - if ( ! method_exists( '\Integration_With_Salesforce_Admin', 'get_connection_status' ) || \Integration_With_Salesforce_Admin::get_connection_status() ) { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'admin.php?page=integration-with-salesforce' ); - $data['docsUrl'] = 'https://woocommerce.com/document/salesforce-integration/'; - $data['supportUrl'] = 'https://wpswings.com/submit-query/'; - } - - return $data; - } - - /** - * Get Vimeo extension data. - * - * @return array|bool - */ - protected static function get_vimeo_extension_data() { - $slug = 'vimeo'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/vimeo.png'; - - if ( 'activated' === $data['status'] && class_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth' ) ) { - if ( method_exists( '\Tribe\Vimeo_WP\Vimeo\Vimeo_Auth', 'has_access_token' ) ) { - $vimeo_auth = new \Tribe\Vimeo_WP\Vimeo\Vimeo_Auth(); - if ( $vimeo_auth->has_access_token() ) { - $data['status'] = 'configured'; - } - } else { - $data['status'] = 'configured'; - } - - $data['settingsUrl'] = admin_url( 'options-general.php?page=vimeo_settings' ); - $data['docsUrl'] = 'https://woocommerce.com/document/vimeo/'; - $data['supportUrl'] = 'https://vimeo.com/help/contact'; - } - - return $data; - } - - /** - * Get Trustpilot extension data. - * - * @return array|bool - */ - protected static function get_trustpilot_extension_data() { - $slug = 'trustpilot-reviews'; - - if ( ! PluginsHelper::is_plugin_installed( $slug ) ) { - return false; - } - - $data = self::get_extension_base_data( $slug ); - $data['icon'] = WC_ADMIN_IMAGES_FOLDER_URL . '/marketing/trustpilot.png'; - - if ( 'activated' === $data['status'] ) { - $data['status'] = 'configured'; - $data['settingsUrl'] = admin_url( 'admin.php?page=woocommerce-trustpilot-settings-page' ); - $data['docsUrl'] = 'https://woocommerce.com/document/trustpilot-reviews/'; - $data['supportUrl'] = 'https://support.trustpilot.com/hc/en-us/requests/new'; - } - - return $data; - } - - - /** - * Get an array of basic data for a given extension. - * - * @param string $slug Plugin slug. - * - * @return array|false - */ - protected static function get_extension_base_data( $slug ) { - $status = PluginsHelper::is_plugin_active( $slug ) ? 'activated' : 'installed'; - $plugin_data = PluginsHelper::get_plugin_data( $slug ); - - if ( ! $plugin_data ) { - return false; - } - - return [ - 'slug' => $slug, - 'status' => $status, - 'name' => $plugin_data['Name'], - 'description' => html_entity_decode( wp_trim_words( $plugin_data['Description'], 20 ) ), - 'supportUrl' => 'https://woocommerce.com/my-account/create-a-ticket/?utm_medium=product', - ]; - } - } diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php new file mode 100644 index 00000000000..7b3f99a4b3a --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingCampaign.php @@ -0,0 +1,110 @@ +id = $id; + $this->title = $title; + $this->manage_url = $manage_url; + $this->cost = $cost; + } + + /** + * Returns the marketing campaign's unique identifier. + * + * @return string + */ + public function get_id(): string { + return $this->id; + } + + /** + * Returns the title of the marketing campaign. + * + * @return string + */ + public function get_title(): string { + return $this->title; + } + + /** + * Returns the URL to manage the marketing campaign. + * + * @return string + */ + public function get_manage_url(): string { + return $this->manage_url; + } + + /** + * Returns the cost of the marketing campaign with the currency. + * + * @return Price|null + */ + public function get_cost(): ?Price { + return $this->cost; + } + + /** + * Serialize the marketing campaign data. + * + * @return array + */ + public function jsonSerialize() { + return [ + 'id' => $this->get_id(), + 'title' => $this->get_title(), + 'manage_url' => $this->get_manage_url(), + 'cost' => $this->get_cost(), + ]; + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php new file mode 100644 index 00000000000..3a7233f073b --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/MarketingChannelInterface.php @@ -0,0 +1,82 @@ +marketing_specs = $marketing_specs; + $this->allowed_channels = $this->get_allowed_channels(); + } + + /** + * Registers a marketing channel. + * + * Note that only a predetermined list of third party extensions can be registered as a marketing channel. + * + * @param MarketingChannelInterface $channel The marketing channel to register. + * + * @return void + * + * @see MarketingChannels::is_channel_allowed() Checks if the marketing channel is allowed to be registered or not. + */ + public function register( MarketingChannelInterface $channel ): void { + if ( ! $this->is_channel_allowed( $channel ) ) { + // Silently log an error and bail. + wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); + + return; + } + + $this->registered_channels[ $channel->get_slug() ] = $channel; + } + + /** + * Returns an array of all registered marketing channels. + * + * @return MarketingChannelInterface[] + */ + public function get_registered_channels(): array { + /** + * Filter the list of registered marketing channels. + * + * Note that only a predetermined list of third party extensions can be registered as a marketing channel. + * Any new plugins added to this array will be cross-checked with that list, which is obtained from WooCommerce.com API. + * + * @param MarketingChannelInterface[] $channels Array of registered marketing channels. + * + * @since x.x.x + */ + $channels = apply_filters( 'woocommerce_marketing_channels', $this->registered_channels ); + + // Only return allowed channels. + $allowed_channels = array_filter( + $channels, + function ( MarketingChannelInterface $channel ) { + if ( ! $this->is_channel_allowed( $channel ) ) { + // Silently log an error and bail. + wc_get_logger()->error( sprintf( 'Marketing channel %s (%s) cannot be registered!', $channel->get_name(), $channel->get_slug() ) ); + + return false; + } + + return true; + } + ); + + return array_values( $allowed_channels ); + } + + /** + * Returns an array of plugin slugs for the marketing channels that are allowed to be registered. + * + * @return array + */ + protected function get_allowed_channels(): array { + $recommended_channels = $this->marketing_specs->get_recommended_plugins(); + if ( empty( $recommended_channels ) ) { + return []; + } + + return array_column( $recommended_channels, 'product', 'product' ); + } + + /** + * Determines whether the given marketing channel is allowed to be registered. + * + * @param MarketingChannelInterface $channel The marketing channel object. + * + * @return bool + */ + protected function is_channel_allowed( MarketingChannelInterface $channel ): bool { + return isset( $this->allowed_channels[ $channel->get_slug() ] ); + } +} diff --git a/plugins/woocommerce/src/Admin/Marketing/Price.php b/plugins/woocommerce/src/Admin/Marketing/Price.php new file mode 100644 index 00000000000..9dbb00837ae --- /dev/null +++ b/plugins/woocommerce/src/Admin/Marketing/Price.php @@ -0,0 +1,70 @@ +value = $value; + $this->currency = $currency; + } + + /** + * Get value of the price. + * + * @return string + */ + public function get_value(): string { + return $this->value; + } + + /** + * Get the currency of the price. + * + * @return string + */ + public function get_currency(): string { + return $this->currency; + } + + /** + * Serialize the price data. + * + * @return array + */ + public function jsonSerialize() { + return [ + 'value' => $this->get_value(), + 'currency' => $this->get_currency(), + ]; + } +} diff --git a/plugins/woocommerce/src/Container.php b/plugins/woocommerce/src/Container.php index 3815db81840..0e64a37d322 100644 --- a/plugins/woocommerce/src/Container.php +++ b/plugins/woocommerce/src/Container.php @@ -10,6 +10,7 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\COTMig use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\AssignDefaultCategoryServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\FeaturesServiceProvider; +use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\MarketingServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrdersControllersServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderAdminServiceProvider; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\OrderMetaBoxServiceProvider; @@ -65,6 +66,7 @@ final class Container { OrderMetaBoxServiceProvider::class, OrderAdminServiceProvider::class, FeaturesServiceProvider::class, + MarketingServiceProvider::class, ); /** diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing.php b/plugins/woocommerce/src/Internal/Admin/Marketing.php index 65cf4a5b464..f11cd2d31d9 100644 --- a/plugins/woocommerce/src/Internal/Admin/Marketing.php +++ b/plugins/woocommerce/src/Internal/Admin/Marketing.php @@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Internal\Admin; use Automattic\WooCommerce\Admin\Features\Features; use Automattic\WooCommerce\Admin\Marketing\InstalledExtensions; -use Automattic\WooCommerce\Internal\Admin\Loader; use Automattic\WooCommerce\Admin\PageController; /** @@ -17,20 +16,6 @@ class Marketing { use CouponsMovedTrait; - /** - * Name of recommended plugins transient. - * - * @var string - */ - const RECOMMENDED_PLUGINS_TRANSIENT = 'wc_marketing_recommended_plugins'; - - /** - * Name of knowledge base post transient. - * - * @var string - */ - const KNOWLEDGE_BASE_TRANSIENT = 'wc_marketing_knowledge_base'; - /** * Class instance. * @@ -180,124 +165,15 @@ class Marketing { return $settings; } - $settings['marketing']['installedExtensions'] = InstalledExtensions::get_data(); + /** + * InstalledExtensions helper class. + * + * @var InstalledExtensions $installed_extensions + */ + $installed_extensions = wc_get_container()->get( InstalledExtensions::class ); + + $settings['marketing']['installedExtensions'] = $installed_extensions->get_data(); return $settings; } - - /** - * Load recommended plugins from WooCommerce.com - * - * @return array - */ - public function get_recommended_plugins() { - $plugins = get_transient( self::RECOMMENDED_PLUGINS_TRANSIENT ); - - if ( false === $plugins ) { - $request = wp_remote_get( - 'https://woocommerce.com/wp-json/wccom/marketing-tab/1.2/recommendations.json', - array( - 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), - ) - ); - $plugins = []; - - if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { - $plugins = json_decode( $request['body'], true ); - } - - set_transient( - self::RECOMMENDED_PLUGINS_TRANSIENT, - $plugins, - // Expire transient in 15 minutes if remote get failed. - // Cache an empty result to avoid repeated failed requests. - empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS - ); - } - - return array_values( $plugins ); - } - - /** - * Load knowledge base posts from WooCommerce.com - * - * @param string $category Category of posts to retrieve. - * @return array - */ - public function get_knowledge_base_posts( $category ) { - - $kb_transient = self::KNOWLEDGE_BASE_TRANSIENT; - - $categories = array( - 'marketing' => 1744, - 'coupons' => 25202, - ); - - // Default to marketing category (if no category set on the kb component). - if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) { - $category_id = $categories[ $category ]; - $kb_transient = $kb_transient . '_' . strtolower( $category ); - } else { - $category_id = $categories['marketing']; - } - - $posts = get_transient( $kb_transient ); - - if ( false === $posts ) { - $request_url = add_query_arg( - array( - 'categories' => $category_id, - 'page' => 1, - 'per_page' => 8, - '_embed' => 1, - ), - 'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product' - ); - - $request = wp_remote_get( - $request_url, - array( - 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), - ) - ); - $posts = []; - - if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { - $raw_posts = json_decode( $request['body'], true ); - - foreach ( $raw_posts as $raw_post ) { - $post = [ - 'title' => html_entity_decode( $raw_post['title']['rendered'] ), - 'date' => $raw_post['date_gmt'], - 'link' => $raw_post['link'], - 'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '', - 'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '', - ]; - - $featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? []; - if ( count( $featured_media ) > 0 ) { - $image = current( $featured_media ); - $post['image'] = add_query_arg( - array( - 'resize' => '650,340', - 'crop' => 1, - ), - $image['source_url'] - ); - } - - $posts[] = $post; - } - } - - set_transient( - $kb_transient, - $posts, - // Expire transient in 15 minutes if remote get failed. - empty( $posts ) ? 900 : DAY_IN_SECONDS - ); - } - - return $posts; - } } diff --git a/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php new file mode 100644 index 00000000000..ee36ba1375c --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Marketing/MarketingSpecs.php @@ -0,0 +1,145 @@ + 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), + ) + ); + $plugins = []; + + if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { + $plugins = json_decode( $request['body'], true ); + } + + set_transient( + self::RECOMMENDED_PLUGINS_TRANSIENT, + $plugins, + // Expire transient in 15 minutes if remote get failed. + // Cache an empty result to avoid repeated failed requests. + empty( $plugins ) ? 900 : 3 * DAY_IN_SECONDS + ); + } + + return array_values( $plugins ); + } + + /** + * Load knowledge base posts from WooCommerce.com + * + * @param string|null $category Category of posts to retrieve. + * @return array + */ + public function get_knowledge_base_posts( ?string $category ): array { + $kb_transient = self::KNOWLEDGE_BASE_TRANSIENT; + + $categories = array( + 'marketing' => 1744, + 'coupons' => 25202, + ); + + // Default to marketing category (if no category set on the kb component). + if ( ! empty( $category ) && array_key_exists( $category, $categories ) ) { + $category_id = $categories[ $category ]; + $kb_transient = $kb_transient . '_' . strtolower( $category ); + } else { + $category_id = $categories['marketing']; + } + + $posts = get_transient( $kb_transient ); + + if ( false === $posts ) { + $request_url = add_query_arg( + array( + 'categories' => $category_id, + 'page' => 1, + 'per_page' => 8, + '_embed' => 1, + ), + 'https://woocommerce.com/wp-json/wp/v2/posts?utm_medium=product' + ); + + $request = wp_remote_get( + $request_url, + array( + 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), + ) + ); + $posts = []; + + if ( ! is_wp_error( $request ) && 200 === $request['response']['code'] ) { + $raw_posts = json_decode( $request['body'], true ); + + foreach ( $raw_posts as $raw_post ) { + $post = [ + 'title' => html_entity_decode( $raw_post['title']['rendered'] ), + 'date' => $raw_post['date_gmt'], + 'link' => $raw_post['link'], + 'author_name' => isset( $raw_post['author_name'] ) ? html_entity_decode( $raw_post['author_name'] ) : '', + 'author_avatar' => isset( $raw_post['author_avatar_url'] ) ? $raw_post['author_avatar_url'] : '', + ]; + + $featured_media = $raw_post['_embedded']['wp:featuredmedia'] ?? []; + if ( count( $featured_media ) > 0 ) { + $image = current( $featured_media ); + $post['image'] = add_query_arg( + array( + 'resize' => '650,340', + 'crop' => 1, + ), + $image['source_url'] + ); + } + + $posts[] = $post; + } + } + + set_transient( + $kb_transient, + $posts, + // Expire transient in 15 minutes if remote get failed. + empty( $posts ) ? 900 : DAY_IN_SECONDS + ); + } + + return $posts; + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php new file mode 100644 index 00000000000..8e27386ae86 --- /dev/null +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/MarketingServiceProvider.php @@ -0,0 +1,44 @@ +share( MarketingSpecs::class ); + $this->share( MarketingChannels::class )->addArgument( MarketingSpecs::class ); + $this->share( InstalledExtensions::class )->addArgument( MarketingChannels::class ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php new file mode 100644 index 00000000000..401e1630294 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingCampaignTest.php @@ -0,0 +1,58 @@ +assertEquals( '1234', $marketing_campaign->get_id() ); + $this->assertEquals( 'Ad #1234', $marketing_campaign->get_title() ); + $this->assertEquals( 'https://example.com/manage-campaigns', $marketing_campaign->get_manage_url() ); + $this->assertNotNull( $marketing_campaign->get_cost() ); + $this->assertEquals( 'USD', $marketing_campaign->get_cost()->get_currency() ); + $this->assertEquals( '1000', $marketing_campaign->get_cost()->get_value() ); + } + + /** + * @testdox `cost` property can be null. + */ + public function test_cost_can_be_null() { + $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns' ); + + $this->assertNull( $marketing_campaign->get_cost() ); + } + + /** + * @testdox It can be serialized to JSON including all its properties. + */ + public function test_can_be_serialized_to_json() { + $marketing_campaign = new MarketingCampaign( '1234', 'Ad #1234', 'https://example.com/manage-campaigns', new Price( '1000', 'USD' ) ); + + $json = wp_json_encode( $marketing_campaign ); + $this->assertNotEmpty( $json ); + $this->assertEqualSets( + [ + 'id' => $marketing_campaign->get_id(), + 'title' => $marketing_campaign->get_title(), + 'manage_url' => $marketing_campaign->get_manage_url(), + 'cost' => [ + 'value' => $marketing_campaign->get_cost()->get_value(), + 'currency' => $marketing_campaign->get_cost()->get_currency(), + ], + ], + json_decode( $json, true ) + ); + } +} diff --git a/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php new file mode 100644 index 00000000000..cf5885a0557 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Admin/Marketing/MarketingChannelsTest.php @@ -0,0 +1,120 @@ +createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() ) + ->method( 'get_recommended_plugins' ) + ->willReturn( + [ + [ + 'product' => 'test-channel-1', + ], + ] + ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + $marketing_channels->register( $test_channel ); + + $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel, $marketing_channels->get_registered_channels()[0] ); + } + + /** + * @testdox A marketing channel can NOT be registered using the `register` method if it is NOT in the allowed list. + */ + public function test_does_not_register_disallowed_channels() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() )->method( 'get_recommended_plugins' )->willReturn( [] ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + $marketing_channels->register( $test_channel ); + + $this->assertEmpty( $marketing_channels->get_registered_channels() ); + } + + /** + * @testdox A marketing channel can be registered using the `woocommerce_marketing_channels` WordPress filter if it is in the allowed list. + */ + public function test_registers_allowed_channels_using_wp_filter() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + $marketing_specs = $this->createMock( MarketingSpecs::class ); + $marketing_specs->expects( $this->once() ) + ->method( 'get_recommended_plugins' ) + ->willReturn( + [ + [ + 'product' => 'test-channel-1', + ], + ] + ); + + $marketing_channels = new MarketingChannels(); + $marketing_channels->init( $marketing_specs ); + + add_filter( + 'woocommerce_marketing_channels', + function ( array $channels ) use ( $test_channel ) { + $channels[ $test_channel->get_slug() ] = $test_channel; + + return $channels; + } + ); + + $this->assertNotEmpty( $marketing_channels->get_registered_channels() ); + $this->assertEquals( $test_channel, $marketing_channels->get_registered_channels()[0] ); + } + + /** + * @testdox A marketing channel can NOT be registered using the `woocommerce_marketing_channels` WordPress filter if it NOT is in the allowed list. + */ + public function test_does_not_register_disallowed_channels_using_wp_filter() { + $test_channel = $this->createMock( MarketingChannelInterface::class ); + $test_channel->expects( $this->any() )->method( 'get_slug' )->willReturn( 'test-channel-1' ); + + set_transient( MarketingSpecs::RECOMMENDED_PLUGINS_TRANSIENT, [] ); + + add_filter( + 'woocommerce_marketing_channels', + function ( array $channels ) use ( $test_channel ) { + $channels[ $test_channel->get_slug() ] = $test_channel; + + return $channels; + } + ); + + $marketing_channels = new MarketingChannels(); + $this->assertEmpty( $marketing_channels->get_registered_channels() ); + } +} From dedbf7b49227f4542f69aeddb0e9915788089bf8 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Mon, 12 Dec 2022 19:41:00 +1300 Subject: [PATCH 006/370] Migrate uses of ::set-output in code-analyzer to setOutput. (#35895) * Migrate uses of ::set-output to setOutput. --- pnpm-lock.yaml | 145 +++++++++++------- tools/code-analyzer/README.md | 4 +- tools/code-analyzer/package.json | 3 +- .../src/commands/analyzer/analyzer-lint.ts | 2 +- tools/code-analyzer/src/print.ts | 15 +- 5 files changed, 101 insertions(+), 68 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c01f068a36..defee3ca381 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -385,6 +385,9 @@ importers: webpack: 5.70.0_webpack-cli@3.3.12 webpack-cli: 3.3.12_webpack@5.70.0 + packages/js/create-extension: + specifiers: {} + packages/js/csv-export: specifiers: '@babel/core': ^7.17.5 @@ -1872,6 +1875,7 @@ importers: tools/code-analyzer: specifiers: + '@actions/core': ^1.10.0 '@commander-js/extra-typings': ^0.1.0 '@tsconfig/node16': ^1.0.3 '@types/node': ^16.9.4 @@ -1887,6 +1891,7 @@ importers: typescript: ^4.8.3 uuid: ^8.3.2 dependencies: + '@actions/core': 1.10.0 '@commander-js/extra-typings': 0.1.0_commander@9.4.0 '@tsconfig/node16': 1.0.3 '@types/uuid': 8.3.4 @@ -2104,6 +2109,18 @@ importers: packages: + /@actions/core/1.10.0: + resolution: {integrity: sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==} + dependencies: + '@actions/http-client': 2.0.1 + uuid: 8.3.2 + dev: false + + /@actions/http-client/2.0.1: + resolution: {integrity: sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==} + dependencies: + tunnel: 0.0.6 + /@ampproject/remapping/2.1.2: resolution: {integrity: sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==} engines: {node: '>=6.0.0'} @@ -3445,7 +3462,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.16.12: @@ -3455,7 +3472,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/7.16.7_@babel+core@7.17.8: @@ -3744,7 +3761,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.12.9 dev: true @@ -3755,7 +3772,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-dynamic-import': 7.8.3_@babel+core@7.16.12 dev: false @@ -3808,7 +3825,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.12.9 dev: true @@ -3819,7 +3836,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-export-namespace-from': 7.8.3_@babel+core@7.16.12 dev: false @@ -3862,7 +3879,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.12.9 dev: true @@ -3873,7 +3890,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-json-strings': 7.8.3_@babel+core@7.16.12 dev: false @@ -3916,7 +3933,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.12.9 dev: true @@ -3927,7 +3944,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-logical-assignment-operators': 7.10.4_@babel+core@7.16.12 dev: false @@ -4023,7 +4040,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.12.9 dev: true @@ -4034,7 +4051,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-numeric-separator': 7.10.4_@babel+core@7.16.12 dev: false @@ -4157,7 +4174,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.12.9 dev: true @@ -4168,7 +4185,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 '@babel/plugin-syntax-optional-catch-binding': 7.8.3_@babel+core@7.16.12 dev: false @@ -4295,7 +4312,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.12.9 + '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.12.9 '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color @@ -4308,7 +4325,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-create-class-features-plugin': 7.19.0_@babel+core@7.16.12 + '@babel/helper-create-class-features-plugin': 7.17.6_@babel+core@7.16.12 '@babel/helper-plugin-utils': 7.19.0 transitivePeerDependencies: - supports-color @@ -5001,7 +5018,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.16.12: @@ -5011,7 +5028,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-arrow-functions/7.16.7_@babel+core@7.17.8: @@ -5119,7 +5136,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.16.12: @@ -5129,7 +5146,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-block-scoped-functions/7.16.7_@babel+core@7.17.8: @@ -5168,7 +5185,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.16.12: @@ -5178,7 +5195,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-block-scoping/7.16.7_@babel+core@7.17.8: @@ -5226,12 +5243,12 @@ packages: dependencies: '@babel/core': 7.12.9 '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-environment-visitor': 7.16.7 + '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-replace-supers': 7.16.7 - '@babel/helper-split-export-declaration': 7.16.7 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-replace-supers': 7.19.1 + '@babel/helper-split-export-declaration': 7.18.6 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5245,12 +5262,12 @@ packages: dependencies: '@babel/core': 7.16.12 '@babel/helper-annotate-as-pure': 7.16.7 - '@babel/helper-environment-visitor': 7.16.7 + '@babel/helper-environment-visitor': 7.18.9 '@babel/helper-function-name': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 - '@babel/helper-plugin-utils': 7.18.9 - '@babel/helper-replace-supers': 7.16.7 - '@babel/helper-split-export-declaration': 7.16.7 + '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-replace-supers': 7.19.1 + '@babel/helper-split-export-declaration': 7.18.6 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -5311,7 +5328,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.16.12: @@ -5321,7 +5338,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-computed-properties/7.16.7_@babel+core@7.17.8: @@ -5360,7 +5377,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.16.12: @@ -5370,7 +5387,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-destructuring/7.17.7_@babel+core@7.17.8: @@ -5485,7 +5502,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.16.12: @@ -5495,7 +5512,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-duplicate-keys/7.16.7_@babel+core@7.17.8: @@ -5598,7 +5615,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.16.12: @@ -5608,7 +5625,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-for-of/7.16.7_@babel+core@7.17.8: @@ -5705,7 +5722,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-literals/7.16.7_@babel+core@7.16.12: @@ -5715,7 +5732,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-literals/7.16.7_@babel+core@7.17.8: @@ -5754,7 +5771,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.16.12: @@ -5764,7 +5781,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-member-expression-literals/7.16.7_@babel+core@7.17.8: @@ -6138,7 +6155,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.16.12: @@ -6148,7 +6165,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-new-target/7.16.7_@babel+core@7.17.8: @@ -6310,7 +6327,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.16.12: @@ -6320,7 +6337,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-property-literals/7.16.7_@babel+core@7.17.8: @@ -6522,7 +6539,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.16.12: @@ -6532,7 +6549,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-reserved-words/7.16.7_@babel+core@7.17.8: @@ -6638,7 +6655,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.16.12: @@ -6648,7 +6665,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-shorthand-properties/7.16.7_@babel+core@7.17.8: @@ -6741,7 +6758,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.16.12: @@ -6751,7 +6768,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-sticky-regex/7.16.7_@babel+core@7.17.8: @@ -6790,7 +6807,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.16.12: @@ -6800,7 +6817,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-template-literals/7.16.7_@babel+core@7.17.8: @@ -6839,7 +6856,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.16.12: @@ -6849,7 +6866,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-typeof-symbol/7.16.7_@babel+core@7.17.8: @@ -6928,7 +6945,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.12.9 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: true /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.16.12: @@ -6938,7 +6955,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.16.12 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.18.9 dev: false /@babel/plugin-transform-unicode-escapes/7.16.7_@babel+core@7.17.8: @@ -8213,6 +8230,7 @@ packages: minimatch: 3.1.2 transitivePeerDependencies: - supports-color + dev: true /@humanwhocodes/config-array/0.5.0: resolution: {integrity: sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==} @@ -14488,6 +14506,7 @@ packages: typescript: 4.8.4 transitivePeerDependencies: - supports-color + dev: true /@typescript-eslint/scope-manager/4.33.0: resolution: {integrity: sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==} @@ -23148,7 +23167,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.43.0_zksrc6ykdxhogxjbhb5axiabwi + '@typescript-eslint/parser': 5.43.0_z4bbprzjrhnsfa24uvmcbu7f5q debug: 3.2.7 eslint-import-resolver-node: 0.3.6 find-up: 2.1.0 @@ -23944,6 +23963,7 @@ packages: dependencies: eslint: 8.28.0 eslint-visitor-keys: 2.1.0 + dev: true /eslint-visitor-keys/1.3.0: resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} @@ -24329,6 +24349,7 @@ packages: text-table: 0.2.0 transitivePeerDependencies: - supports-color + dev: true /espree/5.0.1: resolution: {integrity: sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==} @@ -27826,6 +27847,7 @@ packages: /is-path-inside/3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + dev: true /is-plain-obj/1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} @@ -30371,6 +30393,7 @@ packages: /js-sdsl/4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} + dev: true /js-sha3/0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} @@ -39933,6 +39956,10 @@ packages: dependencies: safe-buffer: 5.2.1 + /tunnel/0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + /turbo-android-arm64/1.4.5: resolution: {integrity: sha512-cKPJVyS1A2BBVbcH8XVeBArtEjHxioEm9zQa3Hv68usQOOFW+KOjH+0fGvjqMrWztLVFhE+npeVsnyu/6AmRew==} cpu: [arm64] diff --git a/tools/code-analyzer/README.md b/tools/code-analyzer/README.md index 340da2d4f4e..cb07a057b0a 100644 --- a/tools/code-analyzer/README.md +++ b/tools/code-analyzer/README.md @@ -8,7 +8,7 @@ Currently there are just 2 commands: -1. `lint`. Analyzer is used as a linter for PRs to check if hook/template/db changes were introduced. It produces output either directly on CI or via GH actions `set-output`. +1. `lint`. Analyzer is used as a linter for PRs to check if hook/template/db changes were introduced. It produces output either directly on CI or via setting output variables in GH actions. Here is an example `analyzer` command, run from this directory: @@ -18,7 +18,7 @@ In this command we compare the `release/6.7` and `release/6.8` branches to find To find out more about the other arguments to the command you can run `pnpm run analyzer -- --help` -2. `major-minor`. This simple CLI tool gives you the latest `.0` major/minor released version of a plugin's mainfile based on Woo release conventions. +2. `major-minor`. This simple CLI tool gives you the latest `.0` major/minor released version of a plugin's mainfile based on Woo release conventions. Here is an example `major-minor` command, run from this directory: diff --git a/tools/code-analyzer/package.json b/tools/code-analyzer/package.json index 556a14c3bf4..1781c296fc8 100644 --- a/tools/code-analyzer/package.json +++ b/tools/code-analyzer/package.json @@ -14,7 +14,8 @@ "commander": "^9.4.0", "dotenv": "^10.0.0", "simple-git": "^3.10.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "@actions/core": "^1.10.0" }, "devDependencies": { "@types/node": "^16.9.4", diff --git a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts index 16da3295a0a..a620f6ca6ee 100644 --- a/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts +++ b/tools/code-analyzer/src/commands/analyzer/analyzer-lint.ts @@ -38,7 +38,7 @@ const program = new Command() ) .option( '-o, --outputStyle ', - 'Output style for the results. Options: github, cli. Github output will use ::set-output to set the results as an output variable.', + 'Output style for the results. Options: github, cli. Github output will set the results as an output variable for Github actions.', 'cli' ) .option( diff --git a/tools/code-analyzer/src/print.ts b/tools/code-analyzer/src/print.ts index 1c36f69578a..592d993da69 100644 --- a/tools/code-analyzer/src/print.ts +++ b/tools/code-analyzer/src/print.ts @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { setOutput } from '@actions/core'; + /** * Internal dependencies */ @@ -29,7 +34,7 @@ export const printTemplateResults = ( ); } - log( `::set-output name=templates::${ opt }` ); + setOutput( 'templates', opt ); } else { log( `\n## ${ title }:` ); for ( const { filePath, code, message } of data ) { @@ -63,7 +68,7 @@ export const printHookResults = ( version, description, hookType, - changeType + changeType, } of data ) { opt += `\\n* **File:** ${ filePath }`; @@ -78,7 +83,7 @@ export const printHookResults = ( ); } - log( `::set-output name=wphooks::${ opt }` ); + setOutput( 'wphooks', opt ); } else { log( `\n## ${ sectionTitle }:` ); log( '---------------------------------------------------' ); @@ -130,7 +135,7 @@ export const printSchemaChange = ( } } ); - log( `::set-output name=schema::${ githubCommentContent }` ); + setOutput( 'schema', githubCommentContent ); } else { log( '\n## SCHEMA CHANGES' ); log( '---------------------------------------------------' ); @@ -163,7 +168,7 @@ export const printDatabaseUpdates = ( ): void => { if ( output === 'github' ) { const githubCommentContent = `\\n\\n### New database updates:\\n * **${ updateFunctionName }** introduced in ${ updateFunctionVersion }`; - log( `::set-output name=database::${ githubCommentContent }` ); + setOutput( 'database', githubCommentContent ); } else { log( '\n## DATABASE UPDATES' ); log( '---------------------------------------------------' ); From e66d33554213f7b87294c6f312c1ee50baa41d16 Mon Sep 17 00:00:00 2001 From: James Allan Date: Mon, 12 Dec 2022 17:23:31 +1000 Subject: [PATCH 007/370] Make set_order_props_from_data() protected rather than private (#35829) * Make set_order_props_from_data() protected This enables 3rd parties that extend the order datastore and need to set their own data. For example, Subscriptions * Add changelog entry * return set_order_props_from_data to private visibility * Make init_order_record and get_order_data_for_ids protected Following feedback here: https://github.com/woocommerce/woocommerce/pull/35829#issuecomment-1340528244 * Update changelog entry Co-authored-by: mattallan --- plugins/woocommerce/changelog/fix-wcs-core-issue-268 | 4 ++++ .../src/Internal/DataStores/Orders/OrdersTableDataStore.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-wcs-core-issue-268 diff --git a/plugins/woocommerce/changelog/fix-wcs-core-issue-268 b/plugins/woocommerce/changelog/fix-wcs-core-issue-268 new file mode 100644 index 00000000000..0a2035f51af --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wcs-core-issue-268 @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Make the OrdersTableDataStore init_order_record() and get_order_data_for_ids() functions protected rather than private diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 6290828af48..a2ba703c0ca 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1027,7 +1027,7 @@ WHERE * * @return void */ - private function init_order_record( \WC_Abstract_Order &$order, int $order_id, \stdClass $order_data ) { + protected function init_order_record( \WC_Abstract_Order &$order, int $order_id, \stdClass $order_data ) { $order->set_defaults(); $order->set_id( $order_id ); $filtered_meta_data = $this->filter_raw_meta_data( $order, $order_data->meta_data ); @@ -1347,7 +1347,7 @@ WHERE * * @return \stdClass[]|object|null DB Order objects or error. */ - private function get_order_data_for_ids( $ids ) { + protected function get_order_data_for_ids( $ids ) { if ( ! $ids ) { return array(); } From 7187c8dff08cc5a5aafbcefeb5f6bf7fbcb3c10e Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Mon, 12 Dec 2022 13:39:44 +0530 Subject: [PATCH 008/370] Split CALC_FOUND_ROW query into seperate count query for better performance (#35468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Néstor Soriano --- plugins/woocommerce/changelog/fix-35464 | 4 ++ .../DataStores/Orders/OrdersTableQuery.php | 32 +++++++++++--- .../Orders/OrdersTableDataStoreTests.php | 42 +++++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-35464 diff --git a/plugins/woocommerce/changelog/fix-35464 b/plugins/woocommerce/changelog/fix-35464 new file mode 100644 index 00000000000..f6d26253ab5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35464 @@ -0,0 +1,4 @@ +Significance: patch +Type: performance + +Split CALC_FOUND_ROW query into seperate count query for better performance. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php index 56805c8fd9e..8b71df2aed5 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableQuery.php @@ -115,6 +115,13 @@ class OrdersTableQuery { */ private $sql = ''; + /** + * Final SQL query to count results after processing of args. + * + * @var string + */ + private $count_sql = ''; + /** * The number of pages (when pagination is enabled). * @@ -586,11 +593,7 @@ class OrdersTableQuery { $fields = $this->fields; // SQL_CALC_FOUND_ROWS. - if ( ( ! $this->arg_isset( 'no_found_rows' ) || ! $this->args['no_found_rows'] ) && $this->limits ) { - $found_rows = 'SQL_CALC_FOUND_ROWS'; - } else { - $found_rows = ''; - } + $found_rows = ''; // JOIN. $join = implode( ' ', array_unique( array_filter( array_map( 'trim', $this->join ) ) ) ); @@ -617,6 +620,23 @@ class OrdersTableQuery { $groupby = $this->groupby ? 'GROUP BY ' . implode( ', ', (array) $this->groupby ) : ''; $this->sql = "SELECT $found_rows DISTINCT $fields FROM $orders_table $join WHERE $where $groupby $orderby $limits"; + $this->build_count_query( $fields, $join, $where, $groupby ); + } + + /** + * Build SQL query for counting total number of results. + * + * @param string $fields Prepared fields for SELECT clause. + * @param string $join Prepared JOIN clause. + * @param string $where Prepared WHERE clause. + * @param string $groupby Prepared GROUP BY clause. + */ + private function build_count_query( $fields, $join, $where, $groupby ) { + if ( ! isset( $this->sql ) || '' === $this->sql ) { + wc_doing_it_wrong( __FUNCTION__, 'Count query can only be build after main query is built.', '7.3.0' ); + } + $orders_table = $this->tables['orders']; + $this->count_sql = "SELECT COUNT(DISTINCT $fields) FROM $orders_table $join WHERE $where $groupby"; } /** @@ -1014,7 +1034,7 @@ class OrdersTableQuery { } if ( $this->limits ) { - $this->found_orders = absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ); + $this->found_orders = absint( $wpdb->get_var( $this->count_sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared $this->max_num_pages = (int) ceil( $this->found_orders / $this->args['limit'] ); } else { $this->found_orders = count( $this->orders ); diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 3b512c14328..765095fe095 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -1003,6 +1003,48 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case { $this->assertEquals( array_slice( $test_orders, 5, 5 ), $query->orders, 'The expected dataset is supplied when paginating through orders.' ); } + /** + * @testdox Test that the query counts works as expected. + * + * @return void + */ + public function test_cot_query_count() { + $this->assertEquals( 0, ( new OrdersTableQuery() )->found_orders, 'We initially have zero orders within our custom order tables.' ); + + for ( $i = 0; $i < 30; $i ++ ) { + $order = new WC_Order(); + $this->switch_data_store( $order, $this->sut ); + if ( 0 === $i % 2 ) { + $order->set_billing_address_2( 'Test' ); + } + $order->save(); + } + + $query = new OrdersTableQuery( array( 'limit' => 5 ) ); + $this->assertEquals( 30, $query->found_orders, 'Specifying limits still calculate all found orders.' ); + + // Count does not change based on the fields that we are fetching. + $query = new OrdersTableQuery( + array( + 'fields' => 'ids', + 'limit' => 5, + ) + ); + $this->assertEquals( 30, $query->found_orders, 'Fetching specific field does not change query count.' ); + + $query = new OrdersTableQuery( + array( + 'field_query' => array( + array( + 'field' => 'billing_address_2', + 'value' => 'Test', + ), + ), + ) + ); + $this->assertEquals( 15, $query->found_orders, 'Counting orders with a field query works.' ); + } + /** * @testDox Test the `get_order_count()` method. */ From d406eeb299e0123eba131caa72bca12ddb26b23b Mon Sep 17 00:00:00 2001 From: louwie17 Date: Mon, 12 Dec 2022 09:43:34 -0400 Subject: [PATCH 009/370] Fix react chunk build warnings (#35930) * Reorganize imports to fix build warnings and remove overlapping css import * Add changelog * Update changelog --- .../woocommerce-admin/client/products/add-product-page.tsx | 2 -- .../woocommerce-admin/client/products/edit-product-page.tsx | 3 +-- plugins/woocommerce/changelog/fix-chunk_build_warnings | 4 ++++ 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-chunk_build_warnings diff --git a/plugins/woocommerce-admin/client/products/add-product-page.tsx b/plugins/woocommerce-admin/client/products/add-product-page.tsx index 37326b87fee..ac84914a862 100644 --- a/plugins/woocommerce-admin/client/products/add-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/add-product-page.tsx @@ -7,8 +7,6 @@ import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ - -import './product-page.scss'; import { ProductForm } from './product-form'; const AddProductPage: React.FC = () => { diff --git a/plugins/woocommerce-admin/client/products/edit-product-page.tsx b/plugins/woocommerce-admin/client/products/edit-product-page.tsx index 1b403e50713..9544f353adf 100644 --- a/plugins/woocommerce-admin/client/products/edit-product-page.tsx +++ b/plugins/woocommerce-admin/client/products/edit-product-page.tsx @@ -16,9 +16,8 @@ import { useParams } from 'react-router-dom'; /** * Internal dependencies */ -import { ProductFormLayout } from './layout/product-form-layout'; -import './product-page.scss'; import { ProductForm } from './product-form'; +import { ProductFormLayout } from './layout/product-form-layout'; const EditProductPage: React.FC = () => { const { productId } = useParams(); diff --git a/plugins/woocommerce/changelog/fix-chunk_build_warnings b/plugins/woocommerce/changelog/fix-chunk_build_warnings new file mode 100644 index 00000000000..8d19a6e74a8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-chunk_build_warnings @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix WooCommerce Admin client React build warnings and remove unnecessary scss imports. From 613e58c06146afc39d87703bd6dd1c755c43c9aa Mon Sep 17 00:00:00 2001 From: louwie17 Date: Mon, 12 Dec 2022 09:56:28 -0400 Subject: [PATCH 010/370] CES exit prompt for product editing screens (#35728) * Add exit page tracker logic and implement it for product pages * Add changelog * Fix lint errors and add comments * Add ces_location prop * Add mock to fix broken test * Add CES exit page survey tests * Fix a bug with React pages redirects and update actions * Fix test * Fix lint * Add default inside location prop * Remove exit prefix within action * Address PR feedback and make sure its not triggered on save * Update copy of exit feedback notice * Add changelog * Update name of param * Fix lint error * Use hasFinishedResolution vs isResolved in customerEffortScoreTracks --- .../changelog/add-35126_ces_exit_prompt | 4 + .../src/customer-effort-score.tsx | 9 +- .../src/customer-feedback-modal/index.tsx | 12 +- .../customer-effort-score-exit-page.ts | 206 ++++++++++++++++++ .../customer-effort-score-modal-container.tsx | 1 + .../customer-effort-score-tracks-container.js | 2 + .../customer-effort-score-tracks.js | 38 +++- .../data/actions.js | 6 + .../data/reducer.js | 4 +- .../customer-effort-score-exit-page.test.ts | 62 ++++++ ...customer-effort-score-exit-page-tracker.ts | 47 ++++ .../embedded-body-layout.tsx | 6 + .../test/embedded-body-layout.test.tsx | 12 + .../woocommerce-admin/client/layout/index.js | 5 + .../client/products/product-form-actions.tsx | 5 + .../test/product-form-actions.spec.tsx | 6 + .../client/typings/global.d.ts | 15 ++ .../product-tracking/product-edit.ts | 4 +- .../product-tracking/product-new.ts | 4 +- .../product-tracking/shared.ts | 49 +++++ .../changelog/add-35126_ces_exit_prompt | 4 + 21 files changed, 483 insertions(+), 18 deletions(-) create mode 100644 packages/js/customer-effort-score/changelog/add-35126_ces_exit_prompt create mode 100644 plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-exit-page.ts create mode 100644 plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts create mode 100644 plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts create mode 100644 plugins/woocommerce/changelog/add-35126_ces_exit_prompt diff --git a/packages/js/customer-effort-score/changelog/add-35126_ces_exit_prompt b/packages/js/customer-effort-score/changelog/add-35126_ces_exit_prompt new file mode 100644 index 00000000000..9b4ba7be2ad --- /dev/null +++ b/packages/js/customer-effort-score/changelog/add-35126_ces_exit_prompt @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add description and noticeLabel props to customer feedback components. diff --git a/packages/js/customer-effort-score/src/customer-effort-score.tsx b/packages/js/customer-effort-score/src/customer-effort-score.tsx index 34725585e5a..9a20ca21e4d 100644 --- a/packages/js/customer-effort-score/src/customer-effort-score.tsx +++ b/packages/js/customer-effort-score/src/customer-effort-score.tsx @@ -20,6 +20,8 @@ type CustomerEffortScoreProps = { comments: string ) => void; title: string; + description?: string; + noticeLabel?: string; firstQuestion: string; secondQuestion: string; onNoticeShownCallback?: () => void; @@ -37,6 +39,8 @@ type CustomerEffortScoreProps = { * @param {Object} props Component props. * @param {Function} props.recordScoreCallback Function to call when the score should be recorded. * @param {string} props.title The title displayed in the modal. + * @param {string} props.description The description displayed in the modal. + * @param {string} props.noticeLabel The notice label displayed in the notice. * @param {string} props.firstQuestion The first survey question. * @param {string} props.secondQuestion The second survey question. * @param {Function} props.onNoticeShownCallback Function to call when the notice is shown. @@ -47,6 +51,8 @@ type CustomerEffortScoreProps = { const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { recordScoreCallback, title, + description, + noticeLabel, firstQuestion, secondQuestion, onNoticeShownCallback = noop, @@ -63,7 +69,7 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { return; } - createNotice( 'success', title, { + createNotice( 'success', noticeLabel || title, { actions: [ { label: __( 'Give feedback', 'woocommerce' ), @@ -94,6 +100,7 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( { return ( void; title: string; + description?: string; firstQuestion: string; secondQuestion: string; defaultScore?: number; @@ -142,10 +145,11 @@ function CustomerFeedbackModal( { lineHeight="20px" marginBottom="1.5em" > - { __( - 'Your feedback will help create a better experience for thousands of merchants like you. Please tell us to what extent you agree or disagree with the statements below.', - 'woocommerce' - ) } + { description || + __( + 'Your feedback will help create a better experience for thousands of merchants like you. Please tell us to what extent you agree or disagree with the statements below.', + 'woocommerce' + ) } { + allowTracking = trackingOption === 'yes'; + } ); + +/** + * Gets the list of exited pages from Localstorage. + */ +export const getExitPageData = () => { + if ( ! window.localStorage ) { + return []; + } + + const items = window.localStorage.getItem( + CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY + ); + const parsedJSONItems = items ? JSON.parse( items ) : []; + const arrayItems = Array.isArray( parsedJSONItems ) ? parsedJSONItems : []; + + return arrayItems; +}; + +/** + * Adds the page to the exit page list in Localstorage. + * + * @param {string} pageId of page exited early. + */ +export const addExitPage = ( pageId: string ) => { + if ( ! window.localStorage ) { + return; + } + + let items = getExitPageData(); + + if ( ! items.find( ( pageExitedId ) => pageExitedId === pageId ) ) { + items.push( pageId ); + } + items = items.slice( -10 ); // Upper limit. + + window.localStorage.setItem( + CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY, + JSON.stringify( items ) + ); +}; + +/** + * Removes the passed in page id from the list in Localstorage. + * + * @param {string} pageId of page to be removed. + */ +export const removeExitPage = ( pageId: string ) => { + if ( ! window.localStorage ) { + return; + } + + let items = getExitPageData(); + + items = items.filter( ( pageExitedId ) => pageExitedId !== pageId ); + items = items.slice( -10 ); // Upper limit. + + window.localStorage.setItem( + CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY, + JSON.stringify( items ) + ); +}; + +const eventListeners: Record< string, ( event: BeforeUnloadEvent ) => void > = + {}; + +/** + * Adds unload event listener to add pageId to exit page list incase there were unsaved changes. + * + * @param {string} pageId the page id of the page being exited early. + * @param {Function} hasUnsavedChanges callback to check if the page had unsaved changes. + */ +export const addCustomerEffortScoreExitPageListener = ( + pageId: string, + hasUnsavedChanges: () => boolean +) => { + eventListeners[ pageId ] = ( event ) => { + if ( hasUnsavedChanges() && allowTracking ) { + addExitPage( pageId ); + } + }; + window.addEventListener( 'unload', eventListeners[ pageId ] ); +}; + +/** + * Removes the unload exit page listener. + * + * @param {string} pageId the page id to remove the listener from. + */ +export const removeCustomerEffortScoreExitPageListener = ( pageId: string ) => { + if ( eventListeners[ pageId ] ) { + window.removeEventListener( 'unload', eventListeners[ pageId ], { + capture: true, + } ); + } +}; + +/** + * Returns the exit page copy of the passed in pageId. + * + * @param {string} pageId page id. + */ +function getExitPageCESCopy( pageId: string ): { + action: string; + title: string; + firstQuestion: string; + secondQuestion: string; + noticeLabel?: string; + description?: string; +} | null { + switch ( pageId ) { + case 'product_edit_view': + case 'editing_new_product': + return { + action: + pageId === 'editing_new_product' ? 'new_product' : pageId, + noticeLabel: __( + 'How is your experience with editing products?', + 'woocommerce' + ), + title: __( + "How's your experience with editing products?", + 'woocommerce' + ), + description: __( + 'We noticed you started editing a product, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.', + 'woocommerce' + ), + firstQuestion: __( + 'The product editing screen is easy to use', + 'woocommerce' + ), + secondQuestion: __( + "The product editing screen's functionality meets my needs", + 'woocommerce' + ), + }; + case 'product_add_view': + case 'new_product': + return { + action: pageId, + noticeLabel: __( + 'How is your experience with creating products?', + 'woocommerce' + ), + title: __( + 'How is your experience with creating products?', + 'woocommerce' + ), + description: __( + 'We noticed you started creating a product, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.', + 'woocommerce' + ), + firstQuestion: __( + 'The product creation screen is easy to use', + 'woocommerce' + ), + secondQuestion: __( + "The product creation screen's functionality meets my needs", + 'woocommerce' + ), + }; + default: + return null; + } +} + +/** + * Checks the exit page list and triggers a CES survey for the first item in the list. + */ +export function triggerExitPageCesSurvey() { + const exitPageItems: string[] = getExitPageData(); + if ( exitPageItems && exitPageItems.length > 0 ) { + const copy = getExitPageCESCopy( exitPageItems[ 0 ] ); + if ( copy && copy.title.length > 0 ) { + dispatch( 'wc/customer-effort-score' ).addCesSurvey( { + ...copy, + pageNow: window.pagenow, + adminPage: window.adminpage, + props: { + ces_location: 'outside', + }, + } ); + } + removeExitPage( exitPageItems[ 0 ] ); + } +} diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx index 051b7a351de..038f166ac07 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-modal-container.tsx @@ -75,6 +75,7 @@ export const CustomerEffortScoreModalContainer: React.FC = () => { return ( { diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js index 42dae830326..69ba68b14b6 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks-container.js @@ -49,6 +49,8 @@ function CustomerEffortScoreTracksContainer( { { - const { getOption, isResolving } = select( OPTIONS_STORE_NAME ); + const { getOption, hasFinishedResolution } = + select( OPTIONS_STORE_NAME ); - const cesShownForActions = - getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ) || []; + const cesShownForActions = getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ); const adminInstallTimestamp = getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0; @@ -207,12 +221,16 @@ export default compose( const allowTracking = allowTrackingOption === 'yes'; const resolving = - isResolving( 'getOption', [ SHOWN_FOR_ACTIONS_OPTION_NAME ] ) || + ! hasFinishedResolution( 'getOption', [ + SHOWN_FOR_ACTIONS_OPTION_NAME, + ] ) || storeAgeInWeeks === null || - isResolving( 'getOption', [ + ! hasFinishedResolution( 'getOption', [ ADMIN_INSTALL_TIMESTAMP_OPTION_NAME, ] ) || - isResolving( 'getOption', [ ALLOW_TRACKING_OPTION_NAME ] ); + ! hasFinishedResolution( 'getOption', [ + ALLOW_TRACKING_OPTION_NAME, + ] ); return { cesShownForActions, diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js index cfff81e1d2a..90841bc30d6 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/actions.js @@ -26,6 +26,8 @@ export function setCesSurveyQueue( queue ) { * @param {Object} args All arguments. * @param {string} args.action action name for the survey * @param {string} args.title title for the snackback + * @param {string} args.description description for feedback modal. + * @param {string} args.noticeLabel noticeLabel for notice. * @param {string} args.firstQuestion first question for modal survey * @param {string} args.secondQuestion second question for modal survey * @param {string} args.pageNow value of window.pagenow @@ -36,6 +38,8 @@ export function setCesSurveyQueue( queue ) { export function addCesSurvey( { action, title, + description, + noticeLabel, firstQuestion, secondQuestion, pageNow = window.pagenow, @@ -47,6 +51,8 @@ export function addCesSurvey( { type: TYPES.ADD_CES_SURVEY, action, title, + description, + noticeLabel, firstQuestion, secondQuestion, pageNow, diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js index be98c28f74d..bbd0b6101fb 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/data/reducer.js @@ -14,7 +14,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => { case TYPES.SET_CES_SURVEY_QUEUE: return { ...state, - queue: action.queue, + queue: [ ...state.queue, ...action.queue ], }; case TYPES.HIDE_CES_MODAL: return { @@ -48,6 +48,8 @@ const reducer = ( state = DEFAULT_STATE, action ) => { const newTrack = { action: action.action, title: action.title, + description: action.description, + noticeLabel: action.noticeLabel, firstQuestion: action.firstQuestion, secondQuestion: action.secondQuestion, pagenow: action.pageNow, diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts new file mode 100644 index 00000000000..5693325bb1e --- /dev/null +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/test/customer-effort-score-exit-page.test.ts @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { triggerExitPageCesSurvey } from '../customer-effort-score-exit-page'; + +jest.mock( '@woocommerce/data', () => ( { + OPTIONS_STORE_NAME: 'options', +} ) ); +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + useSelect: jest.fn(), + dispatch: jest.fn(), + resolveSelect: jest.fn().mockReturnValue( { + getOption: jest.fn().mockResolvedValue( 'yes' ), + } ), +} ) ); + +describe( 'triggerExitPageCesSurvey', () => { + const addCESSurveyMock = jest.fn(); + beforeEach( () => { + jest.clearAllMocks(); + ( dispatch as jest.Mock ).mockReturnValue( { + addCesSurvey: addCESSurveyMock, + } ); + } ); + + it( 'should not trigger addCESSurvey if local storage is empty', () => { + triggerExitPageCesSurvey(); + expect( addCESSurveyMock ).not.toHaveBeenCalled(); + } ); + + it( 'should not trigger addCESSurvey if copy does not exist for item, but clear localStorage still', () => { + window.localStorage.setItem( + 'customer-effort-score-exit-page', + JSON.stringify( [ 'random-id' ] ) + ); + triggerExitPageCesSurvey(); + expect( addCESSurveyMock ).not.toHaveBeenCalled(); + const list = window.localStorage.getItem( + 'customer-effort-score-exit-page' + ); + expect( list ).toEqual( '[]' ); + } ); + + it( 'should trigger addCESSurvey if copy does exist for item, and clear localStorage still', () => { + window.localStorage.setItem( + 'customer-effort-score-exit-page', + JSON.stringify( [ 'new_product' ] ) + ); + triggerExitPageCesSurvey(); + expect( addCESSurveyMock ).toHaveBeenCalled(); + const list = window.localStorage.getItem( + 'customer-effort-score-exit-page' + ); + expect( list ).toEqual( '[]' ); + } ); +} ); diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts new file mode 100644 index 00000000000..60121766783 --- /dev/null +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker.ts @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { useEffect, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { + addCustomerEffortScoreExitPageListener, + addExitPage, + removeCustomerEffortScoreExitPageListener, +} from './customer-effort-score-exit-page'; + +export const useCustomerEffortScoreExitPageTracker = ( + pageId: string, + hasUnsavedChanges: boolean +) => { + const hasUnsavedChangesRef = useRef( hasUnsavedChanges ); + + // Using unmounting as a way to see when the react router changes. + useEffect( () => { + hasUnsavedChangesRef.current = hasUnsavedChanges; + }, [ hasUnsavedChanges ] ); + + useEffect( () => { + return () => { + if ( hasUnsavedChangesRef.current ) { + // unmounted. + addExitPage( pageId ); + } + }; + }, [] ); + + // This effect listen to the native beforeunload event to show + // a confirmation message + useEffect( () => { + addCustomerEffortScoreExitPageListener( + pageId, + () => hasUnsavedChanges + ); + + return () => { + removeCustomerEffortScoreExitPageListener( pageId ); + }; + }, [ hasUnsavedChanges ] ); +}; diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx index ec31ae5fc9c..98e7aa7c676 100644 --- a/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx +++ b/plugins/woocommerce-admin/client/embedded-body-layout/embedded-body-layout.tsx @@ -2,6 +2,7 @@ * External dependencies */ import { applyFilters } from '@wordpress/hooks'; +import { useEffect } from '@wordpress/element'; import QueryString, { parse } from 'qs'; /** @@ -12,6 +13,7 @@ import { ShippingRecommendations } from '../shipping'; import { EmbeddedBodyProps } from './embedded-body-props'; import { StoreAddressTour } from '../guided-tours/store-address-tour'; import './style.scss'; +import { triggerExitPageCesSurvey } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; type QueryParams = EmbeddedBodyProps; @@ -34,6 +36,10 @@ const EMBEDDED_BODY_COMPONENT_LIST: React.ElementType[] = [ * Each Fill component receives QueryParams, consisting of a page, tab, and section string. */ export const EmbeddedBodyLayout = () => { + useEffect( () => { + triggerExitPageCesSurvey(); + }, [] ); + const query = parse( location.search.substring( 1 ) ); let queryParams: QueryParams = { page: '', tab: '' }; if ( isWPPage( query ) ) { diff --git a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx index 1561e2077a7..954d90492e1 100644 --- a/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx +++ b/plugins/woocommerce-admin/client/embedded-body-layout/test/embedded-body-layout.test.tsx @@ -9,6 +9,18 @@ import { addFilter } from '@wordpress/hooks'; */ import { EmbeddedBodyLayout } from '../embedded-body-layout'; +jest.mock( + '~/customer-effort-score-tracks/customer-effort-score-exit-page', + () => ( { + triggerExitPageCesSurvey: jest.fn(), + } ) +); +jest.mock( '@wordpress/data', () => ( { + ...jest.requireActual( '@wordpress/data' ), + resolveSelect: jest.fn().mockReturnValue( { + getOption: jest.fn(), + } ), +} ) ); jest.mock( '@woocommerce/data', () => ( { useUser: () => ( { currentUserCan: jest.fn(), diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js index 7e4f299b638..51c928048b2 100644 --- a/plugins/woocommerce-admin/client/layout/index.js +++ b/plugins/woocommerce-admin/client/layout/index.js @@ -39,6 +39,7 @@ import Notices from './notices'; import TransientNotices from './transient-notices'; import { CustomerEffortScoreModalContainer } from '../customer-effort-score-tracks'; import { getAdminSetting } from '~/utils/admin-settings'; +import { triggerExitPageCesSurvey } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; import '~/activity-panel'; import '~/mobile-banner'; import './navigation'; @@ -135,6 +136,7 @@ class _Layout extends Component { componentDidMount() { this.recordPageViewTrack(); + triggerExitPageCesSurvey(); } componentDidUpdate( prevProps ) { @@ -147,6 +149,9 @@ class _Layout extends Component { if ( previousPath !== currentPath ) { this.recordPageViewTrack(); + setTimeout( () => { + triggerExitPageCesSurvey(); + }, 0 ); } } diff --git a/plugins/woocommerce-admin/client/products/product-form-actions.tsx b/plugins/woocommerce-admin/client/products/product-form-actions.tsx index b0960543bae..47c7f4c63c6 100644 --- a/plugins/woocommerce-admin/client/products/product-form-actions.tsx +++ b/plugins/woocommerce-admin/client/products/product-form-actions.tsx @@ -29,6 +29,7 @@ import { WooHeaderItem } from '~/header/utils'; import { useProductHelper } from './use-product-helper'; import './product-form-actions.scss'; import { useProductMVPCESFooter } from '~/customer-effort-score-tracks/use-product-mvp-ces-footer'; +import { useCustomerEffortScoreExitPageTracker } from '~/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker'; export const ProductFormActions: React.FC = () => { const { @@ -47,6 +48,10 @@ export const ProductFormActions: React.FC = () => { useFormContext< Product >(); usePreventLeavingPage( isDirty ); + useCustomerEffortScoreExitPageTracker( + ! values.id ? 'new_product' : 'editing_new_product', + isDirty + ); const { isSmallViewport } = useSelect( ( select ) => { return { diff --git a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx index d69ee933880..a72db8cf7b9 100644 --- a/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx +++ b/plugins/woocommerce-admin/client/products/test/product-form-actions.spec.tsx @@ -56,6 +56,12 @@ jest.mock( '../use-product-helper', () => { }; } ); jest.mock( '~/hooks/usePreventLeavingPage' ); +jest.mock( + '~/customer-effort-score-tracks/use-customer-effort-score-exit-page-tracker', + () => ( { + useCustomerEffortScoreExitPageTracker: jest.fn(), + } ) +); describe( 'ProductFormActions', () => { beforeEach( () => { diff --git a/plugins/woocommerce-admin/client/typings/global.d.ts b/plugins/woocommerce-admin/client/typings/global.d.ts index bd72869cee2..fae26135f04 100644 --- a/plugins/woocommerce-admin/client/typings/global.d.ts +++ b/plugins/woocommerce-admin/client/typings/global.d.ts @@ -1,5 +1,7 @@ declare global { interface Window { + pagenow: string; + adminpage: string; wcSettings: { preloadOptions: Record< string, unknown >; adminUrl: string; @@ -32,6 +34,19 @@ declare global { 'shipping-smart-defaults': boolean; 'shipping-setting-tour': boolean; }; + wp: { + autosave?: { + server: { + postChanged: () => boolean; + }; + }; + }; + tinymce?: { + get: ( name: string ) => { + isHidden: () => boolean; + isDirty: () => boolean; + }; + }; } } diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-edit.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-edit.ts index 98473e18c63..f056efcbcd1 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-edit.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-edit.ts @@ -6,7 +6,7 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { initProductScreenTracks } from './shared'; +import { addExitPageListener, initProductScreenTracks } from './shared'; const initTracks = () => { recordEvent( 'product_edit_view' ); @@ -15,4 +15,6 @@ const initTracks = () => { if ( productScreen && productScreen.name === 'edit' ) { initTracks(); + + addExitPageListener( 'product_edit_view' ); } diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-new.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-new.ts index 3283d80b394..bd4d3acdb11 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-new.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/product-new.ts @@ -6,7 +6,7 @@ import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import { initProductScreenTracks } from './shared'; +import { addExitPageListener, initProductScreenTracks } from './shared'; const initTracks = () => { recordEvent( 'product_add_view' ); @@ -15,4 +15,6 @@ const initTracks = () => { if ( productScreen && productScreen.name === 'new' ) { initTracks(); initProductScreenTracks(); + + addExitPageListener( 'product_add_view' ); } diff --git a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts index cb9f5a5a672..4b93c5acc5d 100644 --- a/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts +++ b/plugins/woocommerce-admin/client/wp-admin-scripts/product-tracking/shared.ts @@ -7,6 +7,7 @@ import { recordEvent } from '@woocommerce/tracks'; * Internal dependencies */ import { waitUntilElementIsPresent } from './utils'; +import { addCustomerEffortScoreExitPageListener } from '~/customer-effort-score-tracks/customer-effort-score-exit-page'; /** * Get the product data. @@ -444,3 +445,51 @@ export const initProductScreenTracks = () => { } ); } ); }; + +export function addExitPageListener( pageId: string ) { + let productChanged = false; + let triggeredDelete = false; + + const deleteButton = document.querySelector( '#submitpost a.submitdelete' ); + + if ( deleteButton ) { + deleteButton.addEventListener( 'click', function () { + triggeredDelete = true; + } ); + } + + function checkIfSubmitButtonsDisabled() { + const submitButtonSelectors = [ + '#submitpost [type="submit"]', + '#submitpost #post-preview', + ]; + let isDisabled = false; + for ( const sel of submitButtonSelectors ) { + document.querySelectorAll( sel ).forEach( ( element ) => { + if ( element.classList.contains( 'disabled' ) ) { + isDisabled = true; + } + } ); + } + return isDisabled; + } + window.addEventListener( 'beforeunload', function ( event ) { + // Check if button disabled or triggered delete to see if user saved or deleted the product instead. + if ( checkIfSubmitButtonsDisabled() || triggeredDelete ) { + productChanged = false; + triggeredDelete = false; + return; + } + const editor = window.tinymce && window.tinymce.get( 'content' ); + + if ( window.wp.autosave ) { + productChanged = window.wp.autosave.server.postChanged(); + } else if ( editor ) { + productChanged = ! editor.isHidden() && editor.isDirty(); + } + } ); + + addCustomerEffortScoreExitPageListener( pageId, () => { + return productChanged; + } ); +} diff --git a/plugins/woocommerce/changelog/add-35126_ces_exit_prompt b/plugins/woocommerce/changelog/add-35126_ces_exit_prompt new file mode 100644 index 00000000000..d03f219ab23 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35126_ces_exit_prompt @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add exit prompt logic to get feedback if users leave product pages without saving when tracking is enabled. From 3cc47d245d77b63383e4d2e1d3817b7f688cd3bf Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Tue, 13 Dec 2022 10:22:13 +1300 Subject: [PATCH 011/370] Allow use of pathspecs to limit the scope of generating a diff in Code Analyzer (#35925) --- tools/cli-core/src/git.ts | 43 ++++++++++++++----- tools/code-analyzer/src/lib/scan-changes.ts | 3 +- .../code-analyzer/src/lib/template-changes.ts | 3 +- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/tools/cli-core/src/git.ts b/tools/cli-core/src/git.ts index 952e438868a..59edd5f96e5 100644 --- a/tools/cli-core/src/git.ts +++ b/tools/cli-core/src/git.ts @@ -125,13 +125,29 @@ export const checkoutRef = ( pathToRepo: string, ref: string ) => { /** * Do a git diff of 2 commit hashes (or branches) * - * @param {string} baseDir - baseDir that the repo is in - * @param {string} hashA - either a git commit hash or a git branch - * @param {string} hashB - either a git commit hash or a git branch + * @param {string} baseDir - baseDir that the repo is in + * @param {string} hashA - either a git commit hash or a git branch + * @param {string} hashB - either a git commit hash or a git branch + * @param {Array} excludePaths - A list of paths to exclude from the diff * @return {Promise} - diff of the changes between the 2 hashes */ -export const diffHashes = ( baseDir: string, hashA: string, hashB: string ) => { +export const diffHashes = ( + baseDir: string, + hashA: string, + hashB: string, + excludePaths: string[] = [] +) => { const git = simpleGit( { baseDir } ); + + if ( excludePaths.length ) { + return git.diff( [ + `${ hashA }..${ hashB }`, + '--', + '.', + ...excludePaths.map( ( ps ) => `:^${ ps }` ), + ] ); + } + return git.diff( [ `${ hashA }..${ hashB }` ] ); }; @@ -177,16 +193,18 @@ export const getCommitHash = async ( baseDir: string, ref: string ) => { /** * generateDiff generates a diff for a given repo and 2 hashes or branch names. * - * @param {string} tmpRepoPath - filepath to the repo to generate a diff from. - * @param {string} hashA - commit hash or branch name. - * @param {string} hashB - commit hash or branch name. - * @param {Function} onError - the handler to call when an error occurs. + * @param {string} tmpRepoPath - filepath to the repo to generate a diff from. + * @param {string} hashA - commit hash or branch name. + * @param {string} hashB - commit hash or branch name. + * @param {Function} onError - the handler to call when an error occurs. + * @param {Array} excludePaths - A list of directories to exclude from the diff. */ export const generateDiff = async ( tmpRepoPath: string, hashA: string, hashB: string, - onError: ( error: string ) => void + onError: ( error: string ) => void, + excludePaths: string[] = [] ) => { try { const git = simpleGit( { baseDir: tmpRepoPath } ); @@ -213,7 +231,12 @@ export const generateDiff = async ( throw new Error( 'Not a git repository' ); } - const diff = await diffHashes( tmpRepoPath, commitHashA, commitHashB ); + const diff = await diffHashes( + tmpRepoPath, + commitHashA, + commitHashB, + excludePaths + ); return diff; } catch ( e ) { diff --git a/tools/code-analyzer/src/lib/scan-changes.ts b/tools/code-analyzer/src/lib/scan-changes.ts index 4ed137f97d3..f89e95069e9 100644 --- a/tools/code-analyzer/src/lib/scan-changes.ts +++ b/tools/code-analyzer/src/lib/scan-changes.ts @@ -36,7 +36,8 @@ export const scanForChanges = async ( tmpRepoPath, base, compareVersion, - Logger.error + Logger.error, + [ 'tools' ] ); // Only checkout the compare version if we're in CLI mode. diff --git a/tools/code-analyzer/src/lib/template-changes.ts b/tools/code-analyzer/src/lib/template-changes.ts index be3885e104b..abecd850a5a 100644 --- a/tools/code-analyzer/src/lib/template-changes.ts +++ b/tools/code-analyzer/src/lib/template-changes.ts @@ -23,14 +23,15 @@ export const scanForTemplateChanges = ( content: string, version: string ) => { /\./g, '\\.' ) }).*`; + const versionRegex = new RegExp( matchVersion, 'g' ); for ( const p in patches ) { const patch = patches[ p ]; const lines = patch.split( '\n' ); const filePath = getFilename( lines[ 0 ] ); - let code = 'warning'; + let code = 'warning'; let message = 'This template may require a version bump!'; for ( const l in lines ) { From 7224c760537359609e0d54c2611575fc915e3100 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Tue, 13 Dec 2022 10:27:42 +1300 Subject: [PATCH 012/370] Add a set_output function to script that uses the new GITHUB_OUTPUT (#35894) --- .../workflows/scripts/release-code-freeze.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/scripts/release-code-freeze.php b/.github/workflows/scripts/release-code-freeze.php index cc0ebe197c7..210e4f5767b 100644 --- a/.github/workflows/scripts/release-code-freeze.php +++ b/.github/workflows/scripts/release-code-freeze.php @@ -1,4 +1,5 @@ Date: Mon, 12 Dec 2022 17:54:51 -0600 Subject: [PATCH 013/370] Delete changelog files based on PR 35669 (#35945) Delete changelog files for 35669 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-35535 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-35535 diff --git a/plugins/woocommerce/changelog/fix-35535 b/plugins/woocommerce/changelog/fix-35535 deleted file mode 100644 index a476e006729..00000000000 --- a/plugins/woocommerce/changelog/fix-35535 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add a data migration for changed New Zealand and Ukraine state codes From 6fe4afad1467e31a594e76fe73052c5982e1371b Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Tue, 13 Dec 2022 01:17:35 +0100 Subject: [PATCH 014/370] Move CSS about notice outside of .woocommerce class scope (#35912) --- .../changelog/fix-notice-css-mini-cart | 4 + .../client/legacy/css/twenty-nineteen.scss | 134 +++++++------- .../client/legacy/css/twenty-seventeen.scss | 116 ++++++------- .../client/legacy/css/twenty-twenty-one.scss | 92 +++++----- .../legacy/css/twenty-twenty-three.scss | 118 ++++++------- .../client/legacy/css/twenty-twenty-two.scss | 110 ++++++------ .../client/legacy/css/twenty-twenty.scss | 164 ++++++++++-------- 7 files changed, 381 insertions(+), 357 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-notice-css-mini-cart diff --git a/plugins/woocommerce/changelog/fix-notice-css-mini-cart b/plugins/woocommerce/changelog/fix-notice-css-mini-cart new file mode 100644 index 00000000000..2d74340c656 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-notice-css-mini-cart @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Move CSS about notice outside of .woocommerce class scope diff --git a/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss b/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss index eb4a61e2850..fd257184f74 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss @@ -130,73 +130,6 @@ a.button { } } -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - margin-bottom: 1.5rem; - padding: 1rem; - background: #eee; - font-size: 0.88889em; - font-family: $headings; - list-style: none; - overflow: hidden; -} - -.woocommerce-message { - background: #eee; - color: $body-color; -} - -.woocommerce-error, -.woocommerce-info { - color: #fff; - - a { - color: #fff; - - &:hover { - color: #fff; - } - - &.button { - background: #111; - } - } -} - -.woocommerce-error { - background: firebrick; -} - -.woocommerce-info { - background: $highlights-color; -} - -.woocommerce-store-notice { - background: $highlights-color; - color: #fff; - padding: 1rem; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - -.woocommerce-store-notice__dismiss-link { - float: right; - color: #fff; - - &:hover { - text-decoration: underline; - color: #fff; - } -} - /** * Tables */ @@ -1373,3 +1306,70 @@ table.variations { } } } + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + margin-bottom: 1.5rem; + padding: 1rem; + background: #eee; + font-size: 0.88889em; + font-family: $headings; + list-style: none; + overflow: hidden; +} + +.woocommerce-message { + background: #eee; + color: $body-color; +} + +.woocommerce-error, +.woocommerce-info { + color: #fff; + + a { + color: #fff; + + &:hover { + color: #fff; + } + + &.button { + background: #111; + } + } +} + +.woocommerce-error { + background: firebrick; +} + +.woocommerce-info { + background: $highlights-color; +} + +.woocommerce-store-notice { + background: $highlights-color; + color: #fff; + padding: 1rem; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #fff; + + &:hover { + text-decoration: underline; + color: #fff; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss b/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss index 1defec7a253..f2c0af5bb9f 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss @@ -176,64 +176,6 @@ } } -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - margin-bottom: 1.5em; - padding: 2em; - background: #eee; -} - -.woocommerce-message { - background: teal; - color: #fff; -} - -.woocommerce-error { - background: firebrick; - color: #fff; -} - -.woocommerce-info { - background: royalblue; - color: #fff; -} - -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - - a { - - @include link_white(); - } -} - -.woocommerce-store-notice { - background: royalblue; - color: #fff; - padding: 1em; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - -.woocommerce-store-notice__dismiss-link { - float: right; - color: #fff; - - &:hover { - text-decoration: underline; - color: #fff; - } -} - /** * Shop page */ @@ -1279,3 +1221,61 @@ table.variations { width: 78%; } } + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + margin-bottom: 1.5em; + padding: 2em; + background: #eee; +} + +.woocommerce-message { + background: teal; + color: #fff; +} + +.woocommerce-error { + background: firebrick; + color: #fff; +} + +.woocommerce-info { + background: royalblue; + color: #fff; +} + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + + a { + + @include link_white(); + } +} + +.woocommerce-store-notice { + background: royalblue; + color: #fff; + padding: 1em; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #fff; + + &:hover { + text-decoration: underline; + color: #fff; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss index 4972c415632..199deae3fc8 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss @@ -239,47 +239,6 @@ a.button { } -.woocommerce-message, -.woocommerce-error li, -.woocommerce-info { - padding: 1.5rem 3rem; - display: flex; - justify-content: space-between; - align-items: center; - - .button { - order: 2; - } -} - -.woocommerce-info { - border-top-color: var( --wc-blue ); -} - -.woocommerce-error { - border-top-color: #b22222; - - > li { - margin: 0; - } -} - -.woocommerce-store-notice { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - padding: 2rem; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - .woocommerce-store-notice__dismiss-link { float: right; color: #000; @@ -3067,3 +3026,54 @@ a.reset_variations { } } } + +.woocommerce-message, +.woocommerce-error li, +.woocommerce-info { + padding: 1.5rem 3rem; + display: flex; + justify-content: space-between; + align-items: center; + + .button { + order: 2; + } +} + +.woocommerce-info { + border-top-color: var( --wc-blue ); +} + +.woocommerce-error { + border-top-color: #b22222; + + > li { + margin: 0; + } +} + +.woocommerce-store-notice { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + padding: 2rem; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #000; + + &:hover { + text-decoration: none; + color: #000; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss index 862f78ea0ab..d33508ed3bd 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss @@ -110,65 +110,6 @@ .woocommerce-breadcrumb { margin-bottom: 1rem; } - - /* - Notice messages (like 'Added to cart', 'Billing address needs to be filled in', etc. - */ - .woocommerce-message, - .woocommerce-error, - .woocommerce-info { - background-color: rgba(176, 176, 176, 0.6); - color: #222; - border-top-color: var(--wp--preset--color--primary); - border-top-style: solid; - border-top-width: 2px; - padding: 1rem 1.5rem; - margin-bottom: 2rem; - list-style: none; - font-size: var(--wp--preset--font-size--small); - display: flow-root; - - &[role="alert"]::before { - background: #d5d5d5; - color: black; - border-radius: 5rem; - font-size: 1rem; - padding-left: 3px; - padding-right: 3px; - margin-right: 1rem; - } - - a { - color: var(--wp--preset--color--contrast); - - .button { - margin-top: -0.5rem; - border: none; - padding: 0.5rem 1rem; - } - } - } - - .woocommerce-error[role="alert"] { - margin: 0; - - &::before { - content: "X"; - padding-right: 4px; - padding-left: 4px; - } - - li { - display: inline-block; - } - } - - .woocommerce-message { - &[role="alert"]::before { - content: "\2713"; - } - } - // Checkout notice group styling. .woocommerce-NoticeGroup-checkout { ul.woocommerce-error[role="alert"] { @@ -1172,3 +1113,62 @@ color: inherit; } } + +/* + Notice messages (like 'Added to cart', 'Billing address needs to be filled in', etc. + */ + .woocommerce-message, + .woocommerce-error, + .woocommerce-info { + background-color: rgba( 176, 176, 176, 0.6 ); + color: #222; + border-top-color: var( --wp--preset--color--primary ); + border-top-style: solid; + border-top-width: 2px; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + list-style: none; + font-size: var( --wp--preset--font-size--small ); + display: flow-root; + + &[role='alert']::before { + background: #d5d5d5; + color: black; + border-radius: 5rem; + font-size: 1rem; + padding-left: 3px; + padding-right: 3px; + margin-right: 1rem; + } + + a { + color: var( --wp--preset--color--contrast ); + + .button { + margin-top: -0.5rem; + border: none; + padding: 0.5rem 1rem; + } + } + } + + .woocommerce-error[role='alert'] { + margin: 0; + + &::before { + content: 'X'; + padding-right: 4px; + padding-left: 4px; + } + + li { + display: inline-block; + } + } + + .woocommerce-message { + &[role='alert']::before { + content: '\2713'; + } + } + diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss index 37e8d7a01c0..3105e01ce15 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss @@ -97,61 +97,6 @@ $tt2-gray: #f7f7f7; margin-bottom: 1rem; } - .woocommerce-message, - .woocommerce-error, - .woocommerce-info { - background: $tt2-gray; - border-top-color: var(--wp--preset--color--primary); - border-top-style: solid; - padding: 1rem 1.5rem; - margin-bottom: 2rem; - list-style: none; - font-size: var(--wp--preset--font-size--small); - - &[role="alert"]::before { - color: var(--wp--preset--color--background); - background: var(--wp--preset--color--primary); - border-radius: 5rem; - font-size: 1rem; - padding-left: 3px; - padding-right: 3px; - margin-right: 1rem; - } - - a.button { - margin-top: -0.5rem; - border: none; - background: #ebe9eb; - color: var(--wp--preset--color--black); - padding: 0.5rem 1rem; - - &:hover, - &:visited { - color: var(--wp--preset--color--black); - } - } - } - - .woocommerce-error[role="alert"] { - margin: 0; - - &::before { - content: "X"; - padding-right: 4px; - padding-left: 4px; - } - - li { - display: inline-block; - } - } - - .woocommerce-message { - &[role="alert"]::before { - content: "\2713"; - } - } - .woocommerce-NoticeGroup-checkout { ul.woocommerce-error[role="alert"] { &::before { @@ -1213,3 +1158,58 @@ $tt2-gray: #f7f7f7; color: inherit; } } + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + background: $tt2-gray; + border-top-color: var(--wp--preset--color--primary); + border-top-style: solid; + padding: 1rem 1.5rem; + margin-bottom: 2rem; + list-style: none; + font-size: var(--wp--preset--font-size--small); + + &[role="alert"]::before { + color: var(--wp--preset--color--background); + background: var(--wp--preset--color--primary); + border-radius: 5rem; + font-size: 1rem; + padding-left: 3px; + padding-right: 3px; + margin-right: 1rem; + } + + a.button { + margin-top: -0.5rem; + border: none; + background: #ebe9eb; + color: var(--wp--preset--color--black); + padding: 0.5rem 1rem; + + &:hover, + &:visited { + color: var(--wp--preset--color--black); + } + } +} + +.woocommerce-error[role="alert"] { + margin: 0; + + &::before { + content: "X"; + padding-right: 4px; + padding-left: 4px; + } + + li { + display: inline-block; + } +} + +.woocommerce-message { + &[role="alert"]::before { + content: "\2713"; + } +} diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty.scss index 3caef90db39..6f91033da6c 100644 --- a/plugins/woocommerce/client/legacy/css/twenty-twenty.scss +++ b/plugins/woocommerce/client/legacy/css/twenty-twenty.scss @@ -183,83 +183,6 @@ a.button { } } -.woocommerce-message, -.woocommerce-error, -.woocommerce-info { - margin-bottom: 5rem; - margin-left: 0; - background: #eee; - color: $body-color; - border-top: 3px solid var( --wc-green ); - - font-size: 0.88889em; - font-family: $headings; - list-style: none; - overflow: hidden; - width: 100%; - - a { - color: #fff; - - &:hover { - color: #fff; - } - - &.button { - background: #000000; - } - } -} - -.woocommerce-message, -.woocommerce-error li, -.woocommerce-info { - padding: 1.5rem 3rem; - display: flex; - justify-content: space-between; - align-items: center; - - .button { - order: 2; - } -} - -.woocommerce-info { - border-color: var( --wc-blue ); -} - -.woocommerce-error { - border-color: $highlights-color; - - > li { - margin: 0; - } -} - -#site-content { - - .woocommerce-error, - .woocommerce-info { - font-family: $headings; - } -} - -.woocommerce-store-notice { - background: #eee; - color: #000; - border-top: 2px solid $highlights-color; - padding: 2rem; - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 999; -} - -.admin-bar .woocommerce-store-notice { - top: 32px; -} - .woocommerce-store-notice__dismiss-link { float: right; color: #000; @@ -2558,3 +2481,90 @@ a.reset_variations { } } } + +.woocommerce-message, +.woocommerce-error, +.woocommerce-info { + margin-bottom: 5rem; + margin-left: 0; + background: #eee; + color: $body-color; + border-top: 3px solid var( --wc-green ); + + font-size: 0.88889em; + font-family: $headings; + list-style: none; + overflow: hidden; + width: 100%; + + a { + color: #fff; + + &:hover { + color: #fff; + } + + &.button { + background: #000000; + } + } +} + +.woocommerce-message, +.woocommerce-error li, +.woocommerce-info { + padding: 1.5rem 3rem; + display: flex; + justify-content: space-between; + align-items: center; + + .button { + order: 2; + } +} + +.woocommerce-info { + border-color: var( --wc-blue ); +} + +.woocommerce-error { + border-color: $highlights-color; + + > li { + margin: 0; + } +} + +#site-content { + + .woocommerce-error, + .woocommerce-info { + font-family: $headings; + } +} + +.woocommerce-store-notice { + background: #eee; + color: #000; + border-top: 2px solid $highlights-color; + padding: 2rem; + position: absolute; + top: 0; + left: 0; + width: 100%; + z-index: 999; +} + +.admin-bar .woocommerce-store-notice { + top: 32px; +} + +.woocommerce-store-notice__dismiss-link { + float: right; + color: #000; + + &:hover { + text-decoration: none; + color: #000; + } +} From f4032654a31fae5d3596114a06222e70be840834 Mon Sep 17 00:00:00 2001 From: Nico Mollet Date: Tue, 13 Dec 2022 01:21:13 +0100 Subject: [PATCH 015/370] Product import: Remove line breaks in keys (#35880) * Product import: Remove line breaks in keys Remove line breaks in keys, to avoid mismatch mapping of keys. * Fix syntax * PHPCS * Changelog. Co-authored-by: barryhughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/changelog/fix-csv-import-header-handling | 4 ++++ .../includes/import/class-wc-product-csv-importer.php | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-csv-import-header-handling diff --git a/plugins/woocommerce/changelog/fix-csv-import-header-handling b/plugins/woocommerce/changelog/fix-csv-import-header-handling new file mode 100644 index 00000000000..d4ab2693fd7 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-csv-import-header-handling @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +When importing product CSV, ensure line breaks within header columns do not break the import process. diff --git a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php index 31a97d379ea..c5f828f7d3a 100644 --- a/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php +++ b/plugins/woocommerce/includes/import/class-wc-product-csv-importer.php @@ -79,6 +79,9 @@ class WC_Product_CSV_Importer extends WC_Product_Importer { if ( false !== $handle ) { $this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine + // Remove line breaks in keys, to avoid mismatch mapping of keys. + $this->raw_keys = wc_clean( wp_unslash( $this->raw_keys ) ); + // Remove BOM signature from the first item. if ( isset( $this->raw_keys[0] ) ) { $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); From 317e2dc164a15b7a5a66e7978a6617d72d148b4f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 18:25:33 -0600 Subject: [PATCH 016/370] Delete changelog files based on PR 35805 (#35879) Delete changelog files for 35805 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.2 | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.2 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.2 b/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.2 deleted file mode 100644 index bb55685787e..00000000000 --- a/plugins/woocommerce/changelog/update-woocommerce-blocks-8.9.2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: update - -Woo Blocks 8.9.2 From 5f9fb16d66e05137e60136f020517e9394692e0e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 18:25:47 -0600 Subject: [PATCH 017/370] Delete changelog files based on PR 35866 (#35870) Delete changelog files for 35866 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-activate-plugin | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-activate-plugin diff --git a/plugins/woocommerce/changelog/fix-activate-plugin b/plugins/woocommerce/changelog/fix-activate-plugin deleted file mode 100644 index a3d0a3196f2..00000000000 --- a/plugins/woocommerce/changelog/fix-activate-plugin +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix error in onboarding wizard when plugin is activated but includes unexpected output. From 7f000f453c8864cd176e59f9853f8381c8b4c9f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Dec 2022 18:25:59 -0600 Subject: [PATCH 018/370] Delete changelog files based on PR 35767 (#35812) Delete changelog files for 35767 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-34282-min-max-attrs | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-34282-min-max-attrs diff --git a/plugins/woocommerce/changelog/fix-34282-min-max-attrs b/plugins/woocommerce/changelog/fix-34282-min-max-attrs deleted file mode 100644 index 054cbab1809..00000000000 --- a/plugins/woocommerce/changelog/fix-34282-min-max-attrs +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: tweak -Comment: This feature has not been released yet (and a changelog was added in PR#34282). This change simply amends things to improve compatibility with Composite Products. - - From 131183597f0eba66092e3fefd0938742b8f60276 Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 12 Dec 2022 18:48:22 -0800 Subject: [PATCH 019/370] Fix RegExp used to filter the country list dropdown on the store details step (#35942) * Add custom regexp for the country dropdown search * Add changelog --- .../dashboard/components/settings/general/store-address.tsx | 3 +++ .../changelog/fix-regex-used-to-filter-country-dropdown | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-regex-used-to-filter-country-dropdown diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx index 166041c6d61..de8c9c40ddd 100644 --- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx +++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.tsx @@ -357,6 +357,9 @@ export function StoreAddress( { label={ __( 'Country / Region', 'woocommerce' ) + ' *' } required autoComplete="new-password" // disable autocomplete and autofill + getSearchExpression={ ( query: string ) => { + return new RegExp( '^' + query, 'i' ); + } } options={ countryStateOptions } excludeSelectedOptions={ false } showAllOnFocus diff --git a/plugins/woocommerce/changelog/fix-regex-used-to-filter-country-dropdown b/plugins/woocommerce/changelog/fix-regex-used-to-filter-country-dropdown new file mode 100644 index 00000000000..5cd85a71fb0 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-regex-used-to-filter-country-dropdown @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix regexp used for filtering country dropdown on the store details step #35941 \ No newline at end of file From e4f6c468cb205936da42143a1714ebc81f1045c0 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Tue, 13 Dec 2022 08:03:45 -0300 Subject: [PATCH 020/370] Automatically show attributes in Variations (#35807) * Automatically show attributes in Variations # Conflicts: # plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js * Add changelog * Use `attribute_taxonomy` instead of `wc-attribute-search`. # Conflicts: # plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js * Rename `add-attribute-used-for-variations` Co-authored-by: Fernando Marichal --- ...-33551_streamline_first_variation_creation | 4 +++ .../js/admin/meta-boxes-product-variation.js | 36 +++++++++++++++---- .../legacy/js/admin/meta-boxes-product.js | 8 ++++- .../views/html-product-attribute.php | 2 +- 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/dev-33551_streamline_first_variation_creation diff --git a/plugins/woocommerce/changelog/dev-33551_streamline_first_variation_creation b/plugins/woocommerce/changelog/dev-33551_streamline_first_variation_creation new file mode 100644 index 00000000000..6ea52390c8e --- /dev/null +++ b/plugins/woocommerce/changelog/dev-33551_streamline_first_variation_creation @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Automatically show attributes in Variations diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js index 84eab9d8bd5..53e16d05c41 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product-variation.js @@ -49,6 +49,10 @@ jQuery( function ( $ ) { '.wc_input_variations_price', this.maybe_enable_button_to_add_price_to_variations ); + $( 'ul.wc-tabs a[href=#variable_product_options]' ).on( + 'click', + this.maybe_add_attributes_to_variations + ); }, /** @@ -373,6 +377,24 @@ jQuery( function ( $ ) { } ); }, + + /** + * Maybe add attributes to variations + */ + maybe_add_attributes_to_variations: function () { + var has_variation_attributes = $( + 'select.attribute_taxonomy' + ).data( 'is-used-for-variations' ); + if ( has_variation_attributes ) { + wc_meta_boxes_product_variations_ajax.link_all_variations( + true + ); + $( 'select.attribute_taxonomy' ).data( + 'is-used-for-variations', + false + ); + } + }, }; /** @@ -995,14 +1017,16 @@ jQuery( function ( $ ) { * * @return {Bool} */ - link_all_variations: function () { + link_all_variations: function ( auto_link_variations = false ) { wc_meta_boxes_product_variations_ajax.check_for_changes(); - if ( - window.confirm( - woocommerce_admin_meta_boxes_variations.i18n_link_all_variations - ) - ) { + var is_confirmed_action = auto_link_variations + ? true + : window.confirm( + woocommerce_admin_meta_boxes_variations.i18n_link_all_variations + ); + + if ( is_confirmed_action ) { wc_meta_boxes_product_variations_ajax.block(); var data = { diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js index 4a9f5b7af06..4e342761f68 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js @@ -771,6 +771,13 @@ jQuery( function ( $ ) { 'disabled-items', newSelectedAttributes ); + var isUsedForVariations = $( 'input#used-for-variation' ).is( + ':checked' + ); + $( 'select.attribute_taxonomy' ).data( + 'is-used-for-variations', + isUsedForVariations + ); // Reload variations panel. var this_page = window.location.toString(); @@ -1035,7 +1042,6 @@ jQuery( function ( $ ) { keepAlive: true, } ); - // add a tooltip to the right of the product image meta box "Set product image" and "Add product gallery images" const setProductImageLink = $( '#set-post-thumbnail' ); const tooltipMarkup = ``; diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php index 02007bc13ae..cec626d8d7b 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php @@ -83,7 +83,7 @@ if ( ! defined( 'ABSPATH' ) ) {
- +
From 4f692a51d2979a9352e8bc1d33b2771c328b09f1 Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Tue, 13 Dec 2022 09:39:05 -0500 Subject: [PATCH 021/370] [HPOS] Improve handling of "visible" statuses in orders list (#35370) --- plugins/woocommerce/changelog/fix-35362 | 4 + .../includes/wc-order-functions.php | 20 +++-- .../src/Internal/Admin/Orders/ListTable.php | 81 ++++++++++++------- .../order/class-wc-tests-order-functions.php | 55 +++++++++++++ 4 files changed, 125 insertions(+), 35 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-35362 diff --git a/plugins/woocommerce/changelog/fix-35362 b/plugins/woocommerce/changelog/fix-35362 new file mode 100644 index 00000000000..53849b5e827 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35362 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix handling of statuses in orders list table (HPOS). diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php index cccfc5e7c56..1454c5a3771 100644 --- a/plugins/woocommerce/includes/wc-order-functions.php +++ b/plugins/woocommerce/includes/wc-order-functions.php @@ -338,25 +338,31 @@ function wc_processing_order_count() { * Return the orders count of a specific order status. * * @param string $status Status. + * @param string $type (Optional) Order type. Leave empty to include all 'for order-count' order types. @{see wc_get_order_types()}. * @return int */ -function wc_orders_count( $status ) { - $count = 0; - $status = 'wc-' . $status; - $order_statuses = array_keys( wc_get_order_statuses() ); +function wc_orders_count( $status, string $type = '' ) { + $count = 0; + $legacy_statuses = array( 'draft', 'trash' ); + $valid_statuses = array_merge( array_keys( wc_get_order_statuses() ), $legacy_statuses ); + $status = ( ! in_array( $status, $legacy_statuses, true ) && 0 !== strpos( $status, 'wc-' ) ) ? 'wc-' . $status : $status; + $valid_types = wc_get_order_types( 'order-count' ); + $type = trim( $type ); - if ( ! in_array( $status, $order_statuses, true ) ) { + if ( ! in_array( $status, $valid_statuses, true ) || ( $type && ! in_array( $type, $valid_types, true ) ) ) { return 0; } - $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status; + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status . $type; $cached_count = wp_cache_get( $cache_key, 'counts' ); if ( false !== $cached_count ) { return $cached_count; } - foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $types_for_count = $type ? array( $type ) : $valid_types; + + foreach ( $types_for_count as $type ) { $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); if ( $data_store ) { $count += $data_store->get_order_count( $status ); diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php index 11f474e72dc..424d9f65c07 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php @@ -161,18 +161,19 @@ class ListTable extends WP_List_Table {
" ); - if ( $this->has_items() || $this->has_filter ) { - $this->views(); - - echo '
'; - $this->print_hidden_form_fields(); - $this->search_box( esc_html__( 'Search orders', 'woocommerce' ), 'orders-search-input' ); - - parent::display(); - echo '
'; - } else { + if ( $this->should_render_blank_state() ) { $this->render_blank_state(); + return; } + + $this->views(); + + echo '
'; + $this->print_hidden_form_fields(); + $this->search_box( esc_html__( 'Search orders', 'woocommerce' ), 'orders-search-input' ); + + parent::display(); + echo '
'; } /** @@ -387,25 +388,22 @@ class ListTable extends WP_List_Table { public function get_views() { $view_counts = array(); $view_links = array(); - $statuses = wc_get_order_statuses(); + $statuses = $this->get_visible_statuses(); $current = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['status'] ?? '' ) ) : 'all'; + $all_count = 0; - // Add 'draft' and 'trash' to list. - foreach ( array( 'draft', 'trash' ) as $wp_status ) { - $statuses[ $wp_status ] = ( get_post_status_object( $wp_status ) )->label; - } - - $statuses_in_list = array_intersect( array_keys( $statuses ), get_post_stati( array( 'show_in_admin_status_list' => true ) ) ); - - foreach ( $statuses_in_list as $slug ) { + foreach ( array_keys( $statuses ) as $slug ) { $total_in_status = $this->count_orders_by_status( $slug ); if ( $total_in_status > 0 ) { $view_counts[ $slug ] = $total_in_status; } + + if ( ( get_post_status_object( $slug ) )->show_in_admin_all_list ) { + $all_count += $total_in_status; + } } - $all_count = array_sum( $view_counts ); $view_links['all'] = $this->get_view_link( 'all', __( 'All', 'woocommerce' ), $all_count, '' === $current || 'all' === $current ); foreach ( $view_counts as $slug => $count ) { @@ -418,20 +416,47 @@ class ListTable extends WP_List_Table { /** * Count orders by status. * - * @param string $status The order status we are interested in. + * @param string|string[] $status The order status we are interested in. * * @return int */ - private function count_orders_by_status( string $status ): int { - $orders = wc_get_orders( - array( - 'limit' => -1, - 'return' => 'ids', - 'status' => $status, + private function count_orders_by_status( $status ): int { + return array_sum( + array_map( + function( $order_status ) { + return wc_orders_count( $order_status, 'shop_order' ); + }, + (array) $status ) ); + } - return count( $orders ); + /** + * Checks whether the blank state should be rendered or not. This depends on whether there are others with a visible + * status. + * + * @return boolean TRUE when the blank state should be rendered, FALSE otherwise. + */ + private function should_render_blank_state(): bool { + return ( ! $this->has_filter ) && 0 === $this->count_orders_by_status( array_keys( $this->get_visible_statuses() ) ); + } + + /** + * Returns a list of slug and labels for order statuses that should be visible in the status list. + * + * @return array slug => label array of order statuses. + */ + private function get_visible_statuses(): array { + return array_intersect_key( + array_merge( + wc_get_order_statuses(), + array( + 'trash' => ( get_post_status_object( 'trash' ) )->label, + 'draft' => ( get_post_status_object( 'draft' ) )->label, + ) + ), + array_flip( get_post_stati( array( 'show_in_admin_status_list' => true ) ) ) + ); } /** diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php index 5c4ffd08cb0..1999e4cf626 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php @@ -80,6 +80,61 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case { // Invalid status returns 0. $this->assertEquals( 0, wc_orders_count( 'unkown-status' ) ); + + // Invalid order type should return 0. + $this->assertEquals( 0, wc_orders_count( 'wc-pending', 'invalid-order-type' ) ); + + wp_cache_flush(); + + // Fake some datastores and order types for testing. + $test_counts = array( + 'order' => array( + array( 'wc-on-hold', 2 ), + array( 'trash', 1 ), + ), + 'order-fake-type' => array( + array( 'wc-on-hold', 3 ), + array( 'trash', 0 ), + ), + ); + + $mock_datastores = array(); + foreach ( array( 'order', 'order-fake-type' ) as $order_type ) { + $mock_datastores[ $order_type ] = $this->getMockBuilder( 'Abstract_WC_Order_Data_Store_CPT' ) + ->setMethods( array( 'get_order_count' ) ) + ->getMock(); + + $mock_datastores[ $order_type ] + ->method( 'get_order_count' ) + ->will( $this->returnValueMap( $test_counts[ $order_type ] ) ); + } + + $add_mock_datastores = function( $stores ) use ( $mock_datastores ) { + return array_merge( $stores, $mock_datastores ); + }; + $add_mock_order_type = function( $order_types ) use ( $mock_datastores ) { + return array( 'shop_order', 'order-fake-type' ); + }; + + add_filter( 'woocommerce_data_stores', $add_mock_datastores ); + add_filter( 'wc_order_types', $add_mock_order_type ); + + // Check counts for specific order types. + $this->assertEquals( 2, wc_orders_count( 'on-hold', 'shop_order' ) ); + $this->assertEquals( 1, wc_orders_count( 'trash', 'shop_order' ) ); + $this->assertEquals( 3, wc_orders_count( 'on-hold', 'order-fake-type' ) ); + $this->assertEquals( 0, wc_orders_count( 'trash', 'order-fake-type' ) ); + + // Check that counts with no order type include all order types. + $this->assertEquals( 5, wc_orders_count( 'on-hold' ) ); + $this->assertEquals( 1, wc_orders_count( 'trash' ) ); + + remove_filter( 'woocommerce_data_stores', $add_mock_datastores ); + remove_filter( 'wc_order_types', $add_mock_order_type ); + + // Confirm that everything's back to normal. + wp_cache_flush(); + $this->assertEquals( 0, wc_orders_count( 'on-hold' ) ); } /** From 4eacc67501c04b18c7e93eeb5263bb1672a075a3 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 13 Dec 2022 08:13:12 -0800 Subject: [PATCH 022/370] Add product tabs to product layout (#35862) * Add product form tabs to layout * Move product sections to respective tabs * Add tab styling * Add changelog entry * Scroll to top on tab change * Update font weight on active or inactive tabs * Add blank EOL --- .../abstracts/_variables.scss | 1 + .../products/layout/product-form-layout.scss | 46 +++++++++++++++ .../products/layout/product-form-layout.tsx | 56 ++++++++++++++++++- .../client/products/product-form-tab.tsx | 16 ++++++ .../client/products/product-form.tsx | 21 +++++-- plugins/woocommerce/changelog/add-35706 | 4 ++ 6 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce-admin/client/products/product-form-tab.tsx create mode 100644 plugins/woocommerce/changelog/add-35706 diff --git a/packages/js/internal-style-build/abstracts/_variables.scss b/packages/js/internal-style-build/abstracts/_variables.scss index 4b9d4ee625a..73155c89baf 100644 --- a/packages/js/internal-style-build/abstracts/_variables.scss +++ b/packages/js/internal-style-build/abstracts/_variables.scss @@ -44,6 +44,7 @@ $alert-green: $valid-green; // WordPress defaults $adminbar-height: 32px; $adminbar-height-mobile: 46px; +$admin-menu-width: 160px; // wp-admin colors $wp-admin-background: #f1f1f1; diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss index 10288998954..73255c8fc66 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss +++ b/plugins/woocommerce-admin/client/products/layout/product-form-layout.scss @@ -1,3 +1,5 @@ +$product-form-tabs-height: 56px; + .product-form-layout { max-width: 1032px; margin: 0 auto; @@ -21,4 +23,48 @@ margin-top: $gap-largest + $gap-smaller; } } + + .components-tab-panel__tabs { + position: fixed; + top: $adminbar-height + $header-height; + left: $admin-menu-width; + width: calc(100% - $admin-menu-width); + background: $white; + z-index: 1001; + border-bottom: 1px solid $gray-400; + border-top: 1px solid $gray-400; + padding: 0 var(--large-gap) 0 var(--large-gap); + + @include breakpoint( '<782px' ) { + top: $adminbar-height-mobile + $header-height; + width: 100%; + left: 0; + } + + @include breakpoint( '782px-960px' ) { + width: calc(100% - 36px); + left: 36px; + } + } + + .components-tab-panel__tabs-item { + min-height: $product-form-tabs-height; + padding-left: 0; + padding-right: 0; + margin-right: $gap-large; + font-weight: 400; + + &:last-child { + margin-right: 0; + } + + &.is-active { + font-weight: 600; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) transparent, inset 0 -3px 0 0 var(--wp-admin-theme-color); + } + } +} + +.woocommerce-admin-product-layout .woocommerce-layout__header { + min-height: $header-height + $product-form-tabs-height; } diff --git a/plugins/woocommerce-admin/client/products/layout/product-form-layout.tsx b/plugins/woocommerce-admin/client/products/layout/product-form-layout.tsx index adfb2ef0bce..e6d3bad2685 100644 --- a/plugins/woocommerce-admin/client/products/layout/product-form-layout.tsx +++ b/plugins/woocommerce-admin/client/products/layout/product-form-layout.tsx @@ -1,8 +1,60 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Children, useEffect } from '@wordpress/element'; +import { TabPanel } from '@wordpress/components'; + /** * Internal dependencies */ import './product-form-layout.scss'; -export const ProductFormLayout: React.FC = ( { children } ) => { - return
{ children }
; +export const ProductFormLayout: React.FC< { + children: JSX.Element | JSX.Element[]; +} > = ( { children } ) => { + useEffect( () => { + window.document.body.classList.add( + 'woocommerce-admin-product-layout' + ); + + return () => { + window.document.body.classList.remove( + 'woocommerce-admin-product-layout' + ); + }; + }, [] ); + + const tabs = Children.map( children, ( child: JSX.Element ) => { + if ( child.type.name !== 'ProductFormTab' ) { + return null; + } + return { + name: child.props.name, + title: child.props.title, + }; + } ); + + return ( + ( window.document.documentElement.scrollTop = 0 ) } + > + { ( tab ) => ( + <> + { Children.map( children, ( child: JSX.Element ) => { + if ( + child.type.name !== 'ProductFormTab' || + child.props.name !== tab.name + ) { + return null; + } + return child; + } ) } + + ) } + + ); }; diff --git a/plugins/woocommerce-admin/client/products/product-form-tab.tsx b/plugins/woocommerce-admin/client/products/product-form-tab.tsx new file mode 100644 index 00000000000..9795f664a11 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/product-form-tab.tsx @@ -0,0 +1,16 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +export const ProductFormTab: React.FC< { + name: string; + title: string; + children: JSX.Element | JSX.Element[] | string; +} > = ( { name, title, children } ) => { + const classes = classnames( + 'woocommerce-product-form-tab', + 'woocommerce-product-form-tab__' + name + ); + return
{ children }
; +}; diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx index 5944cae6501..adba8194aa5 100644 --- a/plugins/woocommerce-admin/client/products/product-form.tsx +++ b/plugins/woocommerce-admin/client/products/product-form.tsx @@ -19,6 +19,7 @@ import './product-page.scss'; import { validate } from './product-validation'; import { AttributesSection } from './sections/attributes-section'; import { ProductFormFooter } from './layout/product-form-footer'; +import { ProductFormTab } from './product-form-tab'; export const ProductForm: React.FC< { product?: PartialProduct; @@ -41,12 +42,20 @@ export const ProductForm: React.FC< { > - - - - - - + + + + + + + + + + + + + + diff --git a/plugins/woocommerce/changelog/add-35706 b/plugins/woocommerce/changelog/add-35706 new file mode 100644 index 00000000000..135eeaecfcb --- /dev/null +++ b/plugins/woocommerce/changelog/add-35706 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product tab headers and move sections to respective tabs From 51c33fa351b588ea9d0b4c3bf53025faec3cde02 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Dec 2022 10:25:16 -0600 Subject: [PATCH 023/370] Delete changelog files based on PR 35780 (#35794) Delete changelog files for 35780 Co-authored-by: WooCommerce Bot --- .../woocommerce/changelog/fix-woocommerce-cart-modal-margin | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-woocommerce-cart-modal-margin diff --git a/plugins/woocommerce/changelog/fix-woocommerce-cart-modal-margin b/plugins/woocommerce/changelog/fix-woocommerce-cart-modal-margin deleted file mode 100644 index 5e7e2581ba8..00000000000 --- a/plugins/woocommerce/changelog/fix-woocommerce-cart-modal-margin +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Increased margin so that overflow modal content doesn't clip header From e1aabf2d9d84141da5600d1920ea06492c66d016 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Tue, 13 Dec 2022 15:29:05 -0800 Subject: [PATCH 024/370] Add product variations list to new product management experience (#35889) * Add product variations section * Add variations list * Add util to get product stock status * Add variation specific attribute type * Add currency code to header column * Fix up variations header width * Add variations loading state * Add changelog entries * Convert spaces to tabs * Fix status typo * Fix up return type for stock status --- packages/js/data/changelog/add-35772 | 4 + .../js/data/src/product-variations/types.ts | 13 ++- .../products/fields/variations/index.ts | 1 + .../fields/variations/variations.scss | 39 ++++++++ .../products/fields/variations/variations.tsx | 91 +++++++++++++++++++ .../client/products/product-form.tsx | 4 + .../sections/product-variations-section.tsx | 46 ++++++++++ .../products/utils/get-product-status.ts | 2 +- .../utils/get-product-stock-status.ts | 49 ++++++++++ plugins/woocommerce/changelog/add-35772 | 4 + 10 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 packages/js/data/changelog/add-35772 create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/index.ts create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/variations.scss create mode 100644 plugins/woocommerce-admin/client/products/fields/variations/variations.tsx create mode 100644 plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx create mode 100644 plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts create mode 100644 plugins/woocommerce/changelog/add-35772 diff --git a/packages/js/data/changelog/add-35772 b/packages/js/data/changelog/add-35772 new file mode 100644 index 00000000000..289817caff8 --- /dev/null +++ b/packages/js/data/changelog/add-35772 @@ -0,0 +1,4 @@ +Significance: minor +Type: dev + +Update attributes type for product variations data store diff --git a/packages/js/data/src/product-variations/types.ts b/packages/js/data/src/product-variations/types.ts index 69a85bd8f04..2369bce1d8c 100644 --- a/packages/js/data/src/product-variations/types.ts +++ b/packages/js/data/src/product-variations/types.ts @@ -9,7 +9,18 @@ import { DispatchFromMap } from '@automattic/data-stores'; import { CrudActions, CrudSelectors } from '../crud/types'; import { Product, ProductQuery, ReadOnlyProperties } from '../products/types'; -export type ProductVariation = Omit< Product, 'name' | 'slug' >; +export type ProductVariationAttribute = { + id: number; + name: string; + option: string; +}; + +export type ProductVariation = Omit< + Product, + 'name' | 'slug' | 'attributes' +> & { + attributes: ProductVariationAttribute[]; +}; type Query = Omit< ProductQuery, 'name' >; diff --git a/plugins/woocommerce-admin/client/products/fields/variations/index.ts b/plugins/woocommerce-admin/client/products/fields/variations/index.ts new file mode 100644 index 00000000000..ec5dc0ca358 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/index.ts @@ -0,0 +1 @@ +export * from './variations'; diff --git a/plugins/woocommerce-admin/client/products/fields/variations/variations.scss b/plugins/woocommerce-admin/client/products/fields/variations/variations.scss new file mode 100644 index 00000000000..920aa7db6c3 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/variations.scss @@ -0,0 +1,39 @@ +.woocommerce-product-variations { + min-height: 300px; + + &__header { + display: grid; + grid-template-columns: calc(38px + 25%) 25% 25%; + padding: $gap-small $gap; + + h4 { + color: $gray-700; + font-size: 11px; + text-transform: uppercase; + margin: 0; + font-weight: 500; + } + } + + .woocommerce-list-item { + display: grid; + grid-template-columns: 38px 25% 25% 25%; + margin-left: -1px; + margin-right: -1px; + margin-bottom: -1px; + } + + .woocommerce-sortable { + margin: 0; + } + + .components-spinner { + width: 34px; + height: 34px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + position: absolute; + margin: 0; + } +} diff --git a/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx b/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx new file mode 100644 index 00000000000..3df18ef77de --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fields/variations/variations.tsx @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Card, Spinner } from '@wordpress/components'; +import { + EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, + ProductVariation, +} from '@woocommerce/data'; +import { ListItem, Sortable, Tag } from '@woocommerce/components'; +import { useContext } from '@wordpress/element'; +import { useParams } from 'react-router-dom'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { CurrencyContext } from '../../../lib/currency-context'; +import { getProductStockStatus } from '../../utils/get-product-stock-status'; +import './variations.scss'; + +export const Variations: React.FC = () => { + const { productId } = useParams(); + const context = useContext( CurrencyContext ); + const { formatAmount, getCurrencyConfig } = context; + const { isLoading, variations } = useSelect( ( select ) => { + const { getProductVariations, hasFinishedResolution } = select( + EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME + ); + return { + isLoading: ! hasFinishedResolution( 'getProductVariations', [ + { + product_id: productId, + }, + ] ), + variations: getProductVariations< ProductVariation[] >( { + product_id: productId, + } ), + }; + } ); + + if ( ! variations || isLoading ) { + return ( + + + + ); + } + + const currencyConfig = getCurrencyConfig(); + + return ( + +
+

{ __( 'Variation', 'woocommerce' ) }

+

+ { sprintf( + /** Translators: The 3 letter currency code for the store. */ + __( 'Price (%s)', 'woocommerce' ), + currencyConfig.code + ) } +

+

{ __( 'Quantity', 'woocommerce' ) }

+
+ + { variations.map( ( variation ) => ( + +
+ { variation.attributes.map( ( attribute ) => ( + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + /* @ts-ignore Additional props are not required. */ + + ) ) } +
+
+ { formatAmount( variation.price ) } +
+
+ { getProductStockStatus( variation ) } +
+
+ ) ) } +
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx index adba8194aa5..d2a98a57ca5 100644 --- a/plugins/woocommerce-admin/client/products/product-form.tsx +++ b/plugins/woocommerce-admin/client/products/product-form.tsx @@ -14,6 +14,7 @@ import { ProductDetailsSection } from './sections/product-details-section'; import { ProductInventorySection } from './sections/product-inventory-section'; import { PricingSection } from './sections/pricing-section'; import { ProductShippingSection } from './sections/product-shipping-section'; +import { ProductVariationsSection } from './sections/product-variations-section'; import { ImagesSection } from './sections/images-section'; import './product-page.scss'; import { validate } from './product-validation'; @@ -56,6 +57,9 @@ export const ProductForm: React.FC< { + + + diff --git a/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx new file mode 100644 index 00000000000..a4719e372b6 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/sections/product-variations-section.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { recordEvent } from '@woocommerce/tracks'; +import { Link } from '@woocommerce/components'; + +/** + * Internal dependencies + */ +import { ProductSectionLayout } from '../layout/product-section-layout'; +import { Variations } from '../fields/variations'; + +export const ProductVariationsSection: React.FC = () => { + return ( + + + { __( + 'Manage individual product combinations created from options.', + 'woocommerce' + ) } + + { + recordEvent( 'add_product_variation_help' ); + } } + > + { __( + 'How to make variations work for you', + 'woocommerce' + ) } + + + } + > + + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/utils/get-product-status.ts b/plugins/woocommerce-admin/client/products/utils/get-product-status.ts index 519a57b552e..d5ddb33f7b7 100644 --- a/plugins/woocommerce-admin/client/products/utils/get-product-status.ts +++ b/plugins/woocommerce-admin/client/products/utils/get-product-status.ts @@ -28,7 +28,7 @@ export const PRODUCT_STATUS_LABELS = { * Get the product status for use in the header. * * @param product Product instance. - * @return {PRODUCT_STATUS_KEYS} Product staus key. + * @return {PRODUCT_STATUS_KEYS} Product status key. */ export const getProductStatus = ( product: PartialProduct | undefined diff --git a/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts b/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts new file mode 100644 index 00000000000..39a29cd4b50 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/utils/get-product-stock-status.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PartialProduct, ProductVariation } from '@woocommerce/data'; + +/** + * Labels for product stock statuses. + */ +export enum PRODUCT_STOCK_STATUS_KEYS { + instock = 'instock', + onbackorder = 'onbackorder', + outofstock = 'outofstock', +} + +/** + * Labels for product stock statuses. + */ +export const PRODUCT_STOCK_STATUS_LABELS = { + [ PRODUCT_STOCK_STATUS_KEYS.instock ]: __( 'In stock', 'woocommerce' ), + [ PRODUCT_STOCK_STATUS_KEYS.onbackorder ]: __( + 'On backorder', + 'woocommerce' + ), + [ PRODUCT_STOCK_STATUS_KEYS.outofstock ]: __( + 'Out of stock', + 'woocommerce' + ), +}; + +/** + * Get the product stock quantity or stock status label. + * + * @param product Product instance. + * @return {PRODUCT_STOCK_STATUS_KEYS|number} Product stock quantity or product status key. + */ +export const getProductStockStatus = ( + product: PartialProduct | Partial< ProductVariation > +): string | number => { + if ( product.manage_stock ) { + return product.stock_quantity || 0; + } + + if ( product.stock_status ) { + return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ]; + } + + return PRODUCT_STOCK_STATUS_LABELS.instock; +}; diff --git a/plugins/woocommerce/changelog/add-35772 b/plugins/woocommerce/changelog/add-35772 new file mode 100644 index 00000000000..31bbe41bd70 --- /dev/null +++ b/plugins/woocommerce/changelog/add-35772 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add product variations list to new product management experience From 884d3f4237fa03f5c435d02edc3a9567d06a9c2f Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Wed, 14 Dec 2022 12:36:00 +1300 Subject: [PATCH 025/370] Revert "Delete changelog files based on PR 35669" (#35960) Revert "Delete changelog files based on PR 35669 (#35945)" This reverts commit 97784693ab998c0818fa28a2dd5e5126bef8aef1. --- plugins/woocommerce/changelog/fix-35535 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-35535 diff --git a/plugins/woocommerce/changelog/fix-35535 b/plugins/woocommerce/changelog/fix-35535 new file mode 100644 index 00000000000..a476e006729 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35535 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add a data migration for changed New Zealand and Ukraine state codes From 9070cff9c54d8f3a18cfb72b60a4817df308b155 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 14 Dec 2022 13:34:37 +0800 Subject: [PATCH 026/370] Fix wrong query param in onboarding product api call (#35926) * Fix wrong query param in onboarding product api call * Add changelog * Fix lint --- plugins/woocommerce/changelog/fix-onboarding-api-qurey | 4 ++++ .../src/Internal/Admin/Onboarding/OnboardingProducts.php | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-onboarding-api-qurey diff --git a/plugins/woocommerce/changelog/fix-onboarding-api-qurey b/plugins/woocommerce/changelog/fix-onboarding-api-qurey new file mode 100644 index 00000000000..7c6134e1b0b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-onboarding-api-qurey @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix wrong query param in onboarding product api call diff --git a/plugins/woocommerce/src/Internal/Admin/Onboarding/OnboardingProducts.php b/plugins/woocommerce/src/Internal/Admin/Onboarding/OnboardingProducts.php index fae3f970ffd..578918c954c 100644 --- a/plugins/woocommerce/src/Internal/Admin/Onboarding/OnboardingProducts.php +++ b/plugins/woocommerce/src/Internal/Admin/Onboarding/OnboardingProducts.php @@ -88,10 +88,12 @@ class OnboardingProducts { $woocommerce_products = wp_remote_get( add_query_arg( array( - 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), - 'locale' => $locale, + 'locale' => $locale, ), 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' + ), + array( + 'user-agent' => 'WooCommerce/' . WC()->version . '; ' . get_bloginfo( 'url' ), ) ); if ( is_wp_error( $woocommerce_products ) ) { From 679e875bd2ab54140a5bb7c136bea3fc1a210e7e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 14 Dec 2022 13:53:52 +0800 Subject: [PATCH 027/370] Fix ellipsis dropdown is hidden in task list (#35949) * Fix the ellipsis dropdown menu is mostly hidden within the task list * Add changelog --- plugins/woocommerce-admin/client/tasks/task-list.scss | 1 - .../changelog/fix-35505-ellipsis-dropdown-is-hidden | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-35505-ellipsis-dropdown-is-hidden diff --git a/plugins/woocommerce-admin/client/tasks/task-list.scss b/plugins/woocommerce-admin/client/tasks/task-list.scss index d9b5df78a3f..b3018b39542 100644 --- a/plugins/woocommerce-admin/client/tasks/task-list.scss +++ b/plugins/woocommerce-admin/client/tasks/task-list.scss @@ -1,5 +1,4 @@ .woocommerce-task-list__item { - overflow: hidden; &.woocommerce-list__item-enter { opacity: 0; max-height: 0; diff --git a/plugins/woocommerce/changelog/fix-35505-ellipsis-dropdown-is-hidden b/plugins/woocommerce/changelog/fix-35505-ellipsis-dropdown-is-hidden new file mode 100644 index 00000000000..5d3a5cf016b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35505-ellipsis-dropdown-is-hidden @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix ellipsis dropdown menu is mostly hidden within the task list From 5786da40304b6210fe2f8f75de490ac5e8e7f0f8 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 14 Dec 2022 15:40:51 +0800 Subject: [PATCH 028/370] Disable TikTok in OBW (#35924) * Disable TikTok in the OBW * Add changelog --- plugins/woocommerce/changelog/update-disable-tiktok-in-obw | 4 ++++ .../Admin/RemoteFreeExtensions/DefaultFreeExtensions.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/update-disable-tiktok-in-obw diff --git a/plugins/woocommerce/changelog/update-disable-tiktok-in-obw b/plugins/woocommerce/changelog/update-disable-tiktok-in-obw new file mode 100644 index 00000000000..d638bb00748 --- /dev/null +++ b/plugins/woocommerce/changelog/update-disable-tiktok-in-obw @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Disable TikTok in the OBW diff --git a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php index dd32405d1e5..fb91c615049 100644 --- a/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php +++ b/plugins/woocommerce/src/Internal/Admin/RemoteFreeExtensions/DefaultFreeExtensions.php @@ -670,7 +670,7 @@ class DefaultFreeExtensions { ], 'is_built_by_wc' => false, ], - 'tiktok-for-business:alt' => [ + 'tiktok-for-business:alt' => [ 'name' => __( 'TikTok for WooCommerce', 'woocommerce' ), 'image_url' => plugins_url( '/assets/images/onboarding/tiktok.svg', WC_PLUGIN_FILE ), 'description' => sprintf( @@ -681,6 +681,7 @@ class DefaultFreeExtensions { ), 'manage_url' => 'admin.php?page=tiktok', 'is_built_by_wc' => false, + 'is_visible' => false, ], ); From 4ac1d822ac9082102536b0f7aa9cb0553965adaa Mon Sep 17 00:00:00 2001 From: timur987 <115007291+timur987@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:59:42 +0300 Subject: [PATCH 029/370] Update In-App Marketplace tour wording (#35929) --- .../client/guided-tours/wc-addons-tour/get-steps.ts | 6 +++--- .../woocommerce/changelog/update-in-app-marketplace-tour | 4 ++++ .../Features/OnboardingTasks/Tasks/TourInAppMarketplace.php | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-in-app-marketplace-tour diff --git a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts index 28776f00b79..5eb00fc2ba5 100644 --- a/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts +++ b/plugins/woocommerce-admin/client/guided-tours/wc-addons-tour/get-steps.ts @@ -24,7 +24,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { descriptions: { desktop: createInterpolateElement( __( - 'This is the place to find extensions, themes, and services for your store - all reviewed and approved by the WooCommerce team.

Whether you’re looking to improve your store or grow your business, you can find a solution here. There are hundreds of options available, and new products are added regularly.

The WooCommerce Marketplace is also available at WooCommerce.com.', + 'Power up your store by adding extra functionality using extensions, find a fresh new look with themes, or integrate your store with other software and services.

The WooCommerce Marketplace is your go-to for all of the above, and the only place you’ll find products that have been reviewed and approved by the WooCommerce team.

Whether you’re looking to improve your store or grow your business, you can find a solution here. There are hundreds of options available, and new products are added regularly.

The WooCommerce Marketplace is also available at WooCommerce.com.', 'woocommerce' ), { @@ -46,7 +46,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { heading: __( 'Find exactly what you need', 'woocommerce' ), descriptions: { desktop: __( - 'Use the search box to find specific products.', + 'Use the search box to find specific products or solutions.', 'woocommerce' ), }, @@ -111,7 +111,7 @@ export const getSteps = (): TourKitTypes.WooStep[] => { descriptions: { desktop: createInterpolateElement( __( - "Products purchased from the WooCommerce Marketplace can be managed in My Subscriptions, either here or on WooCommerce.com.

Every purchase is backed by our 30-day money-back guarantee, and includes email and live chat support.

That's it! We hope the Marketplace helps you build the business of your dreams.", + "Products purchased from the WooCommerce Marketplace can be managed in My Subscriptions, either here or on WooCommerce.com.

Every purchase is backed by our 30-day money-back guarantee, and includes email and live chat support.

That's it! We hope the WooCommerce Marketplace helps you build the business of your dreams.", 'woocommerce' ), { diff --git a/plugins/woocommerce/changelog/update-in-app-marketplace-tour b/plugins/woocommerce/changelog/update-in-app-marketplace-tour new file mode 100644 index 00000000000..a8f74e28c20 --- /dev/null +++ b/plugins/woocommerce/changelog/update-in-app-marketplace-tour @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update wording for In-App Marketplace tour. diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/TourInAppMarketplace.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/TourInAppMarketplace.php index ccc8f98fb0d..a8ce54218f1 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/TourInAppMarketplace.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/TourInAppMarketplace.php @@ -23,7 +23,10 @@ class TourInAppMarketplace extends Task { * @return string */ public function get_title() { - return __( 'Tour the WooCommerce Marketplace', 'woocommerce' ); + return __( + 'Discover where to find powerful store add-ons and integrations, with a WooCommerce Marketplace tour', + 'woocommerce' + ); } /** From d19c20491e5a7ade64c8fd530f01e0f3f3f7e29c Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Wed, 14 Dec 2022 09:50:10 -0300 Subject: [PATCH 030/370] Add a default placeholder title for newly added attributes and always show remove button for attributes (#35904) * Remove CSS that hides the 'Remove' button for product attributes * Add default placeholder title 'Custom attribute' when user adds a new attribute * Add changelog * Add missing esc_html_e * Try to fix PHPCS * Add placeholder value for Attribute name input * Add css and logic to make placeholder title have opacity and remove opacity class after user types the attribute name at the input * Update placeholder value * Fix wrong labels I added e.g. Fabric or Brand to the wrong place. --- .../woocommerce/changelog/enhancement-35116 | 4 ++++ .../woocommerce/client/legacy/css/admin.scss | 22 ++++++++----------- .../legacy/js/admin/meta-boxes-product.js | 10 ++++++--- .../views/html-product-attribute.php | 4 ++-- 4 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 plugins/woocommerce/changelog/enhancement-35116 diff --git a/plugins/woocommerce/changelog/enhancement-35116 b/plugins/woocommerce/changelog/enhancement-35116 new file mode 100644 index 00000000000..c41703b1ec3 --- /dev/null +++ b/plugins/woocommerce/changelog/enhancement-35116 @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Add a default placeholder title for newly added attributes and always show remove button for attributes diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss index 91eaf5d465b..ef4043efed3 100644 --- a/plugins/woocommerce/client/legacy/css/admin.scss +++ b/plugins/woocommerce/client/legacy/css/admin.scss @@ -962,7 +962,7 @@ #variable_product_options .notice { display: flex; margin: 10px; - background-color: #FCFAE8; + background-color: #fcfae8; > p { width: 85%; } @@ -977,7 +977,7 @@ } .woocommerce-set-price-variations { - .woocommerce-usage-modal__wrapper{ + .woocommerce-usage-modal__wrapper { .woocommerce-usage-modal__message { height: 60px; flex-wrap: wrap; @@ -990,7 +990,7 @@ display: flex; justify-content: flex-end; margin-top: 20px; - > button{ + > button { margin-left: 16px; width: 88px; display: unset; @@ -1013,7 +1013,8 @@ #product_attributes { .toolbar-top { - .button, .select2-container { + .button, + .select2-container { margin: 1px; } } @@ -5169,14 +5170,6 @@ img.help_tip { } } -.woocommerce_attribute { - h3 { - .sort, a.delete { - visibility: hidden; - } - } -} - .woocommerce_options_panel { min-height: 175px; box-sizing: border-box; @@ -5644,6 +5637,9 @@ img.help_tip { float: right; } } + .placeholder { + opacity: 0.4; + } } } @@ -7861,7 +7857,7 @@ table.bar_chart { #postdivrich.woocommerce-product-description { margin-top: 20px; margin-bottom: 0px; - + .wp-editor-tools { background: none; padding-top: 0px; diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js index 4e342761f68..3891fcc55d1 100644 --- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js +++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js @@ -520,10 +520,14 @@ jQuery( function ( $ ) { } ); $( '.product_attributes' ).on( 'blur', 'input.attribute_name', function () { - $( this ) + var text = $( this ).val(); + var attributeName = $( this ) .closest( '.woocommerce_attribute' ) - .find( 'strong.attribute_name' ) - .text( $( this ).val() ); + .find( 'strong.attribute_name' ); + var isPlaceholder = attributeName.hasClass( 'placeholder' ); + if ( ( isPlaceholder && text ) || ! isPlaceholder ) { + attributeName.removeClass( 'placeholder' ).text( text ); + } } ); $( '.product_attributes' ).on( diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php index cec626d8d7b..bd836803ae4 100644 --- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php +++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute.php @@ -8,7 +8,7 @@ if ( ! defined( 'ABSPATH' ) ) {
- get_name() ); ?> + get_name() !== '' ? wc_attribute_label( $attribute->get_name() ) : __( 'Custom attribute', 'woocommerce' ) ); ?>